Commit realizado el 12:13:52 08-04-2024

This commit is contained in:
Pagina Web Monito
2024-04-08 12:13:55 -04:00
commit 0c33094de9
7815 changed files with 1365694 additions and 0 deletions

View File

@@ -0,0 +1,91 @@
<?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName
/**
* The Sitemap xml and stylesheet abstract class.
*
* @since 0.9.0
* @package RankMath
* @subpackage RankMath\Sitemap
* @author Rank Math <support@rankmath.com>
*
* @copyright Copyright (C) 2008-2019, Yoast BV
* The following code is a derivative work of the code from the Yoast(https://github.com/Yoast/wordpress-seo/), which is licensed under GPL v3.
*/
namespace RankMath\Sitemap;
defined( 'ABSPATH' ) || exit;
/**
* XML.
*/
abstract class XML {
/**
* HTTP protocol to use in headers.
*
* @var string
*/
protected $http_protocol = null;
/**
* Holds charset of output, might be converted.
*
* @var string
*/
protected $output_charset = 'UTF-8';
/**
* Send file headers.
*
* @param array $headers Array of headers.
* @param bool $is_xsl True if sending headers are for XSL.
*/
protected function send_headers( $headers = [], $is_xsl = false ) {
$defaults = [
'X-Robots-Tag' => 'noindex',
'Content-Type' => 'text/xml; charset=' . $this->get_output_charset(),
'Pragma' => 'public',
'Cache-Control' => 'no-cache, no-store, must-revalidate, max-age=0',
'Expires' => 0,
];
$headers = wp_parse_args( $headers, $defaults );
/**
* Filter the sitemap HTTP headers.
*
* @param array $headers HTTP headers.
* @param bool $is_xsl Whether these headers are for XSL.
*/
$headers = $this->do_filter( 'sitemap/http_headers', $headers, $is_xsl );
header( $this->get_protocol() . ' 200 OK', true, 200 );
foreach ( $headers as $header => $value ) {
header( $header . ': ' . $value );
}
}
/**
* Get HTTP protocol.
*
* @return string
*/
protected function get_protocol() {
if ( ! is_null( $this->http_protocol ) ) {
return $this->http_protocol;
}
$this->http_protocol = ! empty( $_SERVER['SERVER_PROTOCOL'] ) ? sanitize_text_field( $_SERVER['SERVER_PROTOCOL'] ) : 'HTTP/1.1';
return $this->http_protocol;
}
/**
* Get `charset` for the output.
*
* @return string
*/
protected function get_output_charset() {
return $this->output_charset;
}
}

View File

@@ -0,0 +1,382 @@
<?php
/**
* The Sitemap module admin side functionality.
*
* @since 0.9.0
* @package RankMath
* @subpackage RankMath\Sitemap
* @author Rank Math <support@rankmath.com>
*/
namespace RankMath\Sitemap;
use RankMath\KB;
use RankMath\Helper;
use RankMath\Traits\Ajax;
use RankMath\Module\Base;
use RankMath\Admin\Options;
use RankMath\Helpers\Str;
use RankMath\Helpers\Param;
defined( 'ABSPATH' ) || exit;
/**
* Admin class.
*/
class Admin extends Base {
use Ajax;
/**
* Module ID.
*
* @var string
*/
public $id = '';
/**
* Module directory.
*
* @var string
*/
public $directory = '';
/**
* The Constructor.
*/
public function __construct() {
$directory = dirname( __FILE__ );
$this->config(
[
'id' => 'sitemap',
'directory' => $directory,
]
);
parent::__construct();
$this->action( 'init', 'register_setting_page', 999 );
$this->action( 'admin_footer', 'admin_scripts' );
$this->filter( 'rank_math/settings/sitemap', 'post_type_settings' );
$this->filter( 'rank_math/settings/sitemap', 'taxonomy_settings' );
// Attachment.
$this->filter( 'media_send_to_editor', 'media_popup_html', 10, 2 );
if ( Helper::has_cap( 'sitemap' ) ) {
$this->filter( 'attachment_fields_to_edit', 'media_popup_fields', 20, 2 );
$this->filter( 'attachment_fields_to_save', 'media_popup_fields_save', 20, 2 );
}
$this->ajax( 'remove_nginx_notice', 'remove_nginx_notice' );
}
/**
* Register setting page.
*/
public function register_setting_page() {
$sitemap_url = Router::get_base_url( Sitemap::get_sitemap_index_slug() . '.xml' );
$tabs = [
'general' => [
'icon' => 'rm-icon rm-icon-settings',
'title' => esc_html__( 'General', 'rank-math' ),
'file' => $this->directory . '/settings/general.php',
'desc' => esc_html__( 'This tab contains General settings related to the XML sitemaps.', 'rank-math' ) . ' <a href="' . KB::get( 'sitemap-general', 'Options Panel Sitemap General Tab' ) . '" target="_blank">' . esc_html__( 'Learn more', 'rank-math' ) . '</a>',
/* translators: sitemap url */
'after_row' => $this->get_notice_start() . sprintf( esc_html__( 'Your sitemap index can be found here: %s', 'rank-math' ), '<a href="' . $sitemap_url . '" target="_blank">' . $sitemap_url . '</a>' ) . '</p></div>' . $this->get_nginx_notice(),
],
];
$tabs['html_sitemap'] = [
'icon' => 'rm-icon rm-icon-sitemap',
'title' => esc_html__( 'HTML Sitemap', 'rank-math' ),
'file' => $this->directory . '/settings/html-sitemap.php',
'desc' => esc_html__( 'This tab contains settings related to the HTML sitemap.', 'rank-math' ) . ' <a href="' . KB::get( 'sitemap-general', 'Options Panel Sitemap HTML Tab' ) . '" target="_blank">' . esc_html__( 'Learn more', 'rank-math' ) . '</a>',
'classes' => 'html-sitemap',
];
if ( Helper::is_author_archive_indexable() ) {
$tabs['authors'] = [
'icon' => 'rm-icon rm-icon-users',
'title' => esc_html__( 'Authors', 'rank-math' ),
/* translators: Learn more link. */
'desc' => sprintf( esc_html__( 'Set the sitemap options for author archive pages. %s.', 'rank-math' ), '<a href="' . KB::get( 'configure-sitemaps', 'Options Panel Sitemap Authors Tab' ) . '#authors" target="_blank">' . esc_html__( 'Learn more', 'rank-math' ) . '</a>' ),
'file' => $this->directory . '/settings/authors.php',
];
}
$tabs = $this->do_filter( 'settings/sitemap', $tabs );
new Options(
[
'key' => 'rank-math-options-sitemap',
'title' => esc_html__( 'Sitemap Settings', 'rank-math' ),
'menu_title' => esc_html__( 'Sitemap Settings', 'rank-math' ),
'capability' => 'rank_math_sitemap',
'folder' => 'titles',
'position' => 99,
'tabs' => $tabs,
]
);
}
/**
* Add post type tabs in the Sitemap Settings options panel.
*
* @param array $tabs Hold tabs for the options panel.
*
* @return array
*/
public function post_type_settings( $tabs ) {
$icons = Helper::choices_post_type_icons();
$things = [
'attachment' => esc_html__( 'attachments', 'rank-math' ),
'product' => esc_html__( 'your product pages', 'rank-math' ),
];
$urls = [
'post' => KB::get( 'sitemap-post', 'Options Panel Sitemap Posts Tab' ),
'page' => KB::get( 'sitemap-page', 'Options Panel Sitemap Page Tab' ),
'attachment' => KB::get( 'sitemap-media', 'Options Panel Sitemap Attachments Tab' ),
'product' => KB::get( 'sitemap-product', 'Options Panel Sitemap Product Tab' ),
];
// Post type label seprator.
$tabs['p_types'] = [
'title' => esc_html__( 'Post Types:', 'rank-math' ),
'type' => 'seprator',
];
foreach ( Helper::get_accessible_post_types() as $post_type ) {
$object = get_post_type_object( $post_type );
$sitemap_url = Router::get_base_url( $object->name . '-sitemap.xml' );
$notice_end = '</p><div class="rank-math-cmb-dependency hidden" data-relation="or"><span class="hidden" data-field="pt_' . $post_type . '_sitemap" data-comparison="=" data-value="on"></span></div></div>';
$name = strtolower( $object->label );
/* translators: Post Type label */
$thing = isset( $things[ $post_type ] ) ? $things[ $post_type ] : sprintf( __( 'single %s', 'rank-math' ), $name );
$url = isset( $urls[ $post_type ] ) ? $urls[ $post_type ] : KB::get( 'configure-sitemaps' );
$tabs[ 'sitemap-post-type-' . $object->name ] = [
'title' => 'attachment' === $post_type ? esc_html__( 'Attachments', 'rank-math' ) : $object->label,
'icon' => isset( $icons[ $object->name ] ) ? $icons[ $object->name ] : $icons['default'],
/* translators: %1$s: thing, %2$s: Learn more link. */
'desc' => sprintf( esc_html__( 'Change Sitemap settings of %1$s. %2$s.', 'rank-math' ), $thing, '<a href="' . $url . '" target="_blank">' . esc_html__( 'Learn more', 'rank-math' ) . '</a>' ),
'post_type' => $object->name,
'file' => $this->directory . '/settings/post-types.php',
/* translators: Post Type Sitemap Url */
'after_row' => $this->get_notice_start() . sprintf( esc_html__( 'Sitemap URL: %s', 'rank-math' ), '<a href="' . $sitemap_url . '" target="_blank">' . $sitemap_url . '</a>' ) . $notice_end,
];
if ( 'attachment' === $post_type ) {
$tabs[ 'sitemap-post-type-' . $object->name ]['after_row'] = $this->get_notice_start() . esc_html__( 'Please note that this will add the attachment page URLs to the sitemap, not direct image URLs.', 'rank-math' ) . $notice_end;
$tabs[ 'sitemap-post-type-' . $object->name ]['classes'] = 'rank-math-advanced-option';
}
}
return $tabs;
}
/**
* Add taxonomy tabs in the Sitemap Settings options panel.
*
* @param array $tabs Hold tabs for the options panel.
*
* @return array
*/
public function taxonomy_settings( $tabs ) {
$icons = Helper::choices_taxonomy_icons();
// Taxonomy label seprator.
$tabs['t_types'] = [
'title' => esc_html__( 'Taxonomies:', 'rank-math' ),
'type' => 'seprator',
];
foreach ( Helper::get_accessible_taxonomies() as $taxonomy ) {
if ( 'post_format' === $taxonomy->name ) {
continue;
}
$hash_links = [
'category' => '#categories',
'post_tag' => '#tags',
'product_cat' => '#product-categories',
'product_tag' => '#product-tags',
];
$sitemap_url = Router::get_base_url( $taxonomy->name . '-sitemap.xml' );
$notice_end = '</p><div class="rank-math-cmb-dependency hidden" data-relation="or"><span class="hidden" data-field="tax_' . $taxonomy->name . '_sitemap" data-comparison="=" data-value="on"></span></div></div>';
$taxonomy_name = strtolower( $taxonomy->name );
$url = isset( $hash_links[ $taxonomy_name ] ) ? KB::get( 'configure-sitemaps', 'Options Panel Sitemap ' . $taxonomy->labels->name . ' Tab' ) . $hash_links[ $taxonomy_name ] : KB::get( 'configure-sitemaps' );
switch ( $taxonomy->name ) {
case 'product_cat':
case 'product_tag':
/* translators: Taxonomy singular label */
$thing = sprintf( __( 'your product %s pages', 'rank-math' ), strtolower( $taxonomy->labels->singular_name ) );
break;
default:
/* translators: Taxonomy singular label */
$thing = sprintf( __( '%s archives', 'rank-math' ), strtolower( $taxonomy->labels->singular_name ) );
$name = strtolower( $taxonomy->labels->name );
}
$tabs[ 'sitemap-taxonomy-' . $taxonomy->name ] = [
'icon' => isset( $icons[ $taxonomy->name ] ) ? $icons[ $taxonomy->name ] : $icons['default'],
'title' => $taxonomy->label,
/* translators: %1$s: thing, %2$s: Learn more link. */
'desc' => sprintf( esc_html__( 'Change Sitemap settings of %1$s. %2$s.', 'rank-math' ), $thing, '<a href="' . $url . '" target="_blank">' . esc_html__( 'Learn more', 'rank-math' ) . '</a>' ),
'taxonomy' => $taxonomy->name,
'file' => $this->directory . '/settings/taxonomies.php',
/* translators: Taxonomy Sitemap Url */
'after_row' => $this->get_notice_start() . sprintf( esc_html__( 'Sitemap URL: %s', 'rank-math' ), '<a href="' . $sitemap_url . '" target="_blank">' . $sitemap_url . '</a>' ) . $notice_end,
];
}
return $tabs;
}
/**
* Adds new "exclude from sitemap" checkbox to the media popup in the post editor.
*
* @param array $form_fields Default form fields.
* @param object $post Current post.
*
* @return array New form fields
*/
public function media_popup_fields( $form_fields, $post ) {
$exclude = get_post_meta( $post->ID, 'rank_math_exclude_sitemap', true );
$checkbox = '<label><input type="checkbox" name="attachments[' . $post->ID . '][rank_math_media_exclude_sitemap]" ' . checked( $exclude, true, 0 ) . ' /> ';
$checkbox .= esc_html__( 'Exclude this attachment from sitemap', 'rank-math' ) . '</label>';
$form_fields['rank_math_exclude_sitemap'] = [ 'tr' => "\t\t<tr><td></td><td>$checkbox</td></tr>\n" ];
return $form_fields;
}
/**
* Saves new "exclude from sitemap" field as post meta for the attachment.
*
* @param array $post Attachment ID.
* @param array $attachment Attachment data.
*
* @return array Post
*/
public function media_popup_fields_save( $post, $attachment ) {
if ( isset( $attachment['rank_math_media_exclude_sitemap'] ) ) {
update_post_meta( $post['ID'], 'rank_math_exclude_sitemap', true );
} else {
delete_post_meta( $post['ID'], 'rank_math_exclude_sitemap' );
}
Cache_Watcher::invalidate_post( $post['ID'] );
return $post;
}
/**
* Adds the "data-sitemapexclude" HTML attribute to the img tag in the post
* editor when necessary.
*
* @param string $html Original img HTML tag.
* @param int $attachment_id Attachment ID.
*
* @return string New img HTML tag.
*/
public function media_popup_html( $html, $attachment_id ) {
$post = get_post( $attachment_id );
if ( Str::starts_with( 'image', $post->post_mime_type ) && get_post_meta( $attachment_id, 'rank_math_exclude_sitemap', true ) ) {
$html = str_replace( ' class="', ' data-sitemapexclude="true" class="', $html );
}
return $html;
}
/**
* Remove Sitemap nginx notice.
*
* @since 1.0.73
*/
public function remove_nginx_notice() {
check_ajax_referer( 'rank-math-ajax-nonce', 'security' );
$this->has_cap_ajax( 'sitemap' );
update_option( 'rank_math_remove_nginx_notice', true, false );
$this->success();
}
/**
* Get opening tags for the notice HTML.
*
* @return string
*/
private function get_notice_start() {
return '<div class="notice notice-alt notice-info info inline rank-math-notice"><p>';
}
/**
* Get nginx notice.
*
* @since 1.0.41
*
* @return string
*/
private function get_nginx_notice() {
if ( 'rank-math-options-sitemap' !== Param::get( 'page' ) || empty( Param::server( 'SERVER_SOFTWARE' ) ) || get_option( 'rank_math_remove_nginx_notice' ) ) {
return '';
}
$server_software = explode( '/', Param::server( 'SERVER_SOFTWARE' ) );
if ( ! in_array( 'nginx', array_map( 'strtolower', $server_software ), true ) ) {
return '';
}
$sitemap_base = Router::get_sitemap_base() ? Router::get_sitemap_base() : '';
$message = sprintf(
/* Translators: the placeholder is for the sitemap base url. */
__( 'Since you are using an NGINX server, you may need to add the following code to your %s <strong>if your Sitemap pages are not loading</strong>. If you are unsure how to do it, please contact your hosting provider.', 'rank-math' ),
'<a href="https://help.dreamhost.com/hc/en-us/articles/216455077-Nginx-configuration-file-locations/?utm_campaign=Rank+Math" target="_blank">' . __( 'configuration file', 'rank-math' ) . '</a>'
);
return '<div class="sitemap-nginx-notice notice notice-alt notice-warning rank-math-notice">' .
'<p>' . $message .
' <a href="#"><span class="show">' . __( 'Click here to see the code.', 'rank-math' ) . '</span><span class="hide">' . __( 'Hide', 'rank-math' ) . '</span></a>
<a href="#" class="sitemap-close-notice">' . __( 'I already added', 'rank-math' ) . '</a>
</p>
<pre>
# START Nginx Rewrites for Rank Math Sitemaps
rewrite ^/' . $sitemap_base . Sitemap::get_sitemap_index_slug() . '\\.xml$ /index.php?sitemap=1 last;
rewrite ^/' . $sitemap_base . '([^/]+?)-sitemap([0-9]+)?.xml$ /index.php?sitemap=$1&sitemap_n=$2 last;
# END Nginx Rewrites for Rank Math Sitemaps
</pre>
</div>';
}
/**
* Add some inline JS for the sitemap settings admin page.
*/
public function admin_scripts() {
if ( 'rank-math-options-sitemap' !== Param::get( 'page' ) ) {
return;
}
?>
<script>
jQuery( function( $ ) {
$( '.cmb2-id-html-sitemap-seo-titles input' ).on( 'change', function() {
if ( 'seo_titles' === $( this ).filter(':checked').val() ) {
$( '#html_sitemap_sort option[value="alphabetical"]' ).prop( 'disabled', true );
if ( $( '#html_sitemap_sort option:selected' ).prop( 'disabled' ) ) {
$( '#html_sitemap_sort option:first' ).prop( 'selected', true );
}
} else {
$( '#html_sitemap_sort option[value="alphabetical"]' ).prop( 'disabled', false );
}
} ).trigger( 'change' );
} );
</script>
<?php
}
}

View File

@@ -0,0 +1,355 @@
<?php
/**
* The sitemap cache watcher.
*
* @since 0.9.0
* @package RankMath
* @subpackage RankMath\Sitemap
* @author Rank Math <support@rankmath.com>
*
* @copyright Copyright (C) 2008-2019, Yoast BV
* The following code is a derivative work of the code from the Yoast(https://github.com/Yoast/wordpress-seo/), which is licensed under GPL v3.
*/
namespace RankMath\Sitemap;
use RankMath\Helper;
use RankMath\Sitemap\Sitemap;
use RankMath\Traits\Hooker;
defined( 'ABSPATH' ) || exit;
/**
* Cache_Watcher class.
*/
class Cache_Watcher {
use Hooker;
/**
* Holds the options that, when updated, should cause the cache to clear.
*
* @var array
*/
protected static $cache_clear = [];
/**
* Holds the flag to clear all cache.
*
* @var boolean
*/
protected static $clear_all = false;
/**
* Holds the array of types to clear.
*
* @var array
*/
protected static $clear_types = [];
/**
* Holds the array of post types being imported.
*
* @var array
*/
private $importing_post_types = [];
/**
* The constructor.
*/
public function __construct() {
$this->action( 'save_post', 'save_post' );
$this->action( 'transition_post_status', 'status_transition', 10, 3 );
$this->action( 'admin_footer', 'status_transition_bulk_finished' );
$this->action( 'edited_terms', 'invalidate_term', 10, 2 );
$this->action( 'clean_term_cache', 'invalidate_term', 10, 2 );
$this->action( 'clean_object_term_cache', 'invalidate_term', 10, 2 );
$this->action( 'delete_user', 'invalidate_author' );
$this->action( 'user_register', 'invalidate_author' );
$this->action( 'profile_update', 'invalidate_author' );
$this->action( 'rank_math/sitemap/invalidate_object_type', 'invalidate_object_type', 10, 2 );
add_action( 'shutdown', [ __CLASS__, 'clear_queued' ] );
add_action( 'update_option', [ __CLASS__, 'clear_on_option_update' ] );
add_action( 'deleted_term_relationships', [ __CLASS__, 'invalidate' ] );
// Clear cache when user updates any of these options.
self::register_clear_on_option_update( 'home' );
self::register_clear_on_option_update( 'permalink_structure' );
self::register_clear_on_option_update( 'rank_math_modules' );
self::register_clear_on_option_update( 'rank-math-options-titles' );
self::register_clear_on_option_update( 'rank-math-options-general' );
self::register_clear_on_option_update( 'rank-math-options-sitemap' );
self::register_clear_on_option_update( 'date_format' );
}
/**
* Check for relevant post type before invalidation.
*
* @param int $post_id Post ID to possibly invalidate for.
*/
public function save_post( $post_id ) {
$post = get_post( $post_id );
if ( ! empty( $post->post_password ) || 'auto-draft' === $post->post_status ) {
return false;
}
update_user_meta( $post->post_author, 'last_update', get_post_modified_time( 'U', false, $post ) );
$this->invalidate_post( $post_id );
$this->invalidate_author( $post->post_author );
}
/**
* 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 ( 'publish' !== $new_status ) {
return;
}
if ( defined( 'WP_IMPORTING' ) ) {
$this->status_transition_bulk( $new_status, $old_status, $post );
return;
}
if ( $this->can_exclude( $post ) ) {
return;
}
if ( WP_CACHE ) {
wp_schedule_single_event( ( time() + 300 ), 'rank_math/sitemap/hit_index' );
}
}
/**
* Can exclude post type.
*
* @param \WP_Post $post Post object.
*
* @return bool
*/
private function can_exclude( $post ) {
$post_type = get_post_type( $post );
wp_cache_delete( 'lastpostmodified:gmt:' . $post_type, 'timeinfo' );
// None of our interest.
// If the post type is excluded in options, we can stop.
return 'nav_menu_item' === $post_type || ! Sitemap::is_object_indexable( $post->ID );
}
/**
* 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' ) || empty( $this->importing_post_types ) ) {
return;
}
if ( false === $this->maybe_ping_search_engines() ) {
return;
}
if ( WP_CACHE ) {
do_action( 'rank_math/sitemap/hit_index' );
}
}
/**
* Check if we can ping search engines.
*
* @return bool
*/
private function maybe_ping_search_engines() {
$ping = false;
$accessible_post_types = Helper::get_accessible_post_types();
foreach ( $this->importing_post_types as $post_type ) {
wp_cache_delete( 'lastpostmodified:gmt:' . $post_type, 'timeinfo' );
if ( in_array( $post_type, $accessible_post_types, true ) ) {
$ping = true;
}
}
return $ping;
}
/**
* Helper to invalidate in hooks where type is passed as second argument.
*
* @param int $unused Unused term ID value.
* @param string $taxonomy Taxonomy to invalidate.
*/
public static function invalidate_term( $unused, $taxonomy ) {
if ( false !== Helper::get_settings( 'sitemap.tax_' . $taxonomy . '_sitemap' ) ) {
self::invalidate( $taxonomy );
}
}
/**
* Delete cache transients for index and specific type.
*
* Always deletes the main index sitemaps cache, as that's always invalidated by any other change.
*
* @param string $type Sitemap type to invalidate.
*/
public static function invalidate( $type ) {
self::clear( [ $type ] );
}
/**
* Invalidate sitemap cache for the post type of a post.
* Don't invalidate for revisions.
*
* @param int $post_id Post ID to invalidate type for.
*/
public static function invalidate_post( $post_id ) {
if ( wp_is_post_revision( $post_id ) ) {
return;
}
self::invalidate( get_post_type( $post_id ) );
}
/**
* Invalidate sitemap cache for authors.
*
* @param int $user_id User ID.
*/
public static function invalidate_author( $user_id ) {
$user = get_user_by( 'id', $user_id );
if ( 'user_register' === current_action() || 'profile_update' === current_action() ) {
update_user_meta( $user_id, 'last_update', time() );
}
if ( $user && ! is_null( $user->roles ) && ! in_array( 'subscriber', $user->roles, true ) ) {
self::invalidate( 'author' );
}
}
/**
* Function to clear the Sitemap Cache.
*
* @param string $object_type Object type for destination where to save.
* @param int $object_id Object id for destination where to save.
*
* @return void
*/
public static function invalidate_object_type( $object_type, $object_id ) {
if ( 'post' === $object_type ) {
self::invalidate_post( $object_id );
return;
}
if ( 'user' === $object_type ) {
self::invalidate_author( $object_id );
return;
}
if ( 'term' === $object_type ) {
$term = get_term( $object_id );
self::invalidate_term( $object_id, $term->taxonomy );
}
}
/**
* Delete cache transients for given sitemaps types or all by default.
*
* @param array $types Set of sitemap types to delete cache transients for.
*/
public static function clear( $types = [] ) {
if ( ! Sitemap::is_cache_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( '1', $types, true ) ) {
array_unshift( $types, '1' );
}
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 ) {
Cache::invalidate_storage();
self::$clear_all = false;
self::$clear_types = [];
return;
}
foreach ( self::$clear_types as $type ) {
Cache::invalidate_storage( $type );
}
self::$clear_types = [];
}
/**
* Adds a hook that when given option is updated, the cache is cleared.
*
* @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.
*
* @param string $option The option name that's being updated.
*/
public static function clear_on_option_update( $option ) {
if ( ! array_key_exists( $option, self::$cache_clear ) ) {
return;
}
// Clear all caches.
if ( empty( self::$cache_clear[ $option ] ) ) {
self::clear();
return;
}
// Clear specific provided type(s).
$types = (array) self::$cache_clear[ $option ];
self::clear( $types );
}
}

View File

@@ -0,0 +1,271 @@
<?php
/**
* Handle sitemap caching and invalidation.
*
* @since 0.9.0
* @package RankMath
* @subpackage RankMath\Sitemap
* @author Rank Math <support@rankmath.com>
*
* @copyright Copyright (C) 2008-2019, Yoast BV
* The following code is a derivative work of the code from the Yoast(https://github.com/Yoast/wordpress-seo/), which is licensed under GPL v3.
*/
namespace RankMath\Sitemap;
use RankMath\Helper;
use RankMath\Admin\Database\Database;
defined( 'ABSPATH' ) || exit;
/**
* Cache class.
*/
class Cache {
/**
* Cache mode.
*
* @var string
*/
private $mode = 'db';
/**
* The $wp_filesystem object.
*
* @var object WP_Filesystem
*/
private $wp_filesystem;
/**
* Prefix of the filename for sitemap caches.
*
* @var string
*/
const STORAGE_KEY_PREFIX = 'rank_math_';
/**
* The constructor.
*/
public function __construct() {
$this->wp_filesystem = Helper::get_filesystem();
$this->mode = $this->is_writable() ? 'file' : 'db';
/**
* Change sitemap caching mode (can be "file" or "db").
*/
$this->mode = apply_filters( 'rank_math/sitemap/cache_mode', $this->mode );
}
/**
* Is the file writable?
*
* @return bool
*/
public function is_writable() {
if ( is_null( $this->wp_filesystem ) || ! Helper::is_filesystem_direct() ) {
return false;
}
$directory_separator = '/';
$folder_path = $this->get_cache_directory();
$test_file = $folder_path . $this->get_storage_key();
// If folder doesn't exist?
if ( ! file_exists( $folder_path ) ) {
// Can we create the folder?
// returns true if yes and false if not.
$permissions = ( defined( 'FS_CHMOD_DIR' ) ) ? FS_CHMOD_DIR : 0755;
return $this->wp_filesystem->mkdir( $folder_path, $permissions );
}
// Does the file exist?
// File exists. Is it writable?
if ( file_exists( $test_file ) && ! $this->wp_filesystem->is_writable( $test_file ) ) {
// Nope, it's not writable.
return false;
}
// Folder exists, but is it actually writable?
return $this->wp_filesystem->is_writable( $folder_path );
}
/**
* Get the sitemap that is cached.
*
* @param string $type Sitemap type.
* @param int $page Page number to retrieve.
* @param bool $html Is HTML sitemap.
* @return false|string false on no cache found otherwise sitemap file.
*/
public function get_sitemap( $type, $page, $html = false ) {
$filename = $this->get_storage_key( $type, $page, $html );
if ( false === $filename || is_null( $this->wp_filesystem ) ) {
return false;
}
$path = self::get_cache_directory() . $filename;
if ( 'file' === $this->mode
&& is_a( $this->wp_filesystem, 'WP_Filesystem_Direct' )
&& $this->wp_filesystem->exists( $path ) ) {
return $this->wp_filesystem->get_contents( $path );
}
$filename = "sitemap_{$type}_$filename";
$sitemap = get_transient( $filename );
return maybe_unserialize( $sitemap );
}
/**
* Store the sitemap page from cache.
*
* @param string $type Sitemap type.
* @param int $page Page number to store.
* @param string $sitemap Sitemap body to store.
* @param bool $html Is HTML sitemap.
* @return boolean
*/
public function store_sitemap( $type, $page, $sitemap, $html = false ) {
$filename = $this->get_storage_key( $type, $page, $html );
if ( false === $filename || is_null( $this->wp_filesystem ) ) {
return false;
}
if ( 'file' === $this->mode ) {
$stored = $this->wp_filesystem->put_contents( self::get_cache_directory() . $filename, $sitemap, FS_CHMOD_FILE );
if ( true === $stored ) {
self::cached_files( $filename, $type );
return $stored;
}
}
$filename = "sitemap_{$type}_$filename";
return set_transient( $filename, maybe_serialize( $sitemap ), DAY_IN_SECONDS * 100 );
}
/**
* Get filename for sitemap.
*
* @param null|string $type The type to get the key for. Null or '1' for index cache.
* @param int $page The page of cache to get the key for.
* @param boolean $html Whether to add html extension.
* @return boolean|string The key where the cache is stored on. False if the key could not be generated.
*/
private function get_storage_key( $type = null, $page = 1, $html = false ) {
$type = is_null( $type ) ? '1' : $type;
$filename = self::STORAGE_KEY_PREFIX . md5( "{$type}_{$page}_" . home_url() ) . '.' . ( $html ? 'html' : 'xml' );
return $filename;
}
/**
* Get cache directory.
*
* @return string
*/
public static function get_cache_directory() {
$dir = wp_upload_dir();
$default = $dir['basedir'] . '/rank-math';
/**
* Filter XML sitemap cache directory.
*
* @param string $unsigned Default cache directory
*/
$filtered = apply_filters( 'rank_math/sitemap/cache_directory', $default );
if ( ! is_string( $filtered ) || '' === $filtered ) {
$filtered = $default;
}
return trailingslashit( $filtered );
}
/**
* Read/Write cached files.
*
* @param mixed $value Pass null to get option,
* Pass false to delete option,
* Pass value to update option.
* @param string $type Sitemap type.
* @return mixed
*/
public static function cached_files( $value = null, $type = '' ) {
if ( '' !== $type ) {
$options = Helper::option( 'sitemap_cache_files' );
$options[ $value ] = $type;
return Helper::option( 'sitemap_cache_files', $options );
}
return Helper::option( 'sitemap_cache_files', $value );
}
/**
* Invalidate sitemap cache.
*
* @param null|string $type The type to get the key for. Null for all caches.
*/
public static function invalidate_storage( $type = null ) {
/**
* Filter: 'rank_math/sitemap/invalidate_storage' - Allow developers to disable sitemap cache invalidation.
*/
if ( ! apply_filters( 'rank_math/sitemap/invalidate_storage', true, $type ) ) {
return;
}
$wp_filesystem = Helper::get_filesystem();
if ( is_null( $wp_filesystem ) ) {
return;
}
$directory = self::get_cache_directory();
if ( is_null( $type ) ) {
$wp_filesystem->delete( $directory, true );
wp_mkdir_p( $directory );
self::clear_transients();
self::cached_files( false );
Helper::clear_cache( 'sitemap' );
return;
}
$data = [];
$files = self::cached_files();
foreach ( $files as $file => $sitemap_type ) {
if ( $type !== $sitemap_type ) {
$data[ $file ] = $sitemap_type;
continue;
}
$wp_filesystem->delete( $directory . $file );
}
self::clear_transients( $type );
self::cached_files( $data );
Helper::clear_cache( 'sitemap/' . $type );
/**
* Action: 'rank_math/sitemap/invalidated_storage' - Runs after sitemap cache invalidation.
*/
do_action( 'rank_math/sitemap/invalidated_storage', $type );
}
/**
* Reset ALL transient caches.
*
* @param null|string $type The type to get the key for. Null for all caches.
*/
private static function clear_transients( $type = null ) {
if ( is_null( $type ) ) {
return Database::table( 'options' )
->whereLike( 'option_name', '_transient_sitemap_' )
->delete();
}
return Database::table( 'options' )
->whereLike( 'option_name', '_transient_sitemap_' . $type )
->delete();
}
}

View File

@@ -0,0 +1,137 @@
<?php
/**
* The link classifier finds out if a link points to internal content or external content.
*
* @since 0.9.0
* @package RankMath
* @subpackage RankMath\Sitemap
* @author Rank Math <support@rankmath.com>
*
* @copyright Copyright (C) 2008-2019, Yoast BV
* The following code is a derivative work of the code from the Yoast(https://github.com/Yoast/wordpress-seo/), which is licensed under GPL v3.
*/
namespace RankMath\Sitemap;
use RankMath\Helper;
defined( 'ABSPATH' ) || exit;
/**
* Classifier class.
*/
class Classifier {
const TYPE_EXTERNAL = 'external';
const TYPE_INTERNAL = 'internal';
/**
* Base host.
*
* @var string
*/
protected $base_host = '';
/**
* Base path.
*
* @var string
*/
protected $base_path = '';
/**
* Constructor setting the base url.
*
* @param string $base_url The base url to set.
*/
public function __construct( $base_url ) {
$this->base_host = Helper::get_url_part( $base_url, 'host' );
$base_path = Helper::get_url_part( $base_url, 'path' );
if ( $base_path ) {
$this->base_path = trailingslashit( $base_path );
}
}
/**
* Determines if the given link is an outbound or an internal link.
*
* @param string $link The link to classify.
* @return string Returns outbound or internal.
*/
public function classify( $link ) {
$url_parts = wp_parse_url( $link );
// Because parse_url may return false.
if ( ! is_array( $url_parts ) ) {
$url_parts = [];
}
// Short-circuit if filter returns non-null.
$filtered = apply_filters( 'rank_math/links/is_external', null, $url_parts );
if ( null !== $filtered ) {
return $filtered ? self::TYPE_EXTERNAL : self::TYPE_INTERNAL;
}
if ( $this->contains_protocol( $url_parts ) && $this->is_external_link( $url_parts ) ) {
return self::TYPE_EXTERNAL;
}
return self::TYPE_INTERNAL;
}
/**
* Returns true when the link starts with `https://` or `http://`.
*
* @param array $url_parts The URL parts to use.
* @return bool True if the URL starts with a protocol.
*/
protected function contains_protocol( array $url_parts ) {
return isset( $url_parts['scheme'] ) && null !== $url_parts['scheme'];
}
/**
* Checks if the link contains the `home_url`. Returns true if this isn't the case.
*
* @param array $url_parts The URL parts to use.
* @return bool True when the link doesn't contain the home url.
*/
protected function is_external_link( array $url_parts ) {
if ( $this->has_valid_scheme( $url_parts ) || $this->has_different_host( $url_parts ) ) {
return true;
}
// There is no base path.
if ( empty( $this->base_path ) ) {
return false;
}
// When there is a path.
if ( isset( $url_parts['path'] ) ) {
return ( strpos( $url_parts['path'], $this->base_path ) === false );
}
return true;
}
/**
* Checks if the link contains valid scheme.
*
* @param array $url_parts The URL parts to use.
* @return bool
*/
private function has_valid_scheme( array $url_parts ) {
return isset( $url_parts['scheme'] ) && ! in_array( $url_parts['scheme'], [ 'http', 'https' ], true );
}
/**
* Checks if the base host is equal to the host.
*
* @param array $url_parts The URL parts to use.
* @return bool
*/
private function has_different_host( array $url_parts ) {
return isset( $url_parts['host'] ) && $url_parts['host'] !== $this->base_host;
}
}

View File

@@ -0,0 +1,435 @@
<?php
/**
* The sitemap generator.
*
* @since 0.9.0
* @package RankMath
* @subpackage RankMath\Sitemap
* @author Rank Math <support@rankmath.com>
*
* @copyright Copyright (C) 2008-2019, Yoast BV
* The following code is a derivative work of the code from the Yoast(https://github.com/Yoast/wordpress-seo/), which is licensed under GPL v3.
*/
namespace RankMath\Sitemap;
use RankMath\Helper;
use RankMath\Traits\Hooker;
defined( 'ABSPATH' ) || exit;
/**
* Generator class.
*/
class Generator extends XML {
use Hooker;
/**
* 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';
/**
* If data encoding needs to be converted for output.
*
* @var boolean
*/
protected $needs_conversion = false;
/**
* Timezone.
*
* @var Timezone
*/
public $timezone;
/**
* Providers array.
*
* @var Provider
*/
public $providers = [];
/**
* The maximum number of entries per sitemap page.
*
* @var int
*/
private $max_entries;
/**
* Set up object properties.
*/
public function __construct() {
$this->stylesheet = preg_replace( '/(^http[s]?:)/', '', Router::get_base_url( 'main-sitemap.xsl' ) );
$this->stylesheet = '<?xml-stylesheet type="text/xsl" href="' . $this->stylesheet . '"?>';
$this->charset = get_bloginfo( 'charset' );
$this->output_charset = $this->charset;
$this->timezone = new Timezone();
if (
'UTF-8' !== $this->charset
&& 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;
$this->instantiate();
}
/**
* Instantiate required objects.
*/
private function instantiate() {
// Initialize sitemap providers classes.
$this->providers = [
new \RankMath\Sitemap\Providers\Post_Type(),
new \RankMath\Sitemap\Providers\Taxonomy(),
];
// Author Provider.
if ( true === Helper::is_author_archive_indexable() ) {
$this->providers[] = new \RankMath\Sitemap\Providers\Author();
}
$external_providers = $this->do_filter( 'sitemap/providers', [] );
foreach ( $external_providers as $provider ) {
if ( is_object( $provider ) ) {
$this->providers[] = $provider;
}
}
}
/**
* Produce final XML output with debug information.
*
* @param string $type Sitemap type.
* @param int $page Page number to retrieve.
* @return string
*/
public function get_output( $type, $page ) {
$output = '<?xml version="1.0" encoding="' . esc_attr( $this->get_output_charset() ) . '"?>';
if ( $this->stylesheet ) {
/**
* Filter the stylesheet URL for the XML sitemap.
*
* @param string $stylesheet Stylesheet URL.
*/
$output .= $this->do_filter( "sitemap/{$type}_stylesheet_url", $this->stylesheet ) . "\n";
}
$content = $this->build_sitemap( $type, $page );
if ( '' !== $content ) {
return $output . $content;
}
return '';
}
/**
* Attempts to build the requested sitemap.
*
* @param string $type Sitemap type.
* @param int $page Page number to retrieve.
* @return string
*/
public function build_sitemap( $type, $page ) {
$this->max_entries = absint( Helper::get_settings( 'sitemap.items_per_page', 100 ) );
/**
* Filter the type of sitemap to build.
*
* @param string $type Sitemap type, determined by the request.
*/
$type = $this->do_filter( 'sitemap/build_type', $type );
if ( '1' === $type ) {
return $this->build_root_map();
}
foreach ( $this->providers as $provider ) {
if ( ! $provider->handles_type( $type ) ) {
continue;
}
$links = $provider->get_sitemap_links( $type, $this->max_entries, $page );
if ( empty( $links ) && ( empty( $provider->should_show_empty ) || $page > 1 ) ) {
continue;
}
return $this->get_sitemap( $links, $type, $page );
}
return $this->do_filter( "sitemap/{$type}/content", '' );
}
/**
* Build the root sitemap (example.com/sitemap_index.xml) which lists sub-sitemaps for other content types.
*/
public function build_root_map() {
$links = [];
foreach ( $this->providers as $provider ) {
$links = array_merge( $links, $provider->get_index_links( $this->max_entries ) );
}
if ( empty( $links ) ) {
return '';
}
return $this->get_index( $links );
}
/**
* Produce XML output for 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 .= $this->do_filter( 'sitemap/index', '' );
$xml .= '</sitemapindex>';
return $xml;
}
/**
* Produce XML output for urlset.
*
* @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.
*
* @param string $urlset The output for the sitemap's `urlset`.
*/
$xml = $this->do_filter( "sitemap/{$type}_urlset", $urlset );
foreach ( $links as $url ) {
$method = $type . '_sitemap_url';
$xml .= has_filter( "rank_math/sitemap/{$method}" ) ? $this->do_filter( "sitemap/{$method}", $url, $this ) : $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 ( 1 === $current_page ) {
$xml .= $this->do_filter( "sitemap/{$type}_content", '' );
}
$xml .= '</urlset>';
return $xml;
}
/**
* 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 = $this->timezone->format_date( $url['lastmod'] );
}
$output = $this->newline( '<sitemap>', 1 );
$output .= $this->newline( '<loc>' . htmlspecialchars( $url['loc'] ) . '</loc>', 2 );
$output .= empty( $date ) ? '' : $this->newline( '<lastmod>' . htmlspecialchars( $date ) . '</lastmod>', 2 );
$output .= $this->newline( '</sitemap>', 1 );
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 = $this->timezone->format_date( $url['mod'] );
}
$output = $this->newline( '<url>', 1 );
$output .= $this->newline( '<loc>' . $this->encode_url_rfc3986( htmlspecialchars( $url['loc'] ) ) . '</loc>', 2 );
$output .= empty( $date ) ? '' : $this->newline( '<lastmod>' . htmlspecialchars( $date ) . '</lastmod>', 2 );
$output .= $this->sitemap_images( $url );
$output .= $this->newline( '</url>', 1 );
/**
* Filters the output for the sitemap url tag.
*
* @param string $output The output for the sitemap url tag.
* @param array $url The sitemap url array on which the output is based.
*/
return $this->do_filter( 'sitemap/url', $output, $url );
}
/**
* Sitemap Images.
*
* @param array $url Array of parts that make up this entry.
* @return string
*/
public function sitemap_images( $url ) {
if ( empty( $url['images'] ) ) {
return '';
}
$output = '';
foreach ( $url['images'] as $img ) {
if ( empty( $img['src'] ) ) {
continue;
}
$output .= $this->newline( '<image:image>', 2 );
$output .= $this->newline( '<image:loc>' . esc_html( $this->encode_url_rfc3986( $img['src'] ) ) . '</image:loc>', 3 );
$output .= $this->newline( '</image:image>', 2 );
}
return $output;
}
/**
* Convret encoding if needed.
*
* @param string $data Data to be added.
* @param string $tag Tag to create CDATA for.
* @param integer $indent Tab indent count.
*/
public function add_cdata( $data, $tag, $indent = 0 ) {
if ( $this->needs_conversion ) {
$data = mb_convert_encoding( $data, $this->output_charset, $this->charset );
}
$data = _wp_specialchars( html_entity_decode( $data, ENT_QUOTES, $this->output_charset ) );
return $this->newline( "<{$tag}><![CDATA[{$data}]]></{$tag}>", $indent );
}
/**
* Apply some best effort conversion to comply with RFC3986.
*
* @param string $url URL to encode.
* @return string
*/
public function encode_url_rfc3986( $url ) {
if ( filter_var( $url, FILTER_VALIDATE_URL ) ) {
return $url;
}
$url = $this->encode_url_path( $url );
$url = $this->encode_url_query( $url );
return $url;
}
/**
* Apply some best effort conversion to comply with RFC3986.
*
* @param string $url URL to encode.
* @return string
*/
private function encode_url_path( $url ) {
$path = wp_parse_url( $url, PHP_URL_PATH );
if ( empty( $path ) || '/' === $path ) {
return $url;
}
$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 );
$encoded_path = str_replace( '%7E', '~', $encoded_path ); // PHP <5.3.
return str_replace( $path, $encoded_path, $url );
}
/**
* Apply some best effort conversion to comply with RFC3986.
*
* @param string $url URL to encode.
* @return string
*/
private function encode_url_query( $url ) {
$query = wp_parse_url( $url, PHP_URL_QUERY );
if ( empty( $query ) ) {
return $url;
}
parse_str( $query, $parsed_query );
if ( defined( 'PHP_QUERY_RFC3986' ) ) { // PHP 5.4+.
$parsed_query = http_build_query( $parsed_query, '', '&amp;', PHP_QUERY_RFC3986 );
} else {
$parsed_query = http_build_query( $parsed_query, '', '&amp;' );
$parsed_query = str_replace( '+', '%20', $parsed_query );
$parsed_query = str_replace( '%7E', '~', $parsed_query );
}
return str_replace( $query, $parsed_query, $url );
}
/**
* Write a newline with indent count.
*
* @param string $content Content to write.
* @param integer $indent Count of indent.
* @return string
*/
public function newline( $content, $indent = 0 ) {
return str_repeat( "\t", $indent ) . $content . "\n";
}
}

View File

@@ -0,0 +1,564 @@
<?php
/**
* Parse images from a post.
*
* @since 0.9.0
* @package RankMath
* @subpackage RankMath\Sitemap
* @author Rank Math <support@rankmath.com>
*
* @copyright Copyright (C) 2008-2019, Yoast BV
* The following code is a derivative work of the code from the Yoast(https://github.com/Yoast/wordpress-seo/), which is licensed under GPL v3.
*/
namespace RankMath\Sitemap;
use WP_Query;
use DOMDocument;
use RankMath\Helper;
use RankMath\Helpers\Attachment;
use RankMath\Helpers\Str;
use RankMath\Helpers\Url;
use RankMath\Helpers\Arr;
use RankMath\Traits\Hooker;
defined( 'ABSPATH' ) || exit;
/**
* Image_Parser class.
*/
class Image_Parser {
use Hooker;
/**
* 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';
/**
* Hold post.
*
* @var object
*/
private $post = null;
/**
* Hold parsed images data.
*
* @var array
*/
private $images = [];
/**
* 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 ) {
if ( ! Helper::get_settings( 'sitemap.include_images' ) ) {
return false;
}
$this->post = $post;
if ( ! is_object( $this->post ) ) {
return $this->images;
}
$this->get_post_thumbnail();
$this->get_post_images();
$this->get_post_galleries();
$this->get_is_attachment();
$this->get_custom_field_images();
// Reset.
$images = $this->images;
$this->images = [];
$this->post = null;
/**
* 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.
*/
return $this->do_filter( 'sitemap/urlimages', $images, $post->ID );
}
/**
* Get term images.
*
* @param object $term Term to get images from description for.
*
* @return array
*/
public function get_term_images( $term ) {
if ( ! Helper::get_settings( 'sitemap.include_images' ) ) {
return false;
}
$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;
}
/**
* Get post thumbnail.
*/
private function get_post_thumbnail() {
$thumbnail_id = get_post_thumbnail_id( $this->post->ID );
if (
! Helper::get_settings( 'sitemap.include_featured_image' ) ||
! Attachment::attachment_in_sitemap( $thumbnail_id )
) {
return;
}
$this->get_image_item( $this->get_absolute_url( $this->image_url( $thumbnail_id ) ) );
}
/**
* Get images from post content.
*/
private function get_post_images() {
/**
* Filter: 'rank_math/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 = $this->do_filter( 'sitemap/content_before_parse_html_images', $this->post->post_content, $this->post->ID );
$content = do_blocks( $content );
foreach ( $this->parse_html_images( $content ) as $image ) {
$this->get_image_item( $image['src'] );
}
}
/**
* Get post galleries.
*/
private function get_post_galleries() {
foreach ( $this->parse_galleries( $this->post->post_content, $this->post->ID ) as $attachment ) {
$this->get_image_item( $this->get_absolute_url( $this->image_url( $attachment->ID ) ) );
}
}
/**
* Get image if post is attachment.
*/
private function get_is_attachment() {
if ( 'attachment' === $this->post->post_type && wp_attachment_is_image( $this->post ) ) {
$this->get_image_item( $this->get_absolute_url( $this->image_url( $this->post->ID ) ) );
}
}
/**
* Get images from custom fields.
*/
private function get_custom_field_images() {
$customs = Helper::get_settings( 'sitemap.pt_' . $this->post->post_type . '_image_customfields' );
if ( empty( $customs ) ) {
return;
}
$customs = Arr::from_string( $customs, "\n" );
foreach ( $customs as $key ) {
$src = get_post_meta( $this->post->ID, $key, true );
if ( Str::is_non_empty( $src ) && Helper::is_image_url( $src ) ) {
$this->get_image_item( $src );
}
}
}
/**
* Parse `<img />` tags in content.
*
* @param string $content Content string to parse.
*
* @return array
*/
private function parse_html_images( $content ) {
$images = [];
$document = $this->get_document( $content );
if ( false === $document ) {
return $images;
}
foreach ( $document->getElementsByTagName( 'img' ) as $img ) {
$src = $this->get_image_src( $img );
if ( false === $src ) {
continue;
}
$images[] = [ 'src' => $src ];
}
return $images;
}
/**
* Get DOM document.
*
* @param string $content Content to parse.
*
* @return bool|DOMDocument
*/
private function get_document( $content ) {
if ( ! class_exists( 'DOMDocument' ) || empty( $content ) ) {
return false;
}
// 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();
return $post_dom;
}
/**
* Get image source from node.
*
* @param DOMNode $node Node instance.
*
* @return bool|string
*/
private function get_image_src( $node ) {
$src = $node->getAttribute( 'src' );
if ( $node->hasAttribute( 'data-sitemapexclude' ) || empty( $src ) ) {
return false;
}
$class = $node->getAttribute( 'class' );
if ( // This detects WP-inserted images, which we need to upsize. R.
! empty( $class )
&& ! Str::contains( 'size-full', $class )
&& preg_match( '|wp-image-(?P<id>\d+)|', $class, $matches )
&& get_post_status( $matches['id'] )
) {
$src = $this->image_url( $matches['id'] );
}
$src = $this->get_absolute_url( $src );
$no_host = esc_url( $src ) !== $src;
if ( ! $this->do_filter( 'sitemap/include_external_image', false ) ) {
$no_host = ! Str::contains( $this->host, $src ) || esc_url( $src ) !== $src;
}
return $no_host ? false : $src;
}
/**
* 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.
*/
private 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'];
}
$attachments = array_merge( $attachments, $this->get_gallery_attachments( $id, $gallery ) );
}
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.
*/
private function get_content_galleries( $content ) {
if ( ! preg_match_all( '/' . get_shortcode_regex( [ 'gallery' ] ) . '/s', $content, $matches, PREG_SET_ORDER ) ) {
return [];
}
$galleries = [];
foreach ( $matches as $shortcode ) {
if ( 'gallery' !== $shortcode[2] ) {
continue;
}
$attributes = shortcode_parse_atts( $shortcode[3] );
$galleries[] = '' === $attributes ? [] : $attributes;
}
return $galleries;
}
/**
* Set image item array with filters applied.
*
* @param string $src Image URL.
*/
private function get_image_item( $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'] = $this->do_filter( 'sitemap/xml_img_src', $src, $this->post );
if ( Str::is_empty( $image['src'] ) ) {
return;
}
/**
* 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.
*/
$this->images[] = $this->do_filter( 'sitemap/xml_img', $image, $this->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 ) {
$src = $this->normalize_image_url( $post_id );
return false === $src ? '' : apply_filters( 'wp_get_attachment_url', $src, $post_id ); // phpcs:ignore
}
/**
* Get attached image URL.
*
* @param int $post_id ID of the post.
*
* @return bool|string
*/
private function normalize_image_url( $post_id ) {
$uploads = $this->get_upload_dir();
$attachment = get_post_meta( $post_id, '_wp_attached_file', true );
if ( false !== $uploads['error'] || empty( $attachment ) ) {
return false;
}
// Check that the upload base exists in the file location.
if ( 0 === strpos( $attachment, $uploads['basedir'] ) ) {
return str_replace( $uploads['basedir'], $uploads['baseurl'], $attachment );
}
if ( false !== strpos( $attachment, 'wp-content/uploads' ) ) {
return $uploads['baseurl'] . substr( $attachment, ( strpos( $attachment, 'wp-content/uploads' ) + 18 ) );
}
// It's a newly uploaded file, therefore $attachment is relative to the baseurl.
return $uploads['baseurl'] . '/' . $attachment;
}
/**
* Get WordPress upload directory.
*
* @return bool|array
*/
private function get_upload_dir() {
static $rank_math_wp_uploads;
if ( empty( $rank_math_wp_uploads ) ) {
$rank_math_wp_uploads = wp_upload_dir();
}
return $rank_math_wp_uploads;
}
/**
* Make absolute URL for domain or protocol-relative one.
*
* @param string $src URL to process.
*
* @return string
*/
private function get_absolute_url( $src ) {
if ( Str::is_empty( $src ) ) {
return $src;
}
if ( true === Url::is_relative( $src ) ) {
return '/' !== $src[0] ? $src :
$this->home_url . $src; // The URL is relative, we'll have to make it absolute.
}
// If not starting with protocol, we add the scheme as the standard requires a protocol.
return ! Str::starts_with( 'http', $src ) ? $this->scheme . ':' . $src : $src;
}
/**
* Returns the attachments for a gallery.
*
* @param int $id The post ID.
* @param array $gallery The gallery config.
*
* @return array The selected attachments.
*/
private 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'] );
}
return empty( $id ) ? [] : $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.
*/
private 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 $include Array with ids to include.
*
* @return array The found attachments.
*/
private function get_gallery_attachments_for_included( $include ) {
$ids_to_include = wp_parse_id_list( $include );
$attachments = $this->get_attachments(
[
'posts_per_page' => count( $ids_to_include ),
'post__in' => $ids_to_include,
]
);
$gallery_attachments = [];
foreach ( $attachments as $val ) {
$gallery_attachments[ $val->ID ] = $val;
}
return $gallery_attachments;
}
/**
* Returns the attachments.
*
* @param array $args Array with query args.
*
* @return array The found attachments.
*/
protected function get_attachments( $args ) {
$default_args = [
'post_status' => 'inherit',
'post_type' => 'attachment',
'post_mime_type' => 'image',
// Defaults taken from function get_posts.
'orderby' => 'date',
'order' => 'DESC',
'meta_key' => '',
'meta_value' => '',
'suppress_filters' => true,
'ignore_sticky_posts' => true,
'no_found_rows' => true,
];
$args = wp_parse_args( $args, $default_args );
$get_attachments = new WP_Query();
return $get_attachments->query( $args );
}
}

View File

@@ -0,0 +1,97 @@
<?php
/**
* Redirect core sitemaps.
*
* Disable WP core sitemap feature added in version 5.5 and
* use Rank Math's sitemaps instead.
*
* @since 1.0.47
* @package RankMath
* @subpackage RankMath\Sitemap
* @author Rank Math <support@rankmath.com>
*
* @copyright Copyright (C) 2008-2019, Yoast BV
* The following code is a derivative work of the code from the Yoast(https://github.com/Yoast/wordpress-seo/), which is licensed under GPL v3.
*/
namespace RankMath\Sitemap;
use RankMath\Helper;
use RankMath\Traits\Hooker;
use RankMath\Helpers\Str;
defined( 'ABSPATH' ) || exit;
/**
* Redirect Core Sitemaps class.
*/
class Redirect_Core_Sitemaps {
use Hooker;
/**
* The Constructor.
*/
public function __construct() {
/**
* Disable the WP core XML sitemaps.
*/
add_filter( 'wp_sitemaps_enabled', '__return_false' );
$this->action( 'template_redirect', 'redirect_core_sitemaps' );
}
/**
* Redirect Core sitemap links to Rank Math Sitemaps.
*/
public function redirect_core_sitemaps() {
// Early Bail.
if ( empty( $_SERVER['REQUEST_URI'] ) ) {
return;
}
$path = sanitize_text_field( wp_unslash( $_SERVER['REQUEST_URI'] ) );
// Early bail if it's not a WP Core sitemap link.
if ( ! Str::starts_with( '/wp-sitemap', $path ) ) {
return;
}
$redirect = $this->get_redirect_url( $path );
if ( ! $redirect ) {
return;
}
Helper::redirect( home_url( $redirect ), 301 );
exit;
}
/**
* Get Redirect URL by path.
*
* @param string $path The original path.
*
* @return string The URL to redirect.
*/
private function get_redirect_url( $path ) {
if ( '/wp-sitemap.xml' === $path ) {
return '/' . Sitemap::get_sitemap_index_slug() . '.xml';
}
if ( preg_match( '/^\/wp-sitemap-(posts|taxonomies)-(\w+)-(\d+)\.xml$/', $path, $matches ) ) {
$index = ( (int) $matches[3] - 1 );
$index = 0 !== $index ? (string) $index : '';
return '/' . $matches[2] . '-sitemap' . $index . '.xml';
}
if ( preg_match( '/^\/wp-sitemap-users-(\d+)\.xml$/', $path, $matches ) ) {
$index = ( (int) $matches[1] - 1 );
$index = 0 !== $index ? (string) $index : '';
return '/author-sitemap' . $index . '.xml';
}
return false;
}
}

View File

@@ -0,0 +1,191 @@
<?php
/**
* The sitemap URL rewrite setup and handling functionality.
*
* @since 0.9.0
* @package RankMath
* @subpackage RankMath\Sitemap
* @author Rank Math <support@rankmath.com>
*
* @copyright Copyright (C) 2008-2019, Yoast BV
* The following code is a derivative work of the code from the Yoast(https://github.com/Yoast/wordpress-seo/), which is licensed under GPL v3.
*/
namespace RankMath\Sitemap;
use RankMath\Helper;
use RankMath\Traits\Hooker;
use RankMath\Helpers\Str;
use RankMath\Helpers\Url;
defined( 'ABSPATH' ) || exit;
/**
* Router class.
*/
class Router {
use Hooker;
/**
* The Constructor.
*/
public function __construct() {
$this->action( 'init', 'init', 1 );
$this->action( 'parse_query', 'request_sitemap', 1 );
$this->action( 'template_redirect', 'template_redirect', 0 );
$this->action( 'after_setup_theme', 'reduce_query_load', 99 );
}
/**
* Sets up rewrite rules.
*/
public function init() {
global $wp;
$base = self::get_sitemap_base();
$wp->add_query_var( 'sitemap' );
$wp->add_query_var( 'sitemap_n' );
$wp->add_query_var( 'xsl' );
add_rewrite_rule( $base . Sitemap::get_sitemap_index_slug() . '\\.xml$', 'index.php?sitemap=1', 'top' );
add_rewrite_rule( $base . '([^/]+?)-sitemap([0-9]+)?\.xml$', 'index.php?sitemap=$matches[1]&sitemap_n=$matches[2]', 'top' );
add_rewrite_rule( $base . '([a-z]+)?-?sitemap\.xsl$', 'index.php?xsl=$matches[1]', 'top' );
}
/**
* Serves sitemap when needed using correct sitemap module.
*
* @param WP_Query $query The WP_Query instance (passed by reference).
*/
public function request_sitemap( $query ) {
if ( ! $query->is_main_query() ) {
return;
}
$xsl = self::get_sitemap_slug( get_query_var( 'xsl' ) );
if ( ! empty( $xsl ) ) {
$this->filter( 'user_has_cap', 'filter_user_has_cap' );
$stylesheet = new Stylesheet();
$stylesheet->output( $xsl );
return;
}
$type = get_query_var( 'sitemap' );
if ( empty( $type ) ) {
return;
}
new Sitemap_XML( $type );
}
/**
* 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 = sanitize_text_field( wp_unslash( $_SERVER['REQUEST_URI'] ) );
$extension = substr( $request, -4 );
if ( Str::contains( 'sitemap', $request ) && in_array( $extension, [ '.xml', '.xsl' ], true ) ) {
remove_all_actions( 'widgets_init' );
}
}
/**
* Redirects `sitemap.xml` to `sitemap_index.xml`.
*/
public function template_redirect() {
if ( ! $this->needs_sitemap_index_redirect() ) {
return;
}
Helper::redirect( home_url( '/' . Sitemap::get_sitemap_index_slug() . '.xml' ), 301 );
exit;
}
/**
* Checks whether the current request needs to be redirected to sitemap_index.xml.
*
* @return bool True if redirect is needed, false otherwise.
*/
public function needs_sitemap_index_redirect() {
global $wp_query;
return $wp_query->is_404 && home_url( '/sitemap.xml' ) === Url::get_current_url();
}
/**
* 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 ) {
$page = self::get_page_url( $page );
$base = self::get_sitemap_base();
return home_url( $base . $page );
}
/**
* Create base URL for the sitemap.
*
* @since 1.0.43
*
* @return string Sitemap base.
*/
public static function get_sitemap_base() {
global $wp_rewrite;
$base = $wp_rewrite->using_index_permalinks() ? $wp_rewrite->index . '/' : '';
/**
* 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.
*/
return apply_filters( 'rank_math/sitemap/base_url', $base );
}
/**
* Get sitemap slug.
*
* @param string $type Sitemap type.
* @return string
*/
public static function get_sitemap_slug( $type ) {
/**
* Filter the slug of the sitemap.
*
* @param string $slug Slug of the sitemap.
*/
return apply_filters( "rank_math/sitemap/{$type}/slug", $type );
}
/**
* Get page URL for the sitemap.
*
* @param string $page Page to append to the base URL.
*
* @return string
*/
public static function get_page_url( $page ) {
global $wp_rewrite;
if ( $wp_rewrite->using_permalinks() ) {
return $page;
}
if ( Sitemap::get_sitemap_index_slug() . '.xml' === $page ) {
return '?sitemap=1';
}
$page = \preg_replace( '/([^\/]+?)-sitemap([0-9]+)?\.xml$/', '?sitemap=$1&sitemap_n=$2', $page );
$page = \preg_replace( '/([a-z]+)?-?sitemap\.xsl$/', '?xsl=$1', $page );
$page = str_replace( 'locations.kml', '?sitemap=locations', $page );
return $page;
}
}

View File

@@ -0,0 +1,72 @@
<?php
/**
* The sitemap index runner class.
*
* @since 1.0.42
* @package RankMath
* @subpackage RankMath\Sitemap
* @author Rank Math <support@rankmath.com>
*/
namespace RankMath\Sitemap;
use RankMath\Runner;
use RankMath\Traits\Hooker;
use RankMath\Helpers\Str;
defined( 'ABSPATH' ) || exit;
/**
* Sitemap Index class.
*/
class Sitemap_Index implements Runner {
use Hooker;
/**
* The hooks.
*/
public function hooks() {
$this->filter( 'robots_txt', 'add_sitemap_directive', 0, 2 );
$this->filter( 'redirect_canonical', 'redirect_canonical' );
}
/**
* Adds the sitemap index to robots.txt.
*
* @param string $output robots.txt output.
* @param bool $public Whether the site is public or not.
*
* @return string robots.txt output.
*/
public function add_sitemap_directive( $output, $public ) {
if (
'0' === $public ||
Str::contains( 'Sitemap:', $output ) ||
Str::contains( 'sitemap:', $output )
) {
return $output;
}
$sitemap_url = esc_url( Router::get_base_url( Sitemap::get_sitemap_index_slug() . '.xml' ) );
return $output . "\nSitemap: {$sitemap_url}\n";
}
/**
* Stop trailing slashes on `sitemap.xml` URLs.
*
* @copyright Copyright (C) 2008-2019, Yoast BV
* The following code is a derivative work of the code from the Yoast(https://github.com/Yoast/wordpress-seo/), which is licensed under GPL v3.
*
* @param string $redirect The redirect URL currently determined.
*
* @return boolean|string $redirect
*/
public function redirect_canonical( $redirect ) {
if ( get_query_var( 'sitemap' ) || get_query_var( 'xsl' ) ) {
return false;
}
return $redirect;
}
}

View File

@@ -0,0 +1,211 @@
<?php
/**
* Render XML output for sitemaps.
*
* @since 0.9.0
* @package RankMath
* @subpackage RankMath\Sitemap
* @author Rank Math <support@rankmath.com>
*
* @copyright Copyright (C) 2008-2019, Yoast BV
* The following code is a derivative work of the code from the Yoast(https://github.com/Yoast/wordpress-seo/), which is licensed under GPL v3.
*/
namespace RankMath\Sitemap;
use RankMath\Helper;
use RankMath\Traits\Hooker;
defined( 'ABSPATH' ) || exit;
/**
* Sitemap XML class.
*/
class Sitemap_XML extends XML {
use Hooker;
/**
* Hold sitemap type.
*
* @var string
*/
public $type;
/**
* Sitemap cache.
*
* @var Cache
*/
public $cache;
/**
* Hold the current page.
*
* @var int
*/
private $current_page = 1;
/**
* Content of the sitemap to output.
*
* @var string
*/
protected $sitemap = '';
/**
* Whether or not the XML sitemap was served from cache or not.
*
* @var boolean
*/
private $transient = false;
/**
* Holds the stats for the sitemap generation.
*
* @var array
*/
private $stats = [];
/**
* The Constructor.
*
* @param string $type Sitemap type.
*/
public function __construct( $type ) {
remove_all_actions( 'widgets_init' );
$this->filter( 'user_has_cap', 'filter_user_has_cap' );
$this->type = $type;
$this->cache = new Cache();
$this->set_n( get_query_var( 'sitemap_n' ) );
$this->output();
}
/**
* Generate sitemap now.
*/
private function output() {
global $wp_query;
$this->init_stats();
if ( ! $this->has_sitemap_in_cache() ) {
$this->build_sitemap();
}
if ( empty( $this->sitemap ) ) {
$wp_query->set_404();
status_header( 404 );
return;
}
$this->send_headers();
echo $this->sitemap; // phpcs:ignore
$this->output_credits();
remove_all_actions( 'wp_footer' );
die;
}
/**
* Output XML credits.
*/
private function output_credits() {
if ( ! $this->do_filter( 'sitemap/remove_credit', false ) ) {
echo "\n<!-- XML Sitemap generated by Rank Math SEO Plugin (c) Rank Math - rankmath.com -->";
}
if ( ! WP_DEBUG_DISPLAY || ! WP_DEBUG ) {
return;
}
$time = timer_stop( 0, 3 );
$sql = get_num_queries() - $this->stats['query'];
$memory = size_format( memory_get_usage() - $this->stats['memory'], 2 );
$template = $this->transient ? 'Served from cache in %s second(s) (Memory usage: %s)' : 'This sitemap was originally generated in %s second(s). (Memory usage: %s) - %s queries';
$template = sprintf( $template, $time, $memory, $sql );
echo "\n<!-- {$template} -->"; // phpcs:ignore
if ( defined( 'SAVEQUERIES' ) && SAVEQUERIES ) {
$queries = print_r( $GLOBALS['wpdb']->queries, true ); // phpcs:ignore
echo "\n<!-- {$queries} -->"; // phpcs:ignore
}
}
/**
* Try to get the sitemap from cache.
*
* @return boolean If the sitemap has been retrieved from cache.
*/
private function has_sitemap_in_cache() {
$this->transient = false;
if ( true !== Sitemap::is_cache_enabled() ) {
return false;
}
$this->sitemap = $this->cache->get_sitemap( $this->type, $this->current_page );
if ( ! empty( $this->sitemap ) ) {
$this->transient = true;
return true;
}
// No cache was found, refresh it because cache is enabled.
$this->build_sitemap();
if ( ! empty( $this->sitemap ) ) {
return $this->cache->store_sitemap( $this->type, $this->current_page, $this->sitemap );
}
return false;
}
/**
* Attempts to build the requested sitemap.
*/
public function build_sitemap() {
$generator = new Generator();
$this->sitemap = $generator->get_output( $this->type, $this->current_page );
}
/**
* Set the sitemap current page to allow creating partial sitemaps with wp-cli
* in a one-off process.
*
* @param integer $current_page The part that should be generated.
*/
public function set_n( $current_page ) {
if ( ! empty( $current_page ) && is_scalar( $current_page ) ) {
$this->maybe_redirect( $current_page );
$this->current_page = max( intval( $current_page ), 1 );
}
}
/**
* Inits building some sitemap generation stats.
*/
private function init_stats() {
timer_start();
$this->stats['query'] = get_num_queries();
$this->stats['memory'] = memory_get_usage();
}
/**
* Redirect page if $current_page has leading zeros.
*
* @param mixed $current_page The page number.
* @return void
*/
private function maybe_redirect( $current_page ) {
// Redirect when there are only zeros.
if ( '' !== $current_page && intval( $current_page ) < 1 ) {
Helper::redirect( preg_replace( '/0+\.xml$/', '.xml', Helper::get_current_page_url() ) );
die();
}
// Redirect when there are leading zeros.
$zeros_stripped = ltrim( $current_page, '0' );
if ( (string) $zeros_stripped !== (string) $current_page ) {
Helper::redirect( preg_replace( '/' . preg_quote( $current_page ) . '\.xml$/', $zeros_stripped . '.xml', Helper::get_current_page_url() ) );
die();
}
}
}

View File

@@ -0,0 +1,327 @@
<?php
/**
* The Sitemap module.
*
* @since 0.9.0
* @package RankMath
* @subpackage RankMath\Sitemap
* @author Rank Math <support@rankmath.com>
*/
namespace RankMath\Sitemap;
use RankMath\Helper;
use RankMath\Helpers\Sitepress;
use RankMath\Traits\Hooker;
use RankMath\Sitemap\Html\Sitemap as Html_Sitemap;
defined( 'ABSPATH' ) || exit;
/**
* Sitemap class.
*
* @copyright Copyright (C) 2008-2019, Yoast BV
* The following code is a derivative work of the code from the Yoast(https://github.com/Yoast/wordpress-seo/), which is licensed under GPL v3.
*/
class Sitemap {
use Hooker;
/**
* Sitemap Index object.
*
* @var Sitemap_Index
*/
public $index;
/**
* The Constructor.
*/
public function __construct() {
if ( is_admin() ) {
new Admin();
}
if ( is_admin() || wp_doing_cron() ) {
new Cache_Watcher();
}
new Router();
$this->index = new Sitemap_Index();
$this->index->hooks();
new Redirect_Core_Sitemaps();
new Html_Sitemap();
add_action( 'rank_math/sitemap/hit_index', [ __CLASS__, 'hit_index' ] );
$this->filter( 'rank_math/admin/notice/new_post_type', 'new_post_type_notice', 10, 2 );
if ( class_exists( 'SitePress' ) ) {
$this->filter( 'rank_math/sitemap/build_type', 'rank_math_build_sitemap_filter' );
$this->filter( 'rank_math/sitemap/entry', 'exclude_hidden_language_posts', 10, 3 );
}
}
/**
* Exclude posts under hidden language.
*
* @since 1.0.5
*
* @param string $url Post URL.
* @param string $type URL type.
* @param object $post Object with some post information.
*
* @return string
*/
public function exclude_hidden_language_posts( $url, $type, $post ) {
if ( 'post' !== $type ) {
return $url;
}
global $sitepress;
// Check that at least ID is set in post object.
if ( ! isset( $post->ID ) ) {
return $url;
}
// Get list of hidden languages.
$hidden_languages = $sitepress->get_setting( 'hidden_languages', [] );
// If there are no hidden languages return original URL.
if ( empty( $hidden_languages ) ) {
return $url;
}
// Get language information for post.
$language_info = $sitepress->post_translations()->get_element_lang_code( $post->ID );
// If language code is one of the hidden languages return empty string to skip the post.
if ( in_array( $language_info, $hidden_languages, true ) ) {
return '';
}
return $url;
}
/**
* Prevent get_permalink from translating and remove filter added by WPML to get terms in current language.
*
* @since 1.0.5
*
* @param string $type Sitemap type.
*
* @return string
*/
public function rank_math_build_sitemap_filter( $type ) {
global $sitepress_settings;
// Before to build the sitemap and as we are on front-end just make sure the links won't be translated. The setting should not be updated in DB.
$sitepress_settings['auto_adjust_ids'] = 0;
/**
* Remove WPML filters while getting terms, to get all languages
*/
Sitepress::get()->remove_term_filters();
return $type;
}
/**
* Add new CPT notice.
*
* @param string $notice New CPT notice.
* @param int $count Count of new post types detected.
* @return string
*/
public function new_post_type_notice( $notice, $count ) {
/* Translators: placeholder is the post type name. */
$notice = __( 'Rank Math has detected a new post type: %1$s. You may want to check the settings of the <a href="%2$s">Titles &amp; Meta page</a> and <a href="%3$s">the Sitemap</a>.', 'rank-math' );
if ( $count > 1 ) {
/* Translators: placeholder is the post type names separated with commas. */
$notice = __( 'Rank Math has detected new post types: %1$s. You may want to check the settings of the <a href="%2$s">Titles &amp; Meta page</a> and <a href="%3$s">the Sitemap</a>.', 'rank-math' );
}
return $notice;
}
/**
* Hit sitemap index to pre-generate the cache.
*/
public static function hit_index() {
wp_remote_get( Router::get_base_url( self::get_sitemap_index_slug() . '.xml' ) );
}
/**
* Exclude object from sitemap.
*
* @param int $object_id Object id.
* @param string $object_type Object type. Accetps: post, term, user.
* @param boolean $include Add or Remove object.
*/
public static function exclude_object( $object_id, $object_type, $include ) {
$field_id = "exclude_{$object_type}s";
$ids = Helper::get_settings( 'sitemap.' . $field_id );
if ( empty( $ids ) ) {
$ids = $object_id;
} else {
$ids = array_filter( wp_parse_id_list( $ids ) );
// Add object.
if ( $include && ! in_array( $object_id, $ids, true ) ) {
$ids[] = $object_id;
}
// Remove object.
if ( ! $include && in_array( $object_id, $ids, true ) ) {
$ids = array_diff( $ids, [ $object_id ] );
}
$ids = implode( ',', $ids );
}
$opt = cmb2_options( 'rank-math-options-sitemap' );
$opt->update( $field_id, $ids, true );
}
/**
* Get the GMT modification date for the last modified post in the post type.
*
* @param string|array $post_types Post type or array of types.
* @param boolean $return_all Flag to return array of values.
* @return string|array|false
*
* @copyright Copyright (C) 2008-2019, Yoast BV
* The following code is a derivative work of the code from the Yoast(https://github.com/Yoast/wordpress-seo/), which is licensed under GPL v3.
*/
public static function get_last_modified_gmt( $post_types, $return_all = false ) {
global $wpdb;
if ( empty( $post_types ) ) {
return false;
}
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 = get_post_types( [ 'public' => true ] );
if ( ! empty( $post_type_names ) ) {
$sql = "
SELECT post_type, MAX( GREATEST( p.post_modified_gmt, p.post_date_gmt ) ) AS date
FROM $wpdb->posts as p
LEFT JOIN {$wpdb->postmeta} AS pm ON ( p.ID = pm.post_id AND pm.meta_key = 'rank_math_robots')
WHERE (
( pm.meta_key = 'rank_math_robots' AND pm.meta_value NOT LIKE '%noindex%' ) OR
pm.post_id IS NULL
)
AND p.post_status IN ( 'publish','inherit' )
AND p.post_type IN ('" . implode( "','", $post_type_names ) . "')
GROUP BY p.post_type
ORDER BY p.post_modified_gmt DESC";
foreach ( $wpdb->get_results( $sql ) as $obj ) { // phpcs:ignore
$post_type_dates[ $obj->post_type ] = $obj->date;
}
}
}
$dates = array_intersect_key( $post_type_dates, array_flip( $post_types ) );
if ( count( $dates ) > 0 ) {
return $return_all ? $dates : max( $dates );
}
return false;
}
/**
* Check if cache is enabled.
*
* @return boolean
*/
public static function is_cache_enabled() {
static $xml_sitemap_caching;
if ( isset( $xml_sitemap_caching ) ) {
return $xml_sitemap_caching;
}
/**
* Filter to enable/disable XML sitemap caching.
*
* @param boolean $true Enable or disable caching.
*/
$xml_sitemap_caching = apply_filters( 'rank_math/sitemap/enable_caching', true );
return $xml_sitemap_caching;
}
/**
* Check if `object` is indexable.
*
* @param int/object $object Post|Term Object.
* @param string $type Object Type.
*
* @return boolean
*/
public static function is_object_indexable( $object, $type = 'post' ) {
/**
* Filter: 'rank_math/sitemap/include_noindex' - Include noindex data in Sitemap.
*
* @param bool $value Whether to include noindex terms in Sitemap.
* @param string $type Object Type.
*
* @return boolean
*/
if ( apply_filters( 'rank_math/sitemap/include_noindex', false, $type ) ) {
return true;
}
$method = 'post' === $type ? 'is_post_indexable' : 'is_term_indexable';
return Helper::$method( $object );
}
/**
* Redirect duplicate sitemaps.
*
* @param int $count Total number of entries.
* @param int $max_entries Entries per sitemap.
*/
public static function maybe_redirect( $count, $max_entries ) {
$current_page = (int) get_query_var( 'sitemap_n' );
if ( ! $current_page && $count > $max_entries ) {
Helper::redirect( preg_replace( '/\.xml$/', '1.xml', Helper::get_current_page_url() ) );
die();
}
if ( $count < $max_entries && $current_page ) {
Helper::redirect( preg_replace( '/' . preg_quote( $current_page ) . '\.xml$/', '.xml', Helper::get_current_page_url() ) );
die();
}
}
/**
* Get the sitemap index slug.
*/
public static function get_sitemap_index_slug() {
/**
* Filter: 'rank_math/sitemap/index_slug' - Modify the sitemap index slug.
*
* @param string $slug Sitemap index slug.
*
* @return string
*/
return apply_filters( 'rank_math/sitemap/index/slug', 'sitemap_index' );
}
}

View File

@@ -0,0 +1,69 @@
<?php
/**
* The stylesheet class for the sitemaps.
*
* @since 0.9.0
* @package RankMath
* @subpackage RankMath\Sitemap
* @author Rank Math <support@rankmath.com>
*
* @copyright Copyright (C) 2008-2019, Yoast BV
* The following code is a derivative work of the code from the Yoast(https://github.com/Yoast/wordpress-seo/), which is licensed under GPL v3.
*/
namespace RankMath\Sitemap;
use RankMath\Traits\Hooker;
defined( 'ABSPATH' ) || exit;
/**
* Stylesheet class.
*/
class Stylesheet extends XML {
use Hooker;
/**
* Hold sitemap type.
*
* @var string
*/
public $type;
/**
* Output the XSL for the XML sitemap.
*
* @param string $type Sitemap type.
*/
public function output( $type ) {
$this->type = $type;
$expires = gmdate( 'D, d M Y H:i:s', ( time() + YEAR_IN_SECONDS ) );
$this->send_headers(
[
'Cache-Control' => 'maxage=' . YEAR_IN_SECONDS,
'Expires' => $expires . ' GMT',
'Etag' => md5( $expires . $this->type ),
],
true
);
/* translators: 1. separator, 2. blogname */
$title = sprintf( __( 'XML Sitemap %1$s %2$s', 'rank-math' ), '-', get_bloginfo( 'name', 'display' ) );
/* translators: 1. separator, 2. blogname */
$kml_title = sprintf( __( 'Locations Sitemap %1$s %2$s', 'rank-math' ), '-', get_bloginfo( 'name', 'display' ) );
if ( 'main' !== $type ) {
/**
* Fires for the output of XSL for XML sitemaps, other than type "main".
*/
$this->do_action( "sitemap/xsl_{$type}", $title, $kml_title );
die;
}
require_once 'sitemap-xsl.php';
die;
}
}

View File

@@ -0,0 +1,62 @@
<?php
/**
* Sitemap date format class.
*
* @since 0.9.0
* @package RankMath
* @subpackage RankMath\Sitemap
* @author Rank Math <support@rankmath.com>
*
* @copyright Copyright (C) 2008-2019, Yoast BV
* The following code is a derivative work of the code from the Yoast(https://github.com/Yoast/wordpress-seo/), which is licensed under GPL v3.
*/
namespace RankMath\Sitemap;
use DateTime;
use Exception;
use DateTimeZone;
defined( 'ABSPATH' ) || exit;
/**
* Timezone.
*/
class Timezone {
/**
* Format arbitrary UTC datetime string to desired form in site's time zone.
*
* @param string $datetime_string The input datetime string in UTC time zone.
* @param string $format Date format to use.
*
* @return string
*/
public function format_date( $datetime_string, $format = DATE_W3C ) {
if ( empty( $datetime_string ) || false === $this->is_valid_datetime( $datetime_string ) ) {
return '';
}
$date_time = new DateTime( $datetime_string, new DateTimeZone( 'UTC' ) );
return false === $date_time ? '' : $date_time->format( $format );
}
/**
* Check if a string is a valid datetime.
*
* @param string $datetime String input to check as valid input for DateTime class.
* @return boolean
*/
private function is_valid_datetime( $datetime ) {
if ( substr( $datetime, 0, 1 ) === '-' ) {
return false;
}
try {
return new DateTime( $datetime ) !== false;
} catch ( Exception $exc ) {
return false;
}
}
}

View File

@@ -0,0 +1,188 @@
<?php
/**
* The HTML sitemap generator for authors.
*
* @since 1.0.104
* @package RankMath
* @subpackage RankMath\Sitemap
* @author Rank Math <support@rankmath.com>
*/
namespace RankMath\Sitemap\Html;
use RankMath\Helper;
use RankMath\Traits\Hooker;
use RankMath\Admin\Database\Database;
use RankMath\Sitemap\Providers\Author;
defined( 'ABSPATH' ) || exit;
/**
* Terms class.
*/
class Authors extends Author {
use Hooker;
/**
* Get all authors.
*
* @return array
*/
private function get_authors() {
$sort_map = [
'published' => [
'field' => 'user_registered',
'order' => 'DESC',
],
'modified' => [
'field' => 'user_registered',
'order' => 'DESC',
],
'alphabetical' => [
'field' => 'display_name',
'order' => 'ASC',
],
'post_id' => [
'field' => 'ID',
'order' => 'DESC',
],
];
$sort_setting = Helper::get_settings( 'sitemap.html_sitemap_sort' );
$sort = ( isset( $sort_map[ $sort_setting ] ) ) ? $sort_map[ $sort_setting ] : $sort_map['published'];
/**
* Filter: 'rank_math/sitemap/html_sitemap/sort_items' - Allow changing the sort order of the HTML sitemap.
*
* @var array $sort {
* @type string $field The field to sort by.
* @type string $order The sort order.
* }
* @var string $order The item type.
* @var string $empty Empty string (unused).
*/
$sort = $this->do_filter( 'sitemap/html_sitemap/sort_items', $sort, 'authors', '' );
$defaults = [
'orderby' => $sort['field'],
'order' => $sort['order'],
];
$args = $this->do_filter( 'sitemap/author/query', wp_parse_args( [ 'posts_per_page' => -1 ], $defaults ) );
$users = $this->get_users( $args );
if ( empty( $users ) ) {
return [];
}
return $users;
}
/**
* Check if user is not in the excluded roles and doesn't have noindex set.
*
* @param object $user Partial user data from database.
*
* @return bool
*/
private function should_include_user( $user ) {
$excluded_roles = (array) Helper::get_settings( 'sitemap.exclude_roles' );
$roles = (array) get_userdata( $user->ID )->roles;
$intersect = array_intersect( $roles, $excluded_roles );
if ( ! empty( $intersect ) ) {
return false;
}
$robots = get_user_meta( $user->ID, 'rank_math_robots', true );
if ( is_array( $robots ) && in_array( 'noindex', $robots, true ) ) {
return false;
}
return true;
}
/**
* Generate the HTML sitemap for authors.
*
* @return string
*/
public function generate_sitemap() {
$users = $this->get_authors();
if ( empty( $users ) ) {
return '';
}
$output[] = '<div class="rank-math-html-sitemap__section rank-math-html-sitemap__section--authors">';
$output[] = '<h2 class="rank-math-html-sitemap__title">' . esc_html__( 'Authors', 'rank-math' ) . '</h2>';
$output[] = '<ul class="rank-math-html-sitemap__list">';
$output[] = $this->generate_authors_list( $users );
$output[] = '</ul>';
$output[] = '</div>';
$output = implode( '', $output );
return $output;
}
/**
* Generate HTML list of authors.
*
* @param array $authors List of authors.
*
* @return string
*/
private function generate_authors_list( $authors ) {
if ( empty( $authors ) ) {
return '';
}
$output = [];
foreach ( $authors as $author ) {
$output[] = '<li class="rank-math-html-sitemap__item">'
. '<a href="' . esc_url( $this->get_author_link( $author ) ) . '" class="rank-math-html-sitemap__link">'
. esc_html( $this->get_author_title( $author ) )
. '</a>'
. '</li>';
}
return implode( '', $output );
}
/**
* Get the author link.
*
* @param object $author Author data from database (not a WP_User object).
*
* @return string
*/
private function get_author_link( $author ) {
return get_author_posts_url( $author->ID, $author->user_nicename );
}
/**
* Get the author title.
*
* @param object $author The author data.
*
* @return string
*/
private function get_author_title( $author ) {
if ( Helper::get_settings( 'sitemap.html_sitemap_seo_titles' ) !== 'seo_titles' ) {
return $author->display_name;
}
// Custom SEO title.
$meta = get_user_meta( $author->ID, 'rank_math_title', true );
if ( ! empty( $meta ) ) {
return Helper::replace_vars( $meta, $author );
}
// Default SEO title from the global settings.
$template = Helper::get_settings( 'titles.pt_author_title' );
if ( ! empty( $template ) ) {
return Helper::replace_vars( $template, $author );
}
// Fallback to author name.
return $author->display_name;
}
}

View File

@@ -0,0 +1,313 @@
<?php
/**
* The HTML sitemap generator for posts.
*
* @since 1.0.104
* @package RankMath
* @subpackage RankMath\Sitemap
* @author Rank Math <support@rankmath.com>
*/
namespace RankMath\Sitemap\Html;
use RankMath\Helper;
use RankMath\Traits\Hooker;
use RankMath\Admin\Database\Database;
use RankMath\Sitemap\Sitemap as SitemapBase;
defined( 'ABSPATH' ) || exit;
/**
* Posts class.
*/
class Posts {
use Hooker;
/**
* An array of posts that have a parent.
*
* @var array
*/
private $children = [];
/**
* Get all posts from a given post type.
*
* @param string $post_type Post type.
* @param array $post_parents An array of post parent ids.
*
* @return array
*/
private function get_posts( $post_type, $post_parents = [] ) {
global $wpdb;
$sort_map = [
'published' => [
'field' => 'post_date',
'order' => 'DESC',
],
'modified' => [
'field' => 'post_modified',
'order' => 'DESC',
],
'alphabetical' => [
'field' => 'post_title',
'order' => 'ASC',
],
'post_id' => [
'field' => 'ID',
'order' => 'DESC',
],
];
$sort_setting = Helper::get_settings( 'sitemap.html_sitemap_sort' );
$sort = ( isset( $sort_map[ $sort_setting ] ) ) ? $sort_map[ $sort_setting ] : $sort_map['published'];
/**
* Filter: 'rank_math/sitemap/html_sitemap/sort_items' - Allow changing the sort order of the HTML sitemap.
*
* @var array $sort {
* @type string $field The field to sort by.
* @type string $order The sort order.
* }
* @var string $post_type The post type name.
* @var string $order The item type.
*/
$sort = $this->do_filter( 'sitemap/html_sitemap/sort_items', $sort, 'posts', $post_type );
$statuses = [ 'publish' ];
/**
* Filter: 'rank_math/sitemap/html_sitemap_post_statuses' - Allow changing the post statuses that should be included in the sitemap.
*
* @var array $statuses Post statuses.
* @var string $post_type Post type name.
*/
$statuses = $this->do_filter( 'sitemap/html_sitemap_post_statuses', $statuses, $post_type );
$get_child = ! empty( $post_parents ) ? " WHERE post_parent !='' " : '';
$sql = "
SELECT l.ID, post_title, post_name, post_parent, post_date, post_type, l.post_modified
FROM (
SELECT DISTINCT p.ID, p.post_modified FROM {$wpdb->posts} as p
LEFT JOIN {$wpdb->postmeta} AS pm ON ( p.ID = pm.post_id AND pm.meta_key = 'rank_math_robots' )
WHERE (
( pm.meta_key = 'rank_math_robots' AND pm.meta_value NOT LIKE '%noindex%' ) OR
pm.post_id IS NULL
)
AND p.post_type IN ( '" . $post_type . "' ) AND p.post_status IN ( '" . join( "', '", esc_sql( $statuses ) ) . "' )
ORDER BY p.post_modified DESC
)
o JOIN {$wpdb->posts} l ON l.ID = o.ID " . $get_child . " ORDER BY " . $sort['field'] . " " . $sort['order']; // phpcs:ignore
return $wpdb->get_results( $wpdb->prepare( $sql ) ); // phpcs:ignore
}
/**
* Generate the HTML sitemap for a given post type.
*
* @param string $post_type Post type name.
* @param bool $show_dates Whether to show dates.
*
* @return string
*/
public function generate_sitemap( $post_type, $show_dates ) {
$posts = $this->get_posts( $post_type );
if ( empty( $posts ) ) {
return '';
}
$output[] = '<div class="rank-math-html-sitemap__section rank-math-html-sitemap__section--post-type rank-math-html-sitemap__section--' . $post_type . '">';
$output[] = '<h2 class="rank-math-html-sitemap__title">' . esc_html( get_post_type_object( $post_type )->labels->name ) . '</h2>';
$output[] = '<ul class="rank-math-html-sitemap__list">';
$output[] = $this->generate_posts_list( $posts, $show_dates, $post_type );
$output[] = '</ul>';
$output[] = '</div>';
$output = implode( '', $output );
return $output;
}
/**
* Generate the post list HTML.
*
* @param array $posts Array of posts.
* @param bool $show_dates Whether to show dates.
* @param string $post_type Post type name.
*
* @return string
*/
private function generate_posts_list( $posts, $show_dates, $post_type ) {
if ( empty( $posts ) ) {
return '';
}
if ( is_post_type_hierarchical( $post_type ) ) {
$post_ids = [];
$post_list = [];
array_map(
function ( $post ) use ( &$post_ids, &$post_list ) {
$post_ids[] = $post->ID;
$post_list[ $post->ID ] = $post;
},
$posts
);
$children = $this->get_posts( $post_type, $post_ids );
foreach ( $children as $child ) {
// Confirm if child has a parent available, the parent might not be index-able and re-add the child to $posts!
$parent = array_filter(
$post_list,
function ( $post ) use ( $child ) {
return $child->post_parent === $post->ID;
}
);
if ( empty( $parent ) ) {
$child->child_has_no_parent = true;
$post_list[ $child->ID ] = $child;
continue;
}
$this->children[ $post_type ][ $child->post_parent ][ $child->ID ] = $child;
}
$post_list = $this->remove_with_parent( $post_list );
return $this->generate_posts_list_hierarchical( $post_list, $show_dates, $post_type );
}
return $this->generate_posts_list_flat( $posts, $show_dates );
}
/**
* Get the post list HTML for non-hierarchical post types.
*
* @param array $posts The posts to output.
* @param bool $show_dates Whether to show the post dates.
*
* @return string
*/
private function generate_posts_list_flat( $posts, $show_dates ) {
$output = [];
foreach ( $posts as $post ) {
if ( ! SitemapBase::is_object_indexable( absint( $post->ID ) ) ) {
continue;
}
$url = $this->do_filter( 'sitemap/entry', esc_url( $this->get_post_link( $post ) ), 'post', $post );
if ( empty( $url ) ) {
continue;
}
$output[] = '<li class="rank-math-html-sitemap__item">'
. '<a href="' . esc_url( $this->get_post_link( $post ) ) . '" class="rank-math-html-sitemap__link">'
. esc_html( $this->get_post_title( $post ) )
. '</a>'
. ( $show_dates ? ' <span class="rank-math-html-sitemap__date">(' . esc_html( mysql2date( get_option( 'date_format' ), $post->post_date ) ) . ')</span>' : '' )
. '</li>';
}
return implode( '', $output );
}
/**
* Get the post list HTML for hierarchical post types. This will output the
* posts in a nested list.
*
* @param array $posts The posts to output.
* @param bool $show_dates Whether to show the post dates.
* @param string $post_type Post type name.
* @param bool $child Whether the passed posts are children.
*
* @return string
*/
private function generate_posts_list_hierarchical( $posts, $show_dates, $post_type, $child = false ) {
$output = [];
$exclude = wp_parse_id_list( Helper::get_settings( 'sitemap.exclude_posts' ) );
foreach ( $posts as $post ) {
$check_parent_index = empty( $post->post_parent ) ? 0 : SitemapBase::is_object_indexable( $post->post_parent );
$is_indexable = SitemapBase::is_object_indexable( absint( $post->ID ) );
if ( ( ! $check_parent_index || $child ) && $is_indexable ) {
$output[] = '<li class="rank-math-html-sitemap__item">'
. '<a href="' . esc_url( get_permalink( $post->ID ) ) . '" class="rank-math-html-sitemap__link">'
. esc_html( $this->get_post_title( $post ) )
. '</a>'
. ( $show_dates ? ' <span class="rank-math-html-sitemap__date">(' . esc_html( mysql2date( get_option( 'date_format' ), $post->post_date ) ) . ')</span>' : '' );
}
if ( ! empty( $this->children[ $post_type ][ $post->ID ] ) ) {
if ( $is_indexable ) {
$output[] = '<ul class="rank-math-html-sitemap__list">';
}
$output[] = $this->generate_posts_list_hierarchical( $this->children[ $post_type ][ $post->ID ], $show_dates, $post_type, true ); // phpcs:ignore
if ( $is_indexable ) {
$output[] = '</ul>';
}
}
if ( $is_indexable ) {
$output[] = '</li>';
}
}
return implode( '', $output );
}
/**
* Get the post permalink.
*
* @param object $post The post object.
*
* @return string
*/
private function get_post_link( $post ) {
return get_permalink( $post->ID );
}
/**
* Get the post title.
*
* @param object $post The post data.
*
* @return string
*/
private function get_post_title( $post ) {
if ( Helper::get_settings( 'sitemap.html_sitemap_seo_titles' ) !== 'seo_titles' ) {
return $post->post_title;
}
// Custom SEO title.
$meta = get_post_meta( $post->ID, 'rank_math_title', true );
if ( ! empty( $meta ) ) {
return Helper::replace_vars( $meta, get_post( $post->ID ) );
}
// Default SEO title from the global settings.
$template = Helper::get_settings( "titles.pt_{$post->post_type}_title" );
if ( ! empty( $template ) ) {
return Helper::replace_vars( $template, get_post( $post->ID ) );
}
// Fallback to post title.
return $post->post_title;
}
/**
* Removes posts with a parent to avoid them being rendered twice.
*
* @param array $posts Array of post objects.
*
* @return array
*/
private function remove_with_parent( $posts ) {
return array_filter(
$posts,
function ( $post ) {
return ! $post->post_parent || isset( $post->child_has_no_parent );
}
);
}
}

View File

@@ -0,0 +1,273 @@
<?php
/**
* The Sitemap module - HTML Sitemap feature.
*
* @since 1.0.104
* @package RankMath
* @subpackage RankMath\Sitemap
* @author Rank Math <support@rankmath.com>
*/
namespace RankMath\Sitemap\Html;
use RankMath\Helper;
use RankMath\Sitemap\Providers\Taxonomy;
use RankMath\Traits\Hooker;
use RankMath\Sitemap\Cache;
defined( 'ABSPATH' ) || exit;
/**
* Sitemap class.
*/
class Sitemap extends Taxonomy {
use Hooker;
/**
* Generators.
*
* @var array
*/
private $generators;
/**
* Cache.
*
* @var object
*/
private $cache;
/**
* Constructor.
*/
public function __construct() {
if ( ! Helper::get_settings( 'sitemap.html_sitemap' ) ) {
return;
}
$this->cache = new Cache();
$this->generators = [
'posts' => new Posts(),
'terms' => new Terms(),
'authors' => new Authors(),
];
$display_mode = Helper::get_settings( 'sitemap.html_sitemap_display' );
if ( 'page' === $display_mode ) {
$this->action( 'the_content', 'show_on_page' );
} elseif ( 'shortcode' === $display_mode ) {
add_shortcode( 'rank_math_html_sitemap', [ $this, 'shortcode' ] );
// Compatibility code for the [aioseo_html_sitemap] shortcode.
add_shortcode( 'aioseo_html_sitemap', [ $this, 'shortcode' ] );
}
}
/**
* Get the HTML sitemap cache.
*
* @param string $name Name of the cache.
*
* @return string
*/
private function get_cache( $name ) {
if ( true !== \RankMath\Sitemap\Sitemap::is_cache_enabled() ) {
return false;
}
return $this->cache->get_sitemap( $name, 1, true );
}
/**
* Set the HTML sitemap cache.
*
* @param string $name Name of the cache.
* @param string $content Content of the cache.
*/
private function set_cache( $name, $content ) {
$this->cache->store_sitemap( $name, 1, $content, true );
}
/**
* Get generator.
*
* @param string $generator Generator name.
*
* @return object
*/
private function get_generator( $generator ) {
if ( ! isset( $this->generators[ $generator ] ) ) {
return false;
}
return $this->generators[ $generator ];
}
/**
* Get the HTML sitemap output.
*
* @return string
*/
public function get_output() {
$post_types = self::get_post_types();
$taxonomies = self::get_taxonomies();
/**
* Filter the setting of excluding empty terms from the XML sitemap.
*
* @param boolean $exclude Defaults to true.
* @param array $taxonomies Array of names for the taxonomies being processed.
*/
$show_dates = Helper::get_settings( 'sitemap.html_sitemap_show_dates' );
$output = [];
$output[] = '<div class="rank-math-html-sitemap">';
foreach ( $post_types as $post_type ) {
$cached = $this->get_cache( $post_type );
if ( ! empty( $cached ) ) {
$output[] = $cached;
continue;
}
$sitemap = $this->get_generator( 'posts' )->generate_sitemap( $post_type, $show_dates );
$this->set_cache( $post_type, $sitemap );
$output[] = $sitemap;
}
if ( ! empty( $taxonomies ) ) {
foreach ( $taxonomies as $taxonomy => $object ) {
$cached = $this->get_cache( $taxonomy );
if ( ! empty( $cached ) ) {
$output[] = $cached;
continue;
}
$hide_empty = ! Helper::get_settings( 'sitemap.tax_' . $taxonomy . '_include_empty' );
$sitemap = $this->get_generator( 'terms' )->generate_sitemap(
$taxonomy,
$show_dates,
[
'hide_empty' => $hide_empty,
'exclude' => wp_parse_id_list( Helper::get_settings( 'sitemap.exclude_terms' ) ),
]
);
$this->set_cache( $taxonomy, $sitemap );
$output[] = $sitemap;
}
}
if ( $this->should_show_author_sitemap() ) {
$cached = $this->get_cache( 'author' );
if ( ! empty( $cached ) ) {
$output[] = $cached;
} else {
$sitemap = $this->get_generator( 'authors' )->generate_sitemap();
$this->set_cache( 'author', $sitemap );
$output[] = $sitemap;
}
}
$output[] = '</div>';
return implode( '', $output );
}
/**
* Get post types to be included in the HTML sitemap.
*
* @return array
*/
public static function get_post_types() {
$post_types = [];
foreach ( Helper::get_accessible_post_types() as $post_type ) {
if ( ! Helper::get_settings( "sitemap.pt_{$post_type}_html_sitemap" ) ) {
continue;
}
$post_types[] = $post_type;
}
/**
* Filter: 'rank_math/sitemap/html_sitemap_post_types' - Allow changing the post types to be included in the HTML sitemap.
*
* @var array $post_types The post types to be included in the HTML sitemap.
*/
return apply_filters( 'rank_math/sitemap/html_sitemap_post_types', $post_types );
}
/**
* Check if author sitemap should be shown or not.
*/
private function should_show_author_sitemap() {
$show = Helper::get_settings( 'sitemap.authors_html_sitemap' );
if ( ! $show ) {
return false;
}
$disable_author_archives = Helper::get_settings( 'titles.disable_author_archives' );
if ( $disable_author_archives ) {
return false;
}
$robots = Helper::get_settings( 'titles.author_robots' );
if ( is_array( $robots ) && in_array( 'noindex', $robots, true ) ) {
return false;
}
return true;
}
/**
* Get taxonomies to be included in the HTML sitemap.
*
* @return array
*/
public static function get_taxonomies() {
$taxonomies = [];
foreach ( Helper::get_accessible_taxonomies() as $taxonomy ) {
if ( ! Helper::get_settings( "sitemap.tax_{$taxonomy->name}_html_sitemap" ) ) {
continue;
}
$taxonomies[ $taxonomy->name ] = $taxonomy;
}
/**
* Filter: 'rank_math/sitemap/html_sitemap_taxonomies' - Allow changing the taxonomies to be included in the HTML sitemap.
*
* @var array $taxonomies The taxonomies to be included in the HTML sitemap.
*/
return apply_filters( 'rank_math/sitemap/html_sitemap_taxonomies', $taxonomies );
}
/**
* Show sitemap on a page (after content).
*
* @param mixed $content The page content.
*/
public function show_on_page( $content ) {
if ( ! is_page() ) {
return $content;
}
if ( ! is_main_query() || ! in_the_loop() ) {
return $content;
}
$post_id = get_the_ID();
if ( (int) Helper::get_settings( 'sitemap.html_sitemap_page' ) !== $post_id ) {
return $content;
}
return $content . $this->get_output();
}
/**
* Shortcode callback.
*/
public function shortcode() {
return $this->get_output();
}
}

View File

@@ -0,0 +1,270 @@
<?php
/**
* The HTML sitemap generator for terms.
*
* @since 1.0.104
* @package RankMath
* @subpackage RankMath\Sitemap
* @author Rank Math <support@rankmath.com>
*/
namespace RankMath\Sitemap\Html;
use RankMath\Helper;
use RankMath\Traits\Hooker;
use RankMath\Admin\Database\Database;
use RankMath\Sitemap\Sitemap as SitemapBase;
defined( 'ABSPATH' ) || exit;
/**
* Terms class.
*/
class Terms {
use Hooker;
/**
* Get all terms from a given taxonomy.
*
* @param string $taxonomy Taxonomy name.
* @param int $parent Parent term ID.
*
* @return array
*/
private function get_terms( $taxonomy, $parent = 0 ) {
$sort_map = [
'published' => [
'field' => 'term_id',
'order' => 'DESC',
],
'modified' => [
'field' => 'term_id',
'order' => 'DESC',
],
'alphabetical' => [
'field' => 'name',
'order' => 'ASC',
],
'post_id' => [
'field' => 'term_id',
'order' => 'DESC',
],
];
$sort_setting = Helper::get_settings( 'sitemap.html_sitemap_sort' );
$sort = ( isset( $sort_map[ $sort_setting ] ) ) ? $sort_map[ $sort_setting ] : $sort_map['published'];
/**
* Filter: 'rank_math/sitemap/html_sitemap/sort_items' - Allow changing the sort order of the HTML sitemap.
*
* @var array $sort {
* @type string $field The field to sort by.
* @type string $order The sort order.
* }
* @var string $taxonomy The taxonomy name.
* @var string $order The item type.
*/
$sort = $this->do_filter( 'sitemap/html_sitemap/sort_items', $sort, 'terms', $taxonomy );
$exclude = wp_parse_id_list( Helper::get_settings( 'sitemap.exclude_terms' ) );
$terms_table = Database::table( 'terms' );
$tt_table = Database::table( 'term_taxonomy' );
$query = $terms_table->where( 'taxonomy', $taxonomy )
->select( [ $terms_table->table . '.term_id', 'name', 'slug', 'taxonomy' ] )
->leftJoin( $tt_table->table, $terms_table->table . '.term_id', $tt_table->table . '.term_id' )
->where( 'parent', $parent );
if ( ! empty( $exclude ) ) {
$query->whereNotIn( $terms_table->table . '.term_id', $exclude );
}
$terms = $query->orderBy( $sort['field'], $sort['order'] )->get();
return $this->get_indexable_terms( $terms, $taxonomy );
}
/**
* Generate the HTML sitemap for a given taxonomy.
*
* @param string $taxonomy Taxonomy name.
* @param bool $show_dates Whether to show dates.
* @param array $args Array with term query arguments.
*
* @return string
*/
public function generate_sitemap( $taxonomy, $show_dates, $args = [] ) {
$terms = get_terms( $taxonomy, $args );
$terms = $this->get_indexable_terms( $terms, $taxonomy );
if ( empty( $terms ) ) {
return '';
}
$output[] = '<div class="rank-math-html-sitemap__section rank-math-html-sitemap__section--taxonomy rank-math-html-sitemap__section--' . $taxonomy . '">';
$output[] = '<h2 class="rank-math-html-sitemap__title">' . esc_html( get_taxonomy( $taxonomy )->labels->name ) . '</h2>';
$output[] = '<ul class="rank-math-html-sitemap__list">';
$output[] = $this->generate_terms_list( $terms, $taxonomy );
$output[] = '</ul>';
$output[] = '</div>';
$output = implode( '', $output );
return $output;
}
/**
* Get the term list HTML.
*
* @param array $terms The terms to output.
* @param object $taxonomy The taxonomy object.
*
* @return string
*/
private function generate_terms_list( $terms, $taxonomy ) {
if ( empty( $terms ) ) {
return '';
}
if ( is_taxonomy_hierarchical( $taxonomy ) ) {
return $this->generate_terms_list_hierarchical( $terms, $taxonomy );
}
return $this->generate_terms_list_flat( $terms, $taxonomy );
}
/**
* Get the term list HTML for non-hierarchical taxonomies.
*
* @param array $terms The terms to output.
* @param string $taxonomy Taxonomy name.
*
* @return string
*/
private function generate_terms_list_flat( $terms, $taxonomy ) {
$output = [];
foreach ( $terms as $term ) {
$output[] = '<li class="rank-math-html-sitemap__item">'
. '<a href="' . esc_url( $this->get_term_link( (int) $term->term_id, $taxonomy ) ) . '" class="rank-math-html-sitemap__link">'
. esc_html( $this->get_term_title( $term, $taxonomy ) )
. '</a>'
. '</li>';
}
return implode( '', $output );
}
/**
* Get the term list HTML for hierarchical taxonomies. This will output the
* terms in a nested list.
*
* @param array $terms The terms to output.
* @param string $taxonomy The taxonomy name.
* @param bool $remove_children Whether to remove terms that have a parent.
*
* @return string
*/
private function generate_terms_list_hierarchical( $terms, $taxonomy, $remove_children = true ) {
$output = [];
if ( $remove_children ) {
// Remove initial with parents because they are queried below in $this->get_terms!
$terms = $this->remove_with_parent( $terms );
}
foreach ( $terms as $term ) {
$output[] = '<li class="rank-math-html-sitemap__item">'
. '<a href="' . esc_url( $this->get_term_link( (int) $term->term_id, $taxonomy ) ) . '" class="rank-math-html-sitemap__link">'
. esc_html( $this->get_term_title( $term, $taxonomy ) )
. '</a>';
$children = $this->get_terms( $taxonomy, $term->term_id );
if ( ! empty( $children ) ) {
$output[] = '<ul class="rank-math-html-sitemap__list">';
$output[] = $this->generate_terms_list_hierarchical( $children, $taxonomy, false );
$output[] = '</ul>';
}
$output[] = '</li>';
}
return implode( '', $output );
}
/**
* Get the term link.
*
* @param int $term_id The term ID.
* @param string $taxonomy The taxonomy name.
*
* @return string
*/
private function get_term_link( $term_id, $taxonomy ) {
$term = get_term( $term_id, $taxonomy );
if ( is_wp_error( $term ) ) {
return '';
}
return get_term_link( $term );
}
/**
* Get the term title.
*
* @param object $term The term data.
* @param string $taxonomy The taxonomy name.
*
* @return string
*/
private function get_term_title( $term, $taxonomy ) {
if ( Helper::get_settings( 'sitemap.html_sitemap_seo_titles' ) !== 'seo_titles' ) {
return $term->name;
}
// Custom SEO title.
$meta = get_term_meta( $term->term_id, 'rank_math_title', true );
if ( ! empty( $meta ) ) {
return Helper::replace_vars( $meta, get_term( $term->term_id, $taxonomy ) );
}
// Default SEO title from the global settings.
$template = Helper::get_settings( "titles.tax_{$taxonomy}_title" );
if ( ! empty( $template ) ) {
return Helper::replace_vars( $template, get_term( $term->term_id, $taxonomy ) );
}
// Fallback to term name.
return $term->name;
}
/**
* Removes terms that have a parent from the list.
*
* @param array $terms The terms list.
*
* @return array
*/
private function remove_with_parent( $terms ) {
return array_filter(
$terms,
function ( $term ) {
return ! $term->parent;
}
);
}
/**
* Remove terms that are not indexable.
*
* @param array $terms Array of terms.
* @param string $taxonomy Taxonomy name that `$terms` are part of.
* @return array
*/
private function get_indexable_terms( $terms, $taxonomy ) {
return array_filter(
$terms,
function( $term ) use ( $taxonomy ) {
return SitemapBase::is_object_indexable( get_term( $term->term_id, $taxonomy ), 'term' );
}
);
}
}

View File

@@ -0,0 +1 @@
<?php // Silence is golden.

View File

@@ -0,0 +1,312 @@
<?php
/**
* The sitemap provider for author archives.
*
* @since 0.9.0
* @package RankMath
* @subpackage RankMath\Sitemap
* @author Rank Math <support@rankmath.com>
*
* @copyright Copyright (C) 2008-2019, Yoast BV
* The following code is a derivative work of the code from the Yoast(https://github.com/Yoast/wordpress-seo/), which is licensed under GPL v3.
*/
namespace RankMath\Sitemap\Providers;
use DateTime;
use DateTimeZone;
use RankMath\Helper;
use RankMath\Sitemap\Router;
use RankMath\Sitemap\Sitemap;
use RankMath\Traits\Hooker;
defined( 'ABSPATH' ) || exit;
/**
* Author class.
*/
class Author implements Provider {
use Hooker;
/**
* Holds the Sitemap slug.
*
* @var string
*/
protected $sitemap_slug = null;
/**
* The constructor.
*/
public function __construct() {
$this->sitemap_slug = Router::get_sitemap_slug( 'author' );
$this->filter( 'rank_math/sitemap/author/query', 'exclude_users', 5 );
$this->filter( 'rank_math/sitemap/author/query', 'exclude_roles', 5 );
$this->filter( 'rank_math/sitemap/author/query', 'exclude_post_types', 5 );
}
/**
* Check if provider supports given item type.
*
* @param string $type Type string to check for.
* @return boolean
*/
public function handles_type( $type ) {
return $this->sitemap_slug === $type && Helper::get_settings( 'sitemap.authors_sitemap' );
}
/**
* Get set of sitemaps index link data.
*
* @param int $max_entries Entries per sitemap.
* @return array
*/
public function get_index_links( $max_entries ) {
if ( ! Helper::get_settings( 'sitemap.authors_sitemap' ) ) {
return [];
}
$users = $this->get_index_users();
if ( empty( $users ) ) {
return [];
}
$page = 1;
$index = [];
$user_pages = array_chunk( $users, $max_entries );
if ( 1 === count( $user_pages ) ) {
$page = '';
}
foreach ( $user_pages as $user_page ) {
$user = array_shift( $user_page ); // Time descending, first user on page is most recently updated.
$item = $this->do_filter(
'sitemap/index/entry',
[
'loc' => Router::get_base_url( $this->sitemap_slug . '-sitemap' . $page . '.xml' ),
'lastmod' => '@' . $user->last_update,
],
'author',
$user
);
if ( ! $item ) {
continue;
}
$index[] = $item;
$page++;
}
return $index;
}
/**
* Get set of sitemap link data.
*
* @param string $type Sitemap type.
* @param int $max_entries Entries per sitemap.
* @param int $current_page Current page of the sitemap.
* @return array
*/
public function get_sitemap_links( $type, $max_entries, $current_page ) {
$links = [];
if ( $current_page < 1 ) {
$current_page = 1;
}
$users = $this->get_users(
[
'offset' => ( $current_page - 1 ) * $max_entries,
'number' => $max_entries,
]
);
if ( empty( $users ) ) {
return $links;
}
Sitemap::maybe_redirect( count( $users ), $max_entries );
foreach ( $users as $user ) {
$url = $this->get_sitemap_url( $user );
if ( ! empty( $url ) ) {
$links[] = $url;
}
}
return $links;
}
/**
* Get sitemap urlset.
*
* @param WP_User $user User instance.
*
* @return bool|array
*/
private function get_sitemap_url( $user ) {
$author_link = get_author_posts_url( $user->ID );
if ( empty( $author_link ) ) {
return false;
}
$mod = isset( $user->last_update ) ? $user->last_update : strtotime( $user->user_registered );
$date = new DateTime();
$date->setTimestamp( $mod );
$date->setTimezone( new DateTimeZone( 'UTC' ) );
$url = [
'loc' => $author_link,
'mod' => $date->format( DATE_W3C ),
];
/** This filter is documented at includes/modules/sitemap/providers/class-post-type.php */
return $this->do_filter( 'sitemap/entry', $url, 'user', $user );
}
/**
* Retrieve users, taking account of all necessary exclusions.
*
* @param array $args Arguments to add.
* @return array
*/
public function get_users( $args = [] ) {
$defaults = [
'orderby' => 'meta_value_num',
'order' => 'DESC',
'meta_query' => [
'relation' => 'AND',
[
'relation' => 'OR',
[
'key' => 'last_update',
],
[
'key' => 'last_update',
'compare' => 'NOT EXISTS',
],
],
[
'relation' => 'OR',
[
'key' => 'rank_math_robots',
'value' => 'noindex',
'compare' => 'NOT LIKE',
],
[
'key' => 'rank_math_robots',
'compare' => 'NOT EXISTS',
],
],
],
];
$args = $this->do_filter( 'sitemap/author/query', wp_parse_args( $args, $defaults ) );
return get_users( $args );
}
/**
* Exclude users.
*
* @param array $args Array of user query arguments.
*
* @return array
*/
public function exclude_users( $args ) {
$exclude = Helper::get_settings( 'sitemap.exclude_users' );
if ( ! empty( $exclude ) ) {
$args['exclude'] = wp_parse_id_list( $exclude );
}
return $args;
}
/**
* Exclude roles.
*
* @param array $args Array of user query arguments.
*
* @return array
*/
public function exclude_roles( $args ) {
$exclude_roles = Helper::get_settings( 'sitemap.exclude_roles' );
if ( ! empty( $exclude_roles ) ) {
$args['role__not_in'] = $exclude_roles;
}
return $args;
}
/**
* Exclude post types.
*
* @param array $args Array of user query arguments.
*
* @return array
*/
public function exclude_post_types( $args ) {
// Exclude post types.
$public_post_types = get_post_types( [ 'public' => true ] );
// We're not supporting sitemaps for author pages for attachments.
unset( $public_post_types['attachment'] );
$args['has_published_posts'] = array_keys( $public_post_types );
return $args;
}
/**
* Get all users according to author sitemap settings.
*
* @return array
*/
private function get_index_users() {
global $wpdb;
$exclude_users = Helper::get_settings( 'sitemap.exclude_users' );
$exclude_roles = Helper::get_settings( 'sitemap.exclude_roles' );
$exclude_users_query = ! $exclude_users ? '' : 'AND post_author NOT IN ( ' . esc_sql( $exclude_users ) . ' )';
$exclude_roles_query = '';
$meta_query = "(
( um.meta_key = 'rank_math_robots' AND um.meta_value NOT LIKE '%noindex%' )
OR um.user_id IS NULL
)
AND ( umt1.meta_key = 'last_update' OR umt1.user_id IS NULL )
";
if ( $exclude_roles ) {
$exclude_roles_query = "AND ( umt.meta_key ='wp_capabilities' AND ( ";
foreach ( $exclude_roles as $key => $role ) {
$exclude_roles_query .= 0 === $key ? " umt.meta_value NOT LIKE '%" . esc_sql( $role ) . "%'" : " AND umt.meta_value NOT LIKE '%" . esc_sql( $role ) . "%'";
}
$exclude_roles_query .= ' ) )';
}
$meta_query .= $exclude_roles_query;
$sql = "
SELECT u.ID, umt1.meta_value as last_update
FROM {$wpdb->users} as u
LEFT JOIN {$wpdb->usermeta} AS um ON ( u.ID = um.user_id AND um.meta_key = 'rank_math_robots' )
LEFT JOIN {$wpdb->usermeta} AS umt ON ( u.ID = umt.user_id AND umt.meta_key = 'wp_capabilities' )
LEFT JOIN {$wpdb->usermeta} AS umt1 ON ( u.ID = umt1.user_id AND umt1.meta_key = 'last_update' )
WHERE ( {$meta_query} )
AND u.ID IN (
SELECT post_author
FROM {$wpdb->posts} as p
WHERE p.post_status = 'publish' AND p.post_password = ''
{$exclude_users_query}
)
ORDER BY umt1.meta_value DESC
";
return $wpdb->get_results( $sql ); // phpcs:ignore
}
}

View File

@@ -0,0 +1,575 @@
<?php
/**
* The sitemap provider for post types.
*
* @since 0.9.0
* @package RankMath
* @subpackage RankMath\Sitemap
* @author Rank Math <support@rankmath.com>
*
* @copyright Copyright (C) 2008-2019, Yoast BV
* The following code is a derivative work of the code from the Yoast(https://github.com/Yoast/wordpress-seo/), which is licensed under GPL v3.
*/
namespace RankMath\Sitemap\Providers;
use RankMath\Helper;
use RankMath\Traits\Hooker;
use RankMath\Sitemap\Router;
use RankMath\Sitemap\Sitemap;
use RankMath\Sitemap\Classifier;
use RankMath\Sitemap\Image_Parser;
defined( 'ABSPATH' ) || exit;
/**
* Post type provider class.
*/
class Post_Type implements Provider {
use Hooker;
/**
* Holds the `home_url()` value to speed up loops.
*
* @var string
*/
protected $home_url = null;
/**
* Holds image parser instance.
*
* @var Image_Parser
*/
protected $image_parser = null;
/**
* Holds link classifier.
*
* @var Classifier
*/
protected $classifier = null;
/**
* Static front page ID.
*
* @var int
*/
protected $page_on_front_id = null;
/**
* Posts page ID.
*
* @var int
*/
protected $page_for_posts_id = null;
/**
* Check if provider supports given item type.
*
* @param string $type Type string to check for.
*
* @return boolean
*/
public function handles_type( $type ) {
if (
false === post_type_exists( $type ) ||
! Helper::get_settings( 'sitemap.pt_' . $type . '_sitemap' ) ||
( 'attachment' === $type && Helper::get_settings( 'general.attachment_redirect_urls', true ) )
) {
return false;
}
/**
* Filter decision if post type is excluded from the XML sitemap.
*
* @param bool $exclude Default false.
* @param string $type Post type name.
*/
return ! $this->do_filter( 'sitemap/exclude_post_type', false, $type );
}
/**
* Get set of sitemaps index link data.
*
* @param int $max_entries Entries per sitemap.
*
* @return array
*/
public function get_index_links( $max_entries ) {
global $wpdb;
$post_types = Helper::get_accessible_post_types();
$post_types = array_filter( $post_types, [ $this, 'handles_type' ] );
$last_modified_times = Sitemap::get_last_modified_gmt( $post_types, true );
$index = [];
foreach ( $post_types as $post_type ) {
$total_count = $this->get_post_type_count( $post_type );
if ( 0 === $total_count ) {
continue;
}
$max_pages = 1;
if ( $total_count > $max_entries ) {
$max_pages = (int) ceil( $total_count / $max_entries );
}
$all_dates = [];
if ( $max_pages > 1 ) {
$sql = "
SELECT post_modified_gmt
FROM ( SELECT @rownum:=@rownum rownum, $wpdb->posts.post_modified_gmt
FROM ( SELECT @rownum:=0 ) r, $wpdb->posts
WHERE post_status IN ( 'publish', 'inherit' )
AND post_type = %s
ORDER BY post_modified_gmt ASC
)
x WHERE rownum %% %d = 0 ORDER BY post_modified_gmt DESC";
$all_dates = $wpdb->get_col( $wpdb->prepare( $sql, $post_type, $max_entries ) ); // phpcs:ignore
}
for ( $page_counter = 0; $page_counter < $max_pages; $page_counter++ ) {
$current_page = ( $max_pages > 1 ) ? ( $page_counter + 1 ) : '';
$date = false;
if ( isset( $all_dates[ $page_counter ] ) ) {
$date = $all_dates[ $page_counter ];
} elseif ( ! empty( $last_modified_times[ $post_type ] ) ) {
$date = $last_modified_times[ $post_type ];
}
$item = $this->do_filter(
'sitemap/index/entry',
[
'loc' => Router::get_base_url( $post_type . '-sitemap' . $current_page . '.xml' ),
'lastmod' => $date,
],
'post',
$post_type,
);
if ( ! $item ) {
continue;
}
$index[] = $item;
}
}
return $index;
}
/**
* Get set of sitemap link data.
*
* @param string $type Sitemap type.
* @param int $max_entries Entries per sitemap.
* @param int $current_page Current page of the sitemap.
*
* @return array
*/
public function get_sitemap_links( $type, $max_entries, $current_page ) {
$links = [];
$steps = $max_entries;
$offset = ( $current_page > 1 ) ? ( ( $current_page - 1 ) * $max_entries ) : 0;
$total = ( $offset + $max_entries );
$typecount = $this->get_post_type_count( $type );
Sitemap::maybe_redirect( $typecount, $max_entries );
if ( $total > $typecount ) {
$total = $typecount;
}
if ( 1 === $current_page ) {
$links = array_merge( $links, $this->get_first_links( $type ) );
}
if ( 0 === $typecount ) {
return $links;
}
$stacked_urls = [];
while ( $total > $offset ) {
$posts = $this->get_posts( $type, $steps, $offset );
$offset += $steps;
if ( empty( $posts ) ) {
continue;
}
foreach ( $posts as $post ) {
$post_id = (int) $post->ID;
if ( ! Sitemap::is_object_indexable( $post_id ) ) {
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 $user Data object for the URL.
*/
$url = $this->do_filter( 'sitemap/entry', $url, 'post', $post );
if ( empty( $url ) ) {
continue;
}
$stacked_urls[] = $url['loc'];
if ( $post_id === $this->get_page_for_posts_id() || $post_id === $this->get_page_on_front_id() ) {
array_unshift( $links, $url );
continue;
}
$links[] = $url;
}
unset( $post, $url );
}
return $links;
}
/**
* Get count of posts for post type.
*
* @param string $post_types Post types to retrieve count for.
*
* @return int
*/
protected function get_post_type_count( $post_types ) {
global $wpdb;
$posts_to_exclude = 'page' === $post_types ? $this->get_blog_page_id() : '';
$post_status = 'attachment' === $post_types ? [ 'publish', 'inherit' ] : [ 'publish' ];
/**
* Filter to add a JOIN clause for get_post_type_count(post types) query.
*
* @param string $join SQL join clause, defaults to an empty string.
* @param array $post_types Post types.
*/
$join_filter = $this->do_filter( 'sitemap/post_count/join', '', $post_types );
/**
* Filter to add a WHERE clause for get_post_type_count(post types) query.
*
* @param string $where SQL WHERE query, defaults to an empty string.
* @param array $post_types Post types.
*/
$where_filter = $this->do_filter( 'sitemap/post_count/where', '', $post_types );
$sql = "SELECT COUNT( DISTINCT p.ID ) as count FROM {$wpdb->posts} as p
{$join_filter}
LEFT JOIN {$wpdb->postmeta} AS pm ON ( p.ID = pm.post_id AND pm.meta_key = 'rank_math_robots' )
WHERE (
( pm.meta_key = 'rank_math_robots' AND pm.meta_value NOT LIKE '%noindex%' ) OR
pm.post_id IS NULL
)
AND p.post_type = '{$post_types}' AND p.post_status IN ( '" . join( "', '", esc_sql( $post_status ) ) . "' ) AND p.post_password = ''
AND p.ID != '{$posts_to_exclude}'
{$where_filter}";
return (int) $wpdb->get_var( $sql ); // phpcs:ignore
}
/**
* 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 = [];
$needs_archive = true;
if ( ! $this->get_page_on_front_id() && ( 'post' === $post_type || 'page' === $post_type ) ) {
$needs_archive = false;
$links[] = [
'loc' => $this->get_home_url(),
'mod' => Sitemap::get_last_modified_gmt( $post_type ),
];
} elseif ( $this->get_page_on_front_id() && 'post' === $post_type && $this->get_page_for_posts_id() ) {
$needs_archive = false;
$links[] = Sitemap::is_object_indexable( $this->get_page_for_posts_id() ) ? [ 'loc' => get_permalink( $this->get_page_for_posts_id() ) ] : '';
}
if ( ! $needs_archive ) {
return array_filter( $links );
}
$archive_url = $this->get_post_type_archive_link( $post_type );
/**
* Filter the URL Rank Math 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 = $this->do_filter( 'sitemap/post_type_archive_link', $archive_url, $post_type );
if ( $archive_url ) {
$links[] = [
'loc' => $archive_url,
'mod' => Sitemap::get_last_modified_gmt( $post_type ),
];
}
return $links;
}
/**
* Get URL for a post type archive.
*
* @param string $post_type Post type.
*
* @return string|boolean URL or false if it should be excluded.
*/
protected function get_post_type_archive_link( $post_type ) {
// Post archive should be excluded if it isn't front page or posts page.
if ( 'post' === $post_type && get_option( 'show_on_front' ) !== 'posts' && ! $this->get_page_for_posts_id() ) {
return false;
}
return get_post_type_archive_link( $post_type );
}
/**
* Retrieve set of posts with optimized query routine.
*
* @param array $post_types Post type to retrieve.
* @param int $count Count of posts to retrieve.
* @param int $offset Starting offset.
*
* @return object[]
*/
protected function get_posts( $post_types, $count, $offset ) {
global $wpdb;
$posts_to_exclude = 'page' === $post_types ? $this->get_blog_page_id() : '';
$post_status = 'attachment' === $post_types ? [ 'publish', 'inherit' ] : [ 'publish' ];
/**
* Filter to add a JOIN clause for get_posts(types) query.
*
* @param string $join SQL join clause, defaults to an empty string.
* @param array $post_types Post types.
*/
$join_filter = $this->do_filter( 'sitemap/get_posts/join', '', $post_types );
/**
* Filter to add a WHERE clause for get_posts(types) query.
*
* @param string $where SQL WHERE query, defaults to an empty string.
* @param array $post_types Post types.
*/
$where_filter = $this->do_filter( 'sitemap/get_posts/where', '', $post_types );
$sql = "
SELECT l.ID, post_title, post_content, post_name, post_parent, post_author, post_modified_gmt, post_date, post_date_gmt, post_type
FROM (
SELECT DISTINCT p.ID FROM {$wpdb->posts} as p
{$join_filter}
LEFT JOIN {$wpdb->postmeta} AS pm ON ( p.ID = pm.post_id AND pm.meta_key = 'rank_math_robots' )
WHERE (
( pm.meta_key = 'rank_math_robots' AND pm.meta_value NOT LIKE '%noindex%' ) OR
pm.post_id IS NULL
)
AND p.post_type = '{$post_types}' AND p.post_status IN ( '" . join( "', '", esc_sql( $post_status ) ) . "' ) AND p.post_password = ''
AND p.ID != '{$posts_to_exclude}'
{$where_filter}
ORDER BY p.post_modified DESC LIMIT %d OFFSET %d
)
o JOIN {$wpdb->posts} l ON l.ID = o.ID
";
$posts = $wpdb->get_results( $wpdb->prepare( $sql, $count, $offset ) ); // phpcs:ignore
$post_ids = [];
foreach ( $posts as $post ) {
$post->post_status = 'publish';
$post->filter = 'sample';
$post_ids[] = $post->ID;
}
update_meta_cache( 'post', $post_ids );
return $posts;
}
/**
* Get where clause to query data.
*
* @param array $post_types Post types slug.
*
* @return string
*/
protected function get_sql_where_clause( $post_types ) {
global $wpdb;
$join = '';
$status = "{$wpdb->posts}.post_status = 'publish'";
// Based on WP_Query->get_posts(). R.
if ( in_array( 'attachment', $post_types, true ) ) {
$join = " LEFT JOIN {$wpdb->posts} AS p2 ON ({$wpdb->posts}.post_parent = p2.ID) ";
$status = "p2.post_status = 'publish'";
}
$where_clause = "
{$join}
WHERE {$status}
AND {$wpdb->posts}.post_type IN ( '" . join( "', '", esc_sql( $post_types ) ) . "' )
AND {$wpdb->posts}.post_password = ''
";
return $where_clause;
}
/**
* Produce array of URL parts for given post object.
*
* @param object $post Post object to get URL parts for.
*
* @return array|boolean
*/
protected function get_url( $post ) {
$url = [];
/**
* Filter the post object before it gets added to the sitemap.
* This allows you to add custom properties to the post object, or replace it entirely.
*
* @param object $post Post object.
*/
$post = $this->do_filter( 'sitemap/post_object', $post );
if ( ! $post ) {
return false;
}
/**
* Filter the URL Rank Math 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'] = $this->do_filter( 'sitemap/xml_post_url', get_permalink( $post ), $post );
/**
* Do not include external URLs.
*
* @see https://wordpress.org/plugins/page-links-to/ can rewrite permalinks to external URLs.
*/
if ( 'external' === $this->get_classifier()->classify( $url['loc'] ) ) {
return false;
}
$modified = max( $post->post_modified_gmt, $post->post_date_gmt );
if ( '0000-00-00 00:00:00' !== $modified ) {
$url['mod'] = $modified;
}
$canonical = Helper::get_post_meta( 'canonical_url', $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;
}
$url['images'] = ! is_null( $this->get_image_parser() ) ? $this->get_image_parser()->get_images( $post ) : [];
return $url;
}
/**
* Get front page ID.
*
* @return int
*/
protected function get_page_on_front_id() {
if ( is_null( $this->page_on_front_id ) ) {
$this->page_on_front_id = intval( get_option( 'page_on_front' ) );
}
return $this->page_on_front_id;
}
/**
* Get page for posts ID.
*
* @return int
*/
protected function get_page_for_posts_id() {
if ( is_null( $this->page_for_posts_id ) ) {
$this->page_for_posts_id = intval( get_option( 'page_for_posts' ) );
}
return $this->page_for_posts_id;
}
/**
* Get the Image Parser.
*
* @return Image_Parser
*/
protected function get_image_parser() {
if ( is_null( $this->image_parser ) ) {
$this->image_parser = new Image_Parser();
}
return $this->image_parser;
}
/**
* Get the link classifier.
*
* @return Classifier
*/
protected function get_classifier() {
if ( is_null( $this->classifier ) ) {
$this->classifier = new Classifier( $this->get_home_url() );
}
return $this->classifier;
}
/**
* Get Home URL.
*
* This has been moved from the constructor because wp_rewrite is not available on plugins_loaded in multisite.
* It will now be requested on need and not on initialization.
*
* @return string
*/
protected function get_home_url() {
if ( is_null( $this->home_url ) ) {
$this->home_url = user_trailingslashit( get_home_url() );
}
return $this->home_url;
}
/**
* Get Blog page id.
*
* @return int
*/
private function get_blog_page_id() {
return get_option( 'show_on_front' ) === 'page' && $this->get_page_for_posts_id() ? $this->get_page_for_posts_id() : '';
}
}

View File

@@ -0,0 +1,316 @@
<?php
/**
* The sitemap provider for taxonomies.
*
* @since 0.9.0
* @package RankMath
* @subpackage RankMath\Sitemap
* @author Rank Math <support@rankmath.com>
*
* @copyright Copyright (C) 2008-2019, Yoast BV
* The following code is a derivative work of the code from the Yoast(https://github.com/Yoast/wordpress-seo/), which is licensed under GPL v3.
*/
namespace RankMath\Sitemap\Providers;
use RankMath\Helper;
use RankMath\Traits\Hooker;
use RankMath\Sitemap\Router;
use RankMath\Sitemap\Sitemap;
use RankMath\Sitemap\Image_Parser;
defined( 'ABSPATH' ) || exit;
/**
* Taxonomy provider
*/
class Taxonomy implements Provider {
use Hooker;
/**
* Holds image parser instance.
*
* @var Image_Parser
*/
protected static $image_parser;
/**
* Check if provider supports given item type.
*
* @param string $type Type string to check for.
* @return boolean
*/
public function handles_type( $type ) {
if ( is_a( $type, 'WP_Taxonomy' ) ) {
$type = $type->name;
}
if (
empty( $type ) ||
false === taxonomy_exists( $type ) ||
false === Helper::is_taxonomy_viewable( $type ) ||
false === Helper::is_taxonomy_indexable( $type ) ||
in_array( $type, [ 'link_category', 'nav_menu', 'post_format' ], true )
) {
return false;
}
/**
* Filter decision if taxonomy is excluded from the XML sitemap.
*
* @param bool $exclude Default false.
* @param string $type Taxonomy name.
*/
return ! $this->do_filter( 'sitemap/exclude_taxonomy', false, $type );
}
/**
* Get set of sitemaps index link data.
*
* @param int $max_entries Entries per sitemap.
* @return array
*/
public function get_index_links( $max_entries ) {
$taxonomies = Helper::get_accessible_taxonomies();
$taxonomies = array_filter( $taxonomies, [ $this, 'handles_type' ] );
if ( empty( $taxonomies ) ) {
return [];
}
// 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 boolean $exclude Defaults to true.
* @param array $taxonomy_names Array of names for the taxonomies being processed.
*/
$hide_empty = $this->do_filter( 'sitemap/exclude_empty_terms', true, $taxonomies );
$all_taxonomies = [];
foreach ( $taxonomies as $taxonomy_name => $object ) {
$all_taxonomies[ $taxonomy_name ] = get_terms(
$taxonomy_name,
[
'hide_empty' => $hide_empty,
'fields' => 'ids',
'orderby' => 'name',
'meta_query' => [
'relation' => 'OR',
[
'key' => 'rank_math_robots',
'value' => 'noindex',
'compare' => 'NOT LIKE',
],
[
'key' => 'rank_math_robots',
'compare' => 'NOT EXISTS',
],
],
]
);
}
$index = [];
foreach ( $all_taxonomies as $tax_name => $terms ) {
if ( is_wp_error( $terms ) ) {
continue;
}
$max_pages = 1;
$total_count = empty( $terms ) ? 1 : count( $terms );
if ( $total_count > $max_entries ) {
$max_pages = (int) ceil( $total_count / $max_entries );
}
$tax = $taxonomies[ $tax_name ];
if ( ! is_array( $tax->object_type ) || count( $tax->object_type ) === 0 ) {
continue;
}
$last_modified_gmt = Sitemap::get_last_modified_gmt( $tax->object_type );
for ( $page_counter = 0; $page_counter < $max_pages; $page_counter++ ) {
$current_page = ( $max_pages > 1 ) ? ( $page_counter + 1 ) : '';
$terms_page = array_splice( $terms, 0, $max_entries );
if ( ! $terms_page ) {
continue;
}
$query = new \WP_Query(
[
'post_type' => $tax->object_type,
'tax_query' => [
[
'taxonomy' => $tax_name,
'terms' => $terms_page,
],
],
'orderby' => 'modified',
'order' => 'DESC',
'posts_per_page' => 1,
]
);
$item = $this->do_filter(
'sitemap/index/entry',
[
'loc' => Router::get_base_url( $tax_name . '-sitemap' . $current_page . '.xml' ),
'lastmod' => $query->have_posts() ? $query->posts[0]->post_modified_gmt : $last_modified_gmt,
],
'term',
$tax_name,
);
if ( ! $item ) {
continue;
}
$index[] = $item;
}
}
return $index;
}
/**
* Get set of sitemap link data.
*
* @param string $type Sitemap type.
* @param int $max_entries Entries per sitemap.
* @param int $current_page Current page of the sitemap.
* @return array
*/
public function get_sitemap_links( $type, $max_entries, $current_page ) {
$links = [];
$taxonomy = get_taxonomy( $type );
$terms = $this->get_terms( $taxonomy, $max_entries, $current_page );
Sitemap::maybe_redirect( count( $this->get_terms( $taxonomy, 0, $current_page ) ), $max_entries );
foreach ( $terms as $term ) {
$url = [];
if ( ! Sitemap::is_object_indexable( $term, 'term' ) ) {
continue;
}
$link = $this->get_term_link( $term );
if ( ! $link ) {
continue;
}
$url['loc'] = $link;
$url['mod'] = $this->get_lastmod( $term );
$url['images'] = ! is_null( $this->get_image_parser() ) ? $this->get_image_parser()->get_term_images( $term ) : [];
/** This filter is documented at inc/sitemaps/class-post-type-sitemap-provider.php */
$url = $this->do_filter( 'sitemap/entry', $url, 'term', $term );
if ( ! empty( $url ) ) {
$links[] = $url;
}
}
return $links;
}
/**
* Get the Image Parser.
*
* @return Image_Parser
*/
protected function get_image_parser() {
if ( class_exists( 'RankMath\Sitemap\Image_Parser' ) && ! isset( self::$image_parser ) ) {
self::$image_parser = new Image_Parser();
}
return self::$image_parser;
}
/**
* Get terms for taxonomy.
*
* @param object $taxonomy Taxonomy name.
* @param int $max_entries Entries per sitemap.
* @param int $current_page Current page of the sitemap.
* @return false|array
*/
private function get_terms( $taxonomy, $max_entries, $current_page ) {
$offset = $current_page > 1 ? ( ( $current_page - 1 ) * $max_entries ) : 0;
$hide_empty = ! Helper::get_settings( 'sitemap.tax_' . $taxonomy->name . '_include_empty' );
// Getting terms.
$terms = get_terms(
[
'taxonomy' => $taxonomy->name,
'orderby' => 'term_order',
'hide_empty' => $hide_empty,
'offset' => $offset,
'number' => $max_entries,
'exclude' => wp_parse_id_list( Helper::get_settings( 'sitemap.exclude_terms' ) ),
/*
* Limits aren't included in queries when hierarchical is set to true (by default).
*
* @link: https://github.com/WordPress/WordPress/blob/5.3/wp-includes/class-wp-term-query.php#L558-L567
*/
'hierarchical' => false,
'update_term_meta_cache' => false,
]
);
if ( is_wp_error( $terms ) || empty( $terms ) ) {
return [];
}
return $terms;
}
/**
* Get term link.
*
* @param WP_Term $term Term object.
* @return string
*/
private function get_term_link( $term ) {
$url = get_term_link( $term, $term->taxonomy );
$canonical = Helper::get_term_meta( 'canonical_url', $term, $term->taxonomy );
if ( $canonical && $canonical !== $url ) {
/*
* Let's assume that if a canonical is set for this term and it's different from
* the URL of this term, 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;
}
return $url;
}
/**
* Get last modified date of post by term.
*
* @param WP_Term $term Term object.
* @return string
*/
public function get_lastmod( $term ) {
global $wpdb;
return $wpdb->get_var(
$wpdb->prepare(
"
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 ('publish', 'inherit')
AND p.post_password = ''
",
$term->taxonomy,
$term->term_id
)
);
}
}

View File

@@ -0,0 +1 @@
<?php // Silence is golden.

View File

@@ -0,0 +1,48 @@
<?php
/**
* The sitemap provider interface.
*
* @since 0.9.0
* @package RankMath
* @subpackage RankMath\Sitemap
* @author Rank Math <support@rankmath.com>
*
* @copyright Copyright (C) 2008-2019, Yoast BV
* The following code is a derivative work of the code from the Yoast(https://github.com/Yoast/wordpress-seo/), which is licensed under GPL v3.
*/
namespace RankMath\Sitemap\Providers;
defined( 'ABSPATH' ) || exit;
/**
* Provider interface.
*/
interface Provider {
/**
* Check if provider supports given item type.
*
* @param string $type Type string to check for.
* @return boolean
*/
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 );
}

View File

@@ -0,0 +1,68 @@
<?php
/**
* Sitemap settings - Authors tab.
*
* @package RankMath
* @subpackage RankMath\Sitemap
*/
use RankMath\Helper;
defined( 'ABSPATH' ) || exit;
$roles = Helper::get_roles();
$default = $roles;
unset( $default['administrator'], $default['editor'], $default['author'] );
$dep = [
'relation' => 'OR',
[ 'authors_sitemap', 'on' ],
[ 'authors_html_sitemap', 'on' ],
];
$cmb->add_field(
[
'id' => 'authors_sitemap',
'type' => 'toggle',
'name' => esc_html__( 'Include in Sitemap', 'rank-math' ),
'desc' => esc_html__( 'Include author archives in the XML sitemap.', 'rank-math' ),
'default' => 'on',
]
);
$cmb->add_field(
[
'id' => 'authors_html_sitemap',
'type' => 'toggle',
'name' => esc_html__( 'Include in HTML Sitemap', 'rank-math' ),
'desc' => esc_html__( 'Include author archives in the HTML sitemap if it\'s enabled.', 'rank-math' ),
'default' => 'on',
'classes' => [
'rank-math-html-sitemap',
! Helper::get_settings( 'sitemap.html_sitemap' ) ? 'hidden' : '',
],
]
);
$cmb->add_field(
[
'id' => 'exclude_roles',
'type' => 'multicheck',
'name' => esc_html__( 'Exclude User Roles', 'rank-math' ),
'desc' => esc_html__( 'Selected roles will be excluded from the XML &amp; HTML sitemaps.', 'rank-math' ),
'options' => $roles,
'default' => $default,
'select_all_button' => false,
'dep' => $dep,
]
);
$cmb->add_field(
[
'id' => 'exclude_users',
'type' => 'text',
'name' => esc_html__( 'Exclude Users', 'rank-math' ),
'desc' => esc_html__( 'Add user IDs, separated by commas, to exclude them from the sitemap.', 'rank-math' ),
'dep' => $dep,
]
);

View File

@@ -0,0 +1,77 @@
<?php
/**
* Sitemap settings - General tab.
*
* @package RankMath
* @subpackage RankMath\Sitemap
*/
defined( 'ABSPATH' ) || exit;
if ( class_exists( 'SitePress' ) ) {
$cmb->add_field(
[
'id' => 'multilingual_sitemap_notice',
'type' => 'notice',
'what' => 'warning',
'content' => sprintf(
/* translators: hreflang tags documentation link */
esc_html__( 'Rank Math generates the default Sitemaps only and WPML takes care of the rest. When a search engine bot visits any post/page, it is shown hreflang tag that helps it crawl the translated pages. This is one of the recommended methods by Google. Please %s', 'rank-math' ),
'<a href="https://support.google.com/webmasters/answer/189077?hl=en" target="blank">' . esc_html__( 'read here', 'rank-math' ) . '</a>'
),
]
);
}
$cmb->add_field(
[
'id' => 'items_per_page',
'type' => 'text',
'name' => esc_html__( 'Links Per Sitemap', 'rank-math' ),
'desc' => esc_html__( 'Max number of links on each sitemap page.', 'rank-math' ),
'default' => '200',
'attributes' => [ 'type' => 'number' ],
'classes' => 'rank-math-advanced-option',
]
);
$cmb->add_field(
[
'id' => 'include_images',
'type' => 'toggle',
'name' => esc_html__( 'Images in Sitemaps', 'rank-math' ),
'desc' => esc_html__( 'Include reference to images from the post content in sitemaps. This helps search engines index the important images on your pages.', 'rank-math' ),
'default' => 'on',
]
);
$cmb->add_field(
[
'id' => 'include_featured_image',
'type' => 'toggle',
'name' => esc_html__( 'Include Featured Images', 'rank-math' ),
'desc' => esc_html__( 'Include the Featured Image too, even if it does not appear directly in the post content.', 'rank-math' ),
'default' => 'off',
'dep' => [ [ 'include_images', 'on' ] ],
]
);
$cmb->add_field(
[
'id' => 'exclude_posts',
'type' => 'text',
'name' => esc_html__( 'Exclude Posts', 'rank-math' ),
'desc' => esc_html__( 'Enter post IDs of posts you want to exclude from the sitemap, separated by commas. This option **applies** to all posts types including posts, pages, and custom post types.', 'rank-math' ),
'classes' => 'rank-math-advanced-option',
]
);
$cmb->add_field(
[
'id' => 'exclude_terms',
'type' => 'text',
'name' => esc_html__( 'Exclude Terms', 'rank-math' ),
'desc' => esc_html__( 'Add term IDs, separated by comma. This option is applied for all taxonomies.', 'rank-math' ),
'classes' => 'rank-math-advanced-option',
]
);

View File

@@ -0,0 +1,134 @@
<?php
/**
* Sitemap settings - HTML Sitemap tab.
*
* @package RankMath
* @subpackage RankMath\Sitemap
*/
use RankMath\Helper;
defined( 'ABSPATH' ) || exit;
$cmb->add_field(
[
'id' => 'html_sitemap',
'type' => 'toggle',
'name' => esc_html__( 'HTML Sitemap', 'rank-math' ),
'desc' => esc_html__( 'Enable the HTML sitemap.', 'rank-math' ),
'default' => 'off',
]
);
$cmb->add_field(
[
'id' => 'html_sitemap_display',
'type' => 'radio_inline',
'name' => esc_html__( 'Display Format', 'rank-math' ),
'desc' => esc_html__( 'Choose how you want to display the HTML sitemap.', 'rank-math' ),
'options' => [
'shortcode' => esc_html__( 'Shortcode', 'rank-math' ),
'page' => esc_html__( 'Page', 'rank-math' ),
],
'default' => 'shortcode',
'dep' => [ [ 'html_sitemap', 'on' ] ],
]
);
$cmb->add_field(
[
'id' => 'html_sitemap_shortcode',
'type' => 'text',
'name' => esc_html__( 'Shortcode', 'rank-math' ),
'desc' => esc_html__( 'Use this shortcode to display the HTML sitemap.', 'rank-math' ),
'default' => '[rank_math_html_sitemap]',
'dep' => [
'relation' => 'AND',
[ 'html_sitemap', 'on' ],
[ 'html_sitemap_display', 'shortcode' ],
],
'classes' => 'rank-math-code',
'attributes' => [
'disabled' => 'disabled',
],
]
);
$rank_math_sitemap_page = Helper::get_settings( 'sitemap.html_sitemap_page' );
$rank_math_sitemap_page_options = [ '' => __( 'Select Page', 'rank-math' ) ];
$rank_math_sitemap_page_after = '<p class="rank-math-selected-page-message hidden">' . esc_html__( 'Selected page: ', 'rank-math' ) . '<span class="rank-math-selected-page"></span></p>';
if ( $rank_math_sitemap_page ) {
$rank_math_sitemap_page_options[ $rank_math_sitemap_page ] = get_the_title( $rank_math_sitemap_page );
$rank_math_sitemap_page_after = '<p class="rank-math-selected-page-message">' . sprintf(
/* translators: link to the selected page */
__( 'Selected page: <a href="%s" target="_blank" class="rank-math-selected-page">%s</a>', 'rank-math' ), // phpcs:ignore
get_permalink( $rank_math_sitemap_page ),
get_permalink( $rank_math_sitemap_page )
) . '</p>';
}
$cmb->add_field(
[
'id' => 'html_sitemap_page',
'type' => 'select',
'name' => esc_html__( 'Page', 'rank-math' ),
'desc' => esc_html__( 'Select the page to display the HTML sitemap. Once the settings are saved, the sitemap will be displayed below the content of the selected page.', 'rank-math' ),
'options' => $rank_math_sitemap_page_options,
'dep' => [
'relation' => 'AND',
[ 'html_sitemap', 'on' ],
[ 'html_sitemap_display', 'page' ],
],
'after' => $rank_math_sitemap_page_after,
'attributes' => [
'data-placeholder' => esc_html__( 'Select a page', 'rank-math' ),
'data-s2-pages' => 'true',
],
]
);
$cmb->add_field(
[
'id' => 'html_sitemap_sort',
'type' => 'select',
'name' => esc_html__( 'Sort By', 'rank-math' ),
'desc' => esc_html__( 'Choose how you want to sort the items in the HTML sitemap.', 'rank-math' ),
'options' => [
// Published Date, Modified Date, Alphabetical, Post ID.
'published' => esc_html__( 'Published Date', 'rank-math' ),
'modified' => esc_html__( 'Modified Date', 'rank-math' ),
'alphabetical' => esc_html__( 'Alphabetical', 'rank-math' ),
'post_id' => esc_html__( 'Post ID', 'rank-math' ),
],
'default' => 'published',
'dep' => [ [ 'html_sitemap', 'on' ] ],
]
);
$cmb->add_field(
[
'id' => 'html_sitemap_show_dates',
'type' => 'toggle',
'name' => esc_html__( 'Show Dates', 'rank-math' ),
'desc' => esc_html__( 'Show published dates for each post & page.', 'rank-math' ),
'default' => 'on',
'dep' => [ [ 'html_sitemap', 'on' ] ],
]
);
$cmb->add_field(
[
'id' => 'html_sitemap_seo_titles',
'type' => 'radio_inline',
'name' => esc_html__( 'Item Titles', 'rank-math' ),
'desc' => esc_html__( 'Show the post/term titles, or the SEO titles in the HTML sitemap.', 'rank-math' ),
'options' => [
'titles' => esc_html__( 'Item Titles', 'rank-math' ),
'seo_titles' => esc_html__( 'SEO Titles', 'rank-math' ),
],
'dep' => [ [ 'html_sitemap', 'on' ] ],
'default' => 'titles',
]
);

View File

@@ -0,0 +1 @@
<?php // Silence is golden.

View File

@@ -0,0 +1,67 @@
<?php
/**
* Sitemap settings - post type tabs.
*
* @package RankMath
* @subpackage RankMath\Sitemap
*/
use RankMath\Helper;
defined( 'ABSPATH' ) || exit;
$attributes = [];
$post_type = $tab['post_type'];
$prefix = "pt_{$post_type}_";
if ( 'attachment' === $post_type && Helper::get_settings( 'general.attachment_redirect_urls', true ) ) {
$cmb->add_field(
[
'id' => 'attachment_redirect_urls_notice',
'type' => 'notice',
'what' => 'warning',
/* translators: The settings page link */
'content' => sprintf( __( 'To configure meta tags for your media attachment pages, you need to first %s to parent.', 'rank-math' ), '<a href="' . esc_url( Helper::get_admin_url( 'options-general#setting-panel-links' ) ) . '">' . esc_html__( 'disable redirect attachments', 'rank-math' ) . '</a>' ),
]
);
$attributes['disabled'] = 'disabled';
}
$cmb->add_field(
[
'id' => $prefix . 'sitemap',
'type' => 'toggle',
'name' => esc_html__( 'Include in Sitemap', 'rank-math' ),
'desc' => esc_html__( 'Include this post type in the XML sitemap.', 'rank-math' ),
'default' => 'attachment' === $post_type ? 'off' : 'on',
'attributes' => $attributes,
]
);
$cmb->add_field(
[
'id' => $prefix . 'html_sitemap',
'type' => 'toggle',
'name' => esc_html__( 'Include in HTML Sitemap', 'rank-math' ),
'desc' => esc_html__( 'Include this post type in the HTML sitemap if it\'s enabled.', 'rank-math' ),
'default' => 'attachment' === $post_type ? 'off' : 'on',
'attributes' => $attributes,
'classes' => [
'rank-math-html-sitemap',
! Helper::get_settings( 'sitemap.html_sitemap' ) ? 'hidden' : '',
],
]
);
if ( 'attachment' !== $post_type ) {
$cmb->add_field(
[
'id' => $prefix . 'image_customfields',
'type' => 'textarea_small',
'name' => esc_html__( 'Image Custom Fields', 'rank-math' ),
'desc' => esc_html__( 'Insert custom field (post meta) names which contain image URLs to include them in the sitemaps. Add one per line.', 'rank-math' ),
'dep' => [ [ $prefix . 'sitemap', 'on' ] ],
'classes' => 'rank-math-advanced-option',
]
);
}

View File

@@ -0,0 +1,51 @@
<?php
/**
* Sitemap settings - taxonomy tabs.
*
* @package RankMath
* @subpackage RankMath\Sitemap
*/
use RankMath\Helper;
defined( 'ABSPATH' ) || exit;
$taxonomy = $tab['taxonomy'];
$prefix = "tax_{$taxonomy}_";
$is_enabled = 'category' === $taxonomy ? 'on' : 'off';
$cmb->add_field(
[
'id' => $prefix . 'sitemap',
'type' => 'toggle',
'name' => esc_html__( 'Include in Sitemap', 'rank-math' ),
'desc' => esc_html__( 'Include archive pages for terms of this taxonomy in the XML sitemap.', 'rank-math' ),
'default' => $is_enabled,
]
);
$cmb->add_field(
[
'id' => $prefix . 'html_sitemap',
'type' => 'toggle',
'name' => esc_html__( 'Include in HTML Sitemap', 'rank-math' ),
'desc' => esc_html__( 'Include archive pages for terms of this taxonomy in the HTML sitemap.', 'rank-math' ),
'default' => $is_enabled,
'classes' => [
'rank-math-html-sitemap',
! Helper::get_settings( 'sitemap.html_sitemap' ) ? 'hidden' : '',
],
]
);
$cmb->add_field(
[
'id' => $prefix . 'include_empty',
'type' => 'toggle',
'name' => esc_html__( 'Include Empty Terms', 'rank-math' ),
'desc' => esc_html__( 'Include archive pages of terms that have no posts associated.', 'rank-math' ),
'default' => 'off',
'dep' => [ [ $prefix . 'sitemap', 'on' ] ],
'classes' => 'rank-math-advanced-option',
]
);

View File

@@ -0,0 +1,349 @@
<?php
/**
* The XSL stylesheet output.
*
* @package RankMath
* @subpackage RankMath\Sitemap
*/
use RankMath\KB;
use RankMath\Helper;
use RankMath\Sitemap\Router;
use RankMath\Sitemap\Sitemap;
defined( 'ABSPATH' ) || exit;
// Echo so opening tag doesn't get confused for PHP.
echo '<?xml version="1.0" encoding="UTF-8"?>';
?>
<xsl:stylesheet version="2.0"
xmlns:html="http://www.w3.org/TR/REC-html40"
xmlns:image="http://www.google.com/schemas/sitemap-image/1.1"
xmlns:sitemap="http://www.sitemaps.org/schemas/sitemap/0.9"
xmlns:kml="http://www.opengis.net/kml/2.2"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:atom="http://www.w3.org/2005/Atom">
<xsl:output method="html" version="1.0" encoding="UTF-8" indent="yes"/>
<xsl:template match="/">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<xsl:choose>
<xsl:when test="kml:kml">
<title><?php echo esc_html( $kml_title ); ?></title>
</xsl:when>
<xsl:otherwise>
<title><?php echo esc_html( $title ); ?></title>
</xsl:otherwise>
</xsl:choose>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<style type="text/css">
body {
font-size: 14px;
font-family: -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Oxygen-Sans,Ubuntu,Cantarell,"Helvetica Neue",sans-serif;
margin: 0;
color: #545353;
}
a {
color: #05809e;
text-decoration: none;
}
h1 {
font-size: 24px;
font-family: Verdana,Geneva,sans-serif;
font-weight: normal;
margin: 0;
}
#description {
background-color: #4275f4;
padding: 20px 40px;
color: #fff;
padding: 30px 30px 20px;
}
#description h1,
#description p,
#description a {
color: #fff;
margin: 0;
font-size: 1.1em;
}
#description h1 {
font-size: 2em;
margin-bottom: 1em;
}
#description p {
margin-top: 5px;
}
#description a {
border-bottom: 1px dotted;
}
#content {
padding: 20px 30px;
background: #fff;
max-width: 75%;
margin: 0 auto;
}
table {
border: none;
border-collapse: collapse;
font-size: .9em;
width: 100%;
}
th {
background-color: #4275f4;
color: #fff;
text-align: left;
padding: 15px 10px;
font-size: 14px;
cursor: pointer;
}
td {
padding: 10px;
border-bottom: 1px solid #ddd;
}
tbody tr:nth-child(even) {
background-color: #f7f7f7;
}
table td a {
display: block;
}
table td a img {
max-height: 30px;
margin: 6px 3px;
}
</style>
</head>
<body>
<xsl:choose>
<xsl:when test="kml:kml">
<div id="description">
<h1><?php esc_html_e( 'KML File', 'rank-math' ); ?></h1>
<?php if ( false === $this->do_filter( 'sitemap/remove_credit', false ) ) : ?>
<p>
<?php
printf(
wp_kses_post(
/* translators: link to rankmath.com */
__( 'This KML File is generated by <a href="%s" target="_blank">Rank Math WordPress SEO Plugin</a>. It is used to provide location information to Google.', 'rank-math' )
),
KB::get( 'seo-suite' )
);
?>
</p>
<?php endif; ?>
<p>
<?php
printf(
wp_kses_post(
/* translators: link to rankmath.com */
__( 'Learn more about <a href="%s" target="_blank">KML File</a>.', 'rank-math' )
),
'https://developers.google.com/kml/documentation/'
);
?>
</p>
</div>
<div id="content">
<p class="expl">
This KML file contains <xsl:value-of select="count(kml:kml/kml:Document/kml:Folder/kml:Placemark)"/> Locations.
</p>
<p class="expl">
<?php
printf(
/* translators: xsl value count */
wp_kses_post( __( '<a href="%s">&#8592; Sitemap Index</a>', 'rank-math' ) ),
esc_url( Router::get_base_url( Sitemap::get_sitemap_index_slug() . '.xml' ) )
);
?>
</p>
<table id="sitemap" cellpadding="3">
<thead>
<tr>
<th width="25%"><?php esc_html_e( 'Name', 'rank-math' ); ?></th>
<th width="40%"><?php esc_html_e( 'Address', 'rank-math' ); ?></th>
<th width="15%"><?php esc_html_e( 'Phone number', 'rank-math' ); ?></th>
<th width="10%"><?php esc_html_e( 'Latitude', 'rank-math' ); ?></th>
<th width="10%"><?php esc_html_e( 'Longitude', 'rank-math' ); ?></th>
</tr>
</thead>
<tbody>
<xsl:variable name="lower" select="'abcdefghijklmnopqrstuvwxyz'"/>
<xsl:variable name="upper" select="'ABCDEFGHIJKLMNOPQRSTUVWXYZ'"/>
<xsl:for-each select="kml:kml/kml:Document/kml:Folder/kml:Placemark">
<tr>
<td>
<xsl:variable name="itemURL">
<xsl:value-of select="atom:link/@href"/>
</xsl:variable>
<a href="{$itemURL}">
<xsl:value-of select="kml:name"/>
</a>
</td>
<td>
<xsl:value-of select="kml:address"/>
</td>
<td>
<xsl:value-of select="kml:phoneNumber"/>
</td>
<td>
<xsl:value-of select="kml:LookAt/kml:latitude"/>
</td>
<td>
<xsl:value-of select="kml:LookAt/kml:longitude"/>
</td>
</tr>
</xsl:for-each>
</tbody>
</table>
</div>
</xsl:when>
<xsl:otherwise>
<div id="description">
<h1><?php esc_html_e( 'XML Sitemap', 'rank-math' ); ?></h1>
<?php if ( false === $this->do_filter( 'sitemap/remove_credit', false ) ) : ?>
<p>
<?php
printf(
wp_kses_post(
/* translators: link to rankmath.com */
__( 'This XML Sitemap is generated by <a href="%s" target="_blank">Rank Math WordPress SEO Plugin</a>. It is what search engines like Google use to crawl and re-crawl posts/pages/products/images/archives on your website.', 'rank-math' )
),
KB::get( 'seo-suite' )
);
?>
</p>
<?php endif; ?>
<p>
<?php
printf(
wp_kses_post(
/* translators: link to rankmath.com */
__( 'Learn more about <a href="%s" target="_blank">XML Sitemaps</a>.', 'rank-math' )
),
'http://sitemaps.org'
);
?>
</p>
</div>
<div id="content">
<xsl:if test="count(sitemap:sitemapindex/sitemap:sitemap) &gt; 0">
<p>
<?php
printf(
/* translators: xsl value count */
wp_kses_post( __( 'This XML Sitemap Index file contains <strong>%s</strong> sitemaps.', 'rank-math' ) ),
'<xsl:value-of select="count(sitemap:sitemapindex/sitemap:sitemap)"/>'
);
?>
</p>
<table id="sitemap" cellpadding="3">
<thead>
<tr>
<th width="75%"><?php esc_html_e( 'Sitemap', 'rank-math' ); ?></th>
<th width="25%"><?php esc_html_e( 'Last Modified', 'rank-math' ); ?></th>
</tr>
</thead>
<tbody>
<xsl:for-each select="sitemap:sitemapindex/sitemap:sitemap">
<xsl:variable name="sitemapURL">
<xsl:value-of select="sitemap:loc"/>
</xsl:variable>
<tr>
<td>
<a href="{$sitemapURL}"><xsl:value-of select="sitemap:loc"/></a>
</td>
<td>
<xsl:value-of select="concat(substring(sitemap:lastmod,0,11),concat(' ', substring(sitemap:lastmod,12,5)),concat(' ', substring(sitemap:lastmod,20,6)))"/>
</td>
</tr>
</xsl:for-each>
</tbody>
</table>
</xsl:if>
<xsl:if test="count(sitemap:sitemapindex/sitemap:sitemap) &lt; 1">
<p>
<?php
printf(
/* translators: xsl value count */
wp_kses_post( __( 'This XML Sitemap contains <strong>%s</strong> URLs.', 'rank-math' ) ),
'<xsl:value-of select="count(sitemap:urlset/sitemap:url)"/>'
);
?>
</p>
<p class="expl">
<?php
printf(
/* translators: xsl value count */
wp_kses_post( __( '<a href="%s">&#8592; Sitemap Index</a>', 'rank-math' ) ),
esc_url( Router::get_base_url( Sitemap::get_sitemap_index_slug() . '.xml' ) )
);
?>
</p>
<table id="sitemap" cellpadding="3">
<thead>
<tr>
<th width="75%"><?php esc_html_e( 'URL', 'rank-math' ); ?></th>
<?php if ( Helper::get_settings( 'sitemap.include_images' ) ) : // phpcs:ignore ?><th width="5%"><?php esc_html_e( 'Images', 'rank-math' ); ?></th><?php endif; ?>
<th title="Last Modification Time" width="20%"><?php esc_html_e( 'Last Mod.', 'rank-math' ); ?></th>
</tr>
</thead>
<tbody>
<xsl:variable name="lower" select="'abcdefghijklmnopqrstuvwxyz'"/>
<xsl:variable name="upper" select="'ABCDEFGHIJKLMNOPQRSTUVWXYZ'"/>
<xsl:for-each select="sitemap:urlset/sitemap:url">
<tr>
<td>
<xsl:variable name="itemURL">
<xsl:value-of select="sitemap:loc"/>
</xsl:variable>
<a href="{$itemURL}">
<xsl:value-of select="sitemap:loc"/>
</a>
</td>
<?php if ( Helper::get_settings( 'sitemap.include_images' ) ) : ?>
<td>
<xsl:value-of select="count(image:image)"/>
</td>
<?php endif; ?>
<td>
<xsl:value-of select="concat(substring(sitemap:lastmod,0,11),concat(' ', substring(sitemap:lastmod,12,5)),concat(' ', substring(sitemap:lastmod,20,6)))"/>
</td>
</tr>
</xsl:for-each>
</tbody>
</table>
</xsl:if>
</div>
</xsl:otherwise>
</xsl:choose>
</body>
</html>
</xsl:template>
</xsl:stylesheet>