From 33e8ced5d90dd9efa182ae9893b5ac89bcd1f910 Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Wed, 3 Jun 2020 10:31:08 +0200 Subject: [PATCH 01/18] First pass at integrating sitemaps --- src/wp-includes/default-constants.php | 16 + src/wp-includes/default-filters.php | 3 + src/wp-includes/formatting.php | 81 ++++ src/wp-includes/l10n.php | 53 +++ src/wp-includes/sitemaps.php | 119 ++++++ .../sitemaps/class-wp-sitemaps-index.php | 85 ++++ .../sitemaps/class-wp-sitemaps-provider.php | 176 ++++++++ .../sitemaps/class-wp-sitemaps-registry.php | 91 ++++ .../sitemaps/class-wp-sitemaps-renderer.php | 251 +++++++++++ .../sitemaps/class-wp-sitemaps-stylesheet.php | 268 ++++++++++++ .../sitemaps/class-wp-sitemaps.php | 252 +++++++++++ .../providers/class-wp-sitemaps-posts.php | 211 ++++++++++ .../class-wp-sitemaps-taxonomies.php | 193 +++++++++ .../providers/class-wp-sitemaps-users.php | 163 +++++++ src/wp-settings.php | 13 + tests/phpunit/includes/bootstrap.php | 3 + .../class-wp-sitemaps-empty-test-provider.php | 48 +++ .../class-wp-sitemaps-test-provider.php | 52 +++ tests/phpunit/includes/normalize-xml.xsl | 76 ++++ tests/phpunit/includes/testcase-xml.php | 88 ++++ tests/phpunit/tests/formatting/EscXml.php | 135 ++++++ tests/phpunit/tests/sitemaps/functions.php | 61 +++ .../phpunit/tests/sitemaps/sitemaps-index.php | 51 +++ .../phpunit/tests/sitemaps/sitemaps-posts.php | 19 + .../tests/sitemaps/sitemaps-registry.php | 44 ++ .../tests/sitemaps/sitemaps-renderer.php | 206 +++++++++ .../tests/sitemaps/sitemaps-stylesheet.php | 42 ++ .../tests/sitemaps/sitemaps-taxonomies.php | 192 +++++++++ .../phpunit/tests/sitemaps/sitemaps-users.php | 57 +++ tests/phpunit/tests/sitemaps/sitemaps.php | 396 ++++++++++++++++++ 30 files changed, 3445 insertions(+) create mode 100644 src/wp-includes/sitemaps.php create mode 100644 src/wp-includes/sitemaps/class-wp-sitemaps-index.php create mode 100644 src/wp-includes/sitemaps/class-wp-sitemaps-provider.php create mode 100644 src/wp-includes/sitemaps/class-wp-sitemaps-registry.php create mode 100644 src/wp-includes/sitemaps/class-wp-sitemaps-renderer.php create mode 100644 src/wp-includes/sitemaps/class-wp-sitemaps-stylesheet.php create mode 100644 src/wp-includes/sitemaps/class-wp-sitemaps.php create mode 100644 src/wp-includes/sitemaps/providers/class-wp-sitemaps-posts.php create mode 100644 src/wp-includes/sitemaps/providers/class-wp-sitemaps-taxonomies.php create mode 100644 src/wp-includes/sitemaps/providers/class-wp-sitemaps-users.php create mode 100644 tests/phpunit/includes/class-wp-sitemaps-empty-test-provider.php create mode 100644 tests/phpunit/includes/class-wp-sitemaps-test-provider.php create mode 100644 tests/phpunit/includes/normalize-xml.xsl create mode 100644 tests/phpunit/includes/testcase-xml.php create mode 100644 tests/phpunit/tests/formatting/EscXml.php create mode 100644 tests/phpunit/tests/sitemaps/functions.php create mode 100644 tests/phpunit/tests/sitemaps/sitemaps-index.php create mode 100644 tests/phpunit/tests/sitemaps/sitemaps-posts.php create mode 100644 tests/phpunit/tests/sitemaps/sitemaps-registry.php create mode 100644 tests/phpunit/tests/sitemaps/sitemaps-renderer.php create mode 100644 tests/phpunit/tests/sitemaps/sitemaps-stylesheet.php create mode 100644 tests/phpunit/tests/sitemaps/sitemaps-taxonomies.php create mode 100644 tests/phpunit/tests/sitemaps/sitemaps-users.php create mode 100644 tests/phpunit/tests/sitemaps/sitemaps.php diff --git a/src/wp-includes/default-constants.php b/src/wp-includes/default-constants.php index 98c8e48369f92..2798a350cd13b 100644 --- a/src/wp-includes/default-constants.php +++ b/src/wp-includes/default-constants.php @@ -408,3 +408,19 @@ function wp_templating_constants() { } } + +/** + * Defines sitemaps-related WordPress constants. + * + * @since 5.5.0 + */ +function wp_sitemaps_constants() { + /** + * Maximum number of URLs included in a sitemap. + * + * @sine 5.5.0 + */ + if ( ! defined( 'WP_SITEMAPS_MAX_URLS' ) ) { + define( 'WP_SITEMAPS_MAX_URLS', 2000 ); + } +} diff --git a/src/wp-includes/default-filters.php b/src/wp-includes/default-filters.php index 43faaf6507bdf..c9024e7ae026a 100644 --- a/src/wp-includes/default-filters.php +++ b/src/wp-includes/default-filters.php @@ -456,6 +456,9 @@ add_action( 'rest_api_init', 'create_initial_rest_routes', 99 ); add_action( 'parse_request', 'rest_api_loaded' ); +// Sitemaps actions. +add_action( 'init', 'wp_sitemaps_get_server' ); + /** * Filters formerly mixed into wp-includes. */ diff --git a/src/wp-includes/formatting.php b/src/wp-includes/formatting.php index a5c52b0e6c03d..185170abd86fd 100644 --- a/src/wp-includes/formatting.php +++ b/src/wp-includes/formatting.php @@ -4536,6 +4536,87 @@ function esc_textarea( $text ) { return apply_filters( 'esc_textarea', $safe_text, $text ); } +/** + * Escaping for XML blocks. + * + * @since 5.5.0 + * + * @param string $text Text to escape. + * @return string + */ +function esc_xml( $text ) { + $safe_text = wp_check_invalid_utf8( $text ); + + $cdata_regex = '\<\!\[CDATA\[.*?\]\]\>'; + $regex = <<(.*?)) # the "anything" matched by the lookahead + (?({$cdata_regex})) # the CDATA Section matched by the lookahead + +| # alternative + + (?(.*)) # non-CDATA Section +/sx +EOF; + $safe_text = (string) preg_replace_callback( + $regex, + function( $matches ) { + if ( ! $matches[0] ) { + return ''; + } elseif ( ! empty( $matches['non_cdata'] ) ) { + // escape HTML entities in the non-CDATA Section. + return _esc_xml_non_cdata_section( $matches['non_cdata'] ); + } + + // Return the CDATA Section unchanged, escape HTML entities in the rest. + return _esc_xml_non_cdata_section( $matches['non_cdata_followed_by_cdata'] ) . $matches['cdata']; + }, + $safe_text + ); + + /** + * Filters a string cleaned and escaped for output in XML. + * + * Text passed to esc_xml() is stripped of invalid or special characters + * before output. HTML named character references are converted to their + * equivalent code points. + * + * @since 5.5.0 + * + * @param string $safe_text The text after it has been escaped. + * @param string $text The text prior to being escaped. + */ + return apply_filters( 'esc_xml', $safe_text, $text ); +} + +/** + * Escaping for non-CDATA Section XML blocks. + * + * @access private + * @since 5.5.0 + * + * @param string $text Text to escape. + * @return string + */ +function _esc_xml_non_cdata_section( $text ) { + global $allowedentitynames; + + $safe_text = _wp_specialchars( $text, ENT_QUOTES ); + // Replace HTML entities with their Unicode codepoints, + // without doing the same for the 5 XML entities. + $html_only_entities = array_diff( $allowedentitynames, array( 'amp', 'lt', 'gt', 'apos', 'quot' ) ); + $safe_text = (string) preg_replace_callback( + '/&(' . implode( '|', $html_only_entities ) . ');/', + function( $matches ) { + return html_entity_decode( $matches[0], ENT_HTML5 ); + }, + $safe_text + ); + + return $safe_text; +} + /** * Escape an HTML tag name. * diff --git a/src/wp-includes/l10n.php b/src/wp-includes/l10n.php index d7e6acd55b1ca..fe7f05921c270 100644 --- a/src/wp-includes/l10n.php +++ b/src/wp-includes/l10n.php @@ -336,6 +336,59 @@ function esc_html_e( $text, $domain = 'default' ) { echo esc_html( translate( $text, $domain ) ); } +/** + * Retrieve the translation of $text and escapes it for safe use in XML output. + * + * If there is no translation, or the text domain isn't loaded, the original text + * is escaped and returned. + * + * @since 5.5.0 + * + * @param string $text Text to translate. + * @param string $domain Optional. Text domain. Unique identifier for retrieving translated strings. + * Default 'default'. + * @return string Translated text. + */ +function esc_xml__( $text, $domain = 'default' ) { + return esc_xml( translate( $text, $domain ) ); +} + +/** + * Display translated text that has been escaped for safe use in XML output. + * + * If there is no translation, or the text domain isn't loaded, the original text + * is escaped and displayed. + * + * If you need the value for use in PHP, use esc_xml__(). + * + * @since 5.5.0 + * + * @param string $text Text to translate. + * @param string $domain Optional. Text domain. Unique identifier for retrieving translated strings. + * Default 'default'. + */ +function esc_xml_e( $text, $domain = 'default' ) { + echo esc_xml( translate( $text, $domain ) ); +} + +/** + * Translate string with gettext context, and escapes it for safe use in XML output. + * + * If there is no translation, or the text domain isn't loaded, the original text + * is escaped and returned. + * + * @since 5.5.0 + * + * @param string $text Text to translate. + * @param string $context Context information for the translators. + * @param string $domain Optional. Text domain. Unique identifier for retrieving translated strings. + * Default 'default'. + * @return string Translated text. + */ +function esc_xml_x( $text, $context, $domain = 'default' ) { + return esc_xml( translate_with_gettext_context( $text, $context, $domain ) ); +} + /** * Retrieve translated string with gettext context. * diff --git a/src/wp-includes/sitemaps.php b/src/wp-includes/sitemaps.php new file mode 100644 index 0000000000000..4464276fdd244 --- /dev/null +++ b/src/wp-includes/sitemaps.php @@ -0,0 +1,119 @@ +init(); + + /** + * Fires when initializing the Sitemaps object. + * + * Additional sitemaps should be registered on this hook. + * + * @since 5.5.0 + * + * @param WP_Sitemaps $sitemaps Server object. + */ + do_action( 'wp_sitemaps_init', $wp_sitemaps ); + } + + return $wp_sitemaps; +} + +/** + * Gets a list of sitemap providers. + * + * @since 5.5.0 + * + * @return array $sitemaps A list of registered sitemap providers. + */ +function wp_get_sitemaps() { + $sitemaps = wp_sitemaps_get_server(); + + if ( ! $sitemaps ) { + return array(); + } + + return $sitemaps->registry->get_sitemaps(); +} + +/** + * Registers a new sitemap provider. + * + * @since 5.5.0 + * + * @param string $name Unique name for the sitemap provider. + * @param WP_Sitemaps_Provider $provider The `Sitemaps_Provider` instance implementing the sitemap. + * @return bool Returns true if the sitemap was added. False on failure. + */ +function wp_register_sitemap( $name, $provider ) { + $sitemaps = wp_sitemaps_get_server(); + + if ( ! $sitemaps ) { + return false; + } + + return $sitemaps->registry->add_sitemap( $name, $provider ); +} + +/** + * Gets the maximum number of URLs for a sitemap. + * + * @since 5.5.0 + * + * @param string $object_type Object type for sitemap to be filtered (e.g. 'post', 'term', 'user'). + * @return int The maximum number of URLs. + */ +function wp_sitemaps_get_max_urls( $object_type ) { + /** + * Filters the maximum number of URLs displayed on a sitemap. + * + * @since 5.5.0 + * + * @param int $max_urls The maximum number of URLs included in a sitemap. Default 2000. + * @param string $object_type Object type for sitemap to be filtered (e.g. 'post', 'term', 'user'). + */ + return apply_filters( 'wp_sitemaps_max_urls', WP_SITEMAPS_MAX_URLS, $object_type ); +} diff --git a/src/wp-includes/sitemaps/class-wp-sitemaps-index.php b/src/wp-includes/sitemaps/class-wp-sitemaps-index.php new file mode 100644 index 0000000000000..98c2135e7768a --- /dev/null +++ b/src/wp-includes/sitemaps/class-wp-sitemaps-index.php @@ -0,0 +1,85 @@ +registry = $registry; + } + + /** + * Gets a sitemap list for the index. + * + * @since 5.5.0 + * + * @return array List of all sitemaps. + */ + public function get_sitemap_list() { + $sitemaps = array(); + + $providers = $this->registry->get_sitemaps(); + /* @var WP_Sitemaps_Provider $provider */ + foreach ( $providers as $provider ) { + $sitemap_entries = $provider->get_sitemap_entries(); + + // Prevent issues with array_push and empty arrays on PHP < 7.3. + if ( ! $sitemap_entries ) { + continue; + } + + // Using array_push is more efficient than array_merge in a loop. + array_push( $sitemaps, ...$sitemap_entries ); + } + + return $sitemaps; + } + + /** + * Builds the URL for the sitemap index. + * + * @since 5.5.0 + * + * @return string The sitemap index url. + */ + public function get_index_url() { + /* @var WP_Rewrite $wp_rewrite */ + global $wp_rewrite; + + $url = home_url( '/wp-sitemap.xml' ); + + if ( ! $wp_rewrite->using_permalinks() ) { + $url = add_query_arg( 'sitemap', 'index', home_url( '/' ) ); + } + + return $url; + } +} diff --git a/src/wp-includes/sitemaps/class-wp-sitemaps-provider.php b/src/wp-includes/sitemaps/class-wp-sitemaps-provider.php new file mode 100644 index 0000000000000..dba1724c5baf6 --- /dev/null +++ b/src/wp-includes/sitemaps/class-wp-sitemaps-provider.php @@ -0,0 +1,176 @@ +get_object_subtypes(); + + // If there are no object subtypes, include a single sitemap for the + // entire object type. + if ( empty( $object_subtypes ) ) { + $sitemap_data[] = array( + 'name' => '', + 'pages' => $this->get_max_num_pages(), + ); + return $sitemap_data; + } + + // Otherwise, include individual sitemaps for every object subtype. + foreach ( $object_subtypes as $object_subtype_name => $data ) { + $object_subtype_name = (string) $object_subtype_name; + + $sitemap_data[] = array( + 'name' => $object_subtype_name, + 'pages' => $this->get_max_num_pages( $object_subtype_name ), + ); + } + + return $sitemap_data; + } + + /** + * Lists sitemap pages exposed by this provider. + * + * The returned data is used to populate the sitemap entries of the index. + * + * @since 5.5.0 + * + * @return array List of sitemaps. + */ + public function get_sitemap_entries() { + $sitemaps = array(); + + $sitemap_types = $this->get_sitemap_type_data(); + + foreach ( $sitemap_types as $type ) { + for ( $page = 1; $page <= $type['pages']; $page ++ ) { + $loc = $this->get_sitemap_url( $type['name'], $page ); + $sitemaps[] = array( + 'loc' => $loc, + ); + } + } + + return $sitemaps; + } + + /** + * Gets the URL of a sitemap entry. + * + * @since 5.5.0 + * + * @param string $name The name of the sitemap. + * @param int $page The page of the sitemap. + * @return string The composed URL for a sitemap entry. + */ + public function get_sitemap_url( $name, $page ) { + /* @var WP_Rewrite $wp_rewrite */ + global $wp_rewrite; + + if ( ! $wp_rewrite->using_permalinks() ) { + return add_query_arg( + // Accounts for cases where name is not included, ex: sitemaps-users-1.xml. + array_filter( + array( + 'sitemap' => $this->name, + 'sitemap-sub-type' => $name, + 'paged' => $page, + ) + ), + home_url( '/' ) + ); + } + + $basename = sprintf( + '/wp-sitemap-%1$s.xml', + implode( + '-', + // Accounts for cases where name is not included, ex: sitemaps-users-1.xml. + array_filter( + array( + $this->name, + $name, + (string) $page, + ) + ) + ) + ); + + return home_url( $basename ); + } + + /** + * Returns the list of supported object subtypes exposed by the provider. + * + * @since 5.5.0 + * + * @return array List of object subtypes objects keyed by their name. + */ + public function get_object_subtypes() { + return array(); + } +} diff --git a/src/wp-includes/sitemaps/class-wp-sitemaps-registry.php b/src/wp-includes/sitemaps/class-wp-sitemaps-registry.php new file mode 100644 index 0000000000000..8870e40adb5ad --- /dev/null +++ b/src/wp-includes/sitemaps/class-wp-sitemaps-registry.php @@ -0,0 +1,91 @@ +sitemaps[ $name ] ) ) { + return false; + } + + if ( ! $provider instanceof WP_Sitemaps_Provider ) { + return false; + } + + $this->sitemaps[ $name ] = $provider; + + return true; + } + + /** + * Returns a single registered sitemaps provider. + * + * @since 5.5.0 + * + * @param string $name Sitemap provider name. + * @return WP_Sitemaps_Provider|null Sitemaps provider if it exists, null otherwise. + */ + public function get_sitemap( $name ) { + if ( ! isset( $this->sitemaps[ $name ] ) ) { + return null; + } + + return $this->sitemaps[ $name ]; + } + + /** + * Lists all registered sitemaps. + * + * @since 5.5.0 + * + * @return array List of sitemaps. + */ + public function get_sitemaps() { + $total_sitemaps = count( $this->sitemaps ); + + if ( $total_sitemaps > $this->max_sitemaps ) { + return array_slice( $this->sitemaps, 0, $this->max_sitemaps, true ); + } + + return $this->sitemaps; + } +} diff --git a/src/wp-includes/sitemaps/class-wp-sitemaps-renderer.php b/src/wp-includes/sitemaps/class-wp-sitemaps-renderer.php new file mode 100644 index 0000000000000..59e2025473ddb --- /dev/null +++ b/src/wp-includes/sitemaps/class-wp-sitemaps-renderer.php @@ -0,0 +1,251 @@ +get_sitemap_stylesheet_url(); + if ( $stylesheet_url ) { + $this->stylesheet = ''; + } + $stylesheet_index_url = $this->get_sitemap_index_stylesheet_url(); + if ( $stylesheet_index_url ) { + $this->stylesheet_index = ''; + } + } + + /** + * Gets the URL for the sitemap stylesheet. + * + * @since 5.5.0 + * + * @return string The sitemap stylesheet url. + */ + public function get_sitemap_stylesheet_url() { + /* @var WP_Rewrite $wp_rewrite */ + global $wp_rewrite; + + $sitemap_url = home_url( '/wp-sitemap.xsl' ); + + if ( ! $wp_rewrite->using_permalinks() ) { + $sitemap_url = add_query_arg( 'sitemap-stylesheet', 'sitemap', home_url( '/' ) ); + } + + /** + * Filters the URL for the sitemap stylesheet. + * + * If a falsy value is returned, no stylesheet will be used and + * the "raw" XML of the sitemap will be displayed. + * + * @since 5.5.0 + * + * @param string $sitemap_url Full URL for the sitemaps xsl file. + */ + return apply_filters( 'wp_sitemaps_stylesheet_url', $sitemap_url ); + } + + /** + * Gets the URL for the sitemap index stylesheet. + * + * @since 5.5.0 + * + * @return string The sitemap index stylesheet url. + */ + public function get_sitemap_index_stylesheet_url() { + /* @var WP_Rewrite $wp_rewrite */ + global $wp_rewrite; + + $sitemap_url = home_url( '/wp-sitemap-index.xsl' ); + + if ( ! $wp_rewrite->using_permalinks() ) { + $sitemap_url = add_query_arg( 'sitemap-stylesheet', 'index', home_url( '/' ) ); + } + + /** + * Filters the URL for the sitemap index stylesheet. + * + * If a falsy value is returned, no stylesheet will be used and + * the "raw" XML of the sitemap index will be displayed. + * + * @since 5.5.0 + * + * @param string $sitemap_url Full URL for the sitemaps index xsl file. + */ + return apply_filters( 'wp_sitemaps_stylesheet_index_url', $sitemap_url ); + } + + /** + * Renders a sitemap index. + * + * @since 5.5.0 + * + * @param array $sitemaps Array of sitemap URLs. + */ + public function render_index( $sitemaps ) { + header( 'Content-type: application/xml; charset=UTF-8' ); + + $this->check_for_simple_xml_availability(); + + $index_xml = $this->get_sitemap_index_xml( $sitemaps ); + + if ( ! empty( $index_xml ) ) { + // All output is escaped within get_sitemap_index_xml(). + // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + echo $index_xml; + } + } + + /** + * Gets XML for a sitemap index. + * + * @since 5.5.0 + * + * @param array $sitemaps Array of sitemap URLs. + * @return string|false A well-formed XML string for a sitemap index. False on error. + */ + public function get_sitemap_index_xml( $sitemaps ) { + $sitemap_index = new SimpleXMLElement( + sprintf( + '%1$s%2$s%3$s', + '', + $this->stylesheet_index, + '' + ) + ); + + foreach ( $sitemaps as $entry ) { + $sitemap = $sitemap_index->addChild( 'sitemap' ); + $sitemap->addChild( 'loc', esc_url( $entry['loc'] ) ); + } + + return $sitemap_index->asXML(); + } + + /** + * Renders a sitemap. + * + * @since 5.5.0 + * + * @param array $url_list Array of URLs for a sitemap. + */ + public function render_sitemap( $url_list ) { + header( 'Content-type: application/xml; charset=UTF-8' ); + + $this->check_for_simple_xml_availability(); + + $sitemap_xml = $this->get_sitemap_xml( $url_list ); + + if ( ! empty( $sitemap_xml ) ) { + // All output is escaped within get_sitemap_xml(). + // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + echo $sitemap_xml; + } + } + + /** + * Gets XML for a sitemap. + * + * @since 5.5.0 + * + * @param array $url_list Array of URLs for a sitemap. + * @return string|false A well-formed XML string for a sitemap index. False on error. + */ + public function get_sitemap_xml( $url_list ) { + $urlset = new SimpleXMLElement( + sprintf( + '%1$s%2$s%3$s', + '', + $this->stylesheet, + '' + ) + ); + + foreach ( $url_list as $url_item ) { + $url = $urlset->addChild( 'url' ); + + // Add each element as a child node to the URL entry. + foreach ( $url_item as $name => $value ) { + if ( 'loc' === $name ) { + $url->addChild( $name, esc_url( $value ) ); + } elseif ( in_array( $name, array( 'lastmod', 'changefreq', 'priority' ), true ) ) { + $url->addChild( $name, esc_attr( $value ) ); + } else { + _doing_it_wrong( + __METHOD__, + /* translators: %s: list of element names */ + sprintf( + __( 'Fields other than %s are not currently supported for sitemaps.' ), + implode( ',', array( 'loc', 'lastmod', 'changefreq', 'priority' ) ) + ), + '5.5.0' + ); + } + } + } + + return $urlset->asXML(); + } + + /** + * Checks for the availability of the SimpleXML extension and errors if missing. + * + * @since 5.5.0 + */ + private function check_for_simple_xml_availability() { + if ( ! class_exists( 'SimpleXMLElement' ) ) { + add_filter( + 'wp_die_handler', + static function () { + return '_xml_wp_die_handler'; + } + ); + + wp_die( + sprintf( + /* translators: %s: SimpleXML */ + __( 'Could not generate XML sitemap due to missing %s extension' ), + 'SimpleXML' + ), + __( 'WordPress › Error' ), + array( + 'response' => 501, // "Not implemented". + ) + ); + } + } +} diff --git a/src/wp-includes/sitemaps/class-wp-sitemaps-stylesheet.php b/src/wp-includes/sitemaps/class-wp-sitemaps-stylesheet.php new file mode 100644 index 0000000000000..191d0bba0d5db --- /dev/null +++ b/src/wp-includes/sitemaps/class-wp-sitemaps-stylesheet.php @@ -0,0 +1,268 @@ +get_sitemap_stylesheet(); + } + + if ( 'index' === $type ) { + // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- All content escaped below. + echo $this->get_sitemap_index_stylesheet(); + } + + exit; + } + + /** + * Returns the escaped xsl for all sitemaps, except index. + * + * @since 5.5.0 + */ + public function get_sitemap_stylesheet() { + $css = $this->get_stylesheet_css(); + $title = esc_html__( 'XML Sitemap' ); + $description = sprintf( + /* translators: %s: URL to sitemaps documentation. */ + __( 'This XML Sitemap is generated by WordPress to make your content more visible for search engines. Learn more about XML sitemaps on sitemaps.org.' ), + __( 'https://www.sitemaps.org/' ) + ); + $text = sprintf( + /* translators: %s: number of URLs. */ + __( 'Number of URLs in this XML Sitemap: %s.' ), + '' + ); + + $lang = get_language_attributes( 'html' ); + $url = esc_html__( 'URL' ); + $lastmod = esc_html__( 'Last Modified' ); + $changefreq = esc_html__( 'Change Frequency' ); + $priority = esc_html__( 'Priority' ); + + $xsl_content = << + + + + + + + + + + + + + {$title} + + + +
+

{$title}

+

{$description}

+
+
+

{$text}

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
{$url}{$lastmod}{$changefreq}{$priority}
+
+ + +
+
+ +XSL; + + /** + * Filters the content of the sitemap stylesheet. + * + * @since 5.5.0 + * + * @param string $xsl Full content for the xml stylesheet. + */ + return apply_filters( 'wp_sitemaps_stylesheet_content', $xsl_content ); + } + + /** + * Returns the escaped xsl for the index sitemaps. + * + * @since 5.5.0 + */ + public function get_sitemap_index_stylesheet() { + $css = $this->get_stylesheet_css(); + $title = esc_html__( 'XML Sitemap' ); + $description = sprintf( + /* translators: %s: URL to sitemaps documentation. */ + __( 'This XML Sitemap is generated by WordPress to make your content more visible for search engines. Learn more about XML sitemaps on sitemaps.org.' ), + __( 'https://www.sitemaps.org/' ) + ); + $text = sprintf( + /* translators: %s: number of URLs. */ + __( 'Number of URLs in this XML Sitemap: %s.' ), + '' + ); + $lang = get_language_attributes( 'html' ); + $url = esc_html__( 'URL' ); + + $xsl_content = << + + + + + + + + {$title} + + + +
+

{$title}

+

{$description}

+
+
+

{$text}

+ + + + + + + + + + + + + +
{$url}
+
+ + +
+
+ +XSL; + + /** + * Filters the content of the sitemap index stylesheet. + * + * @since 5.5.0 + * + * @param string $xsl Full content for the xml stylesheet. + */ + return apply_filters( 'wp_sitemaps_index_stylesheet_content', $xsl_content ); + } + + /** + * Gets the CSS to be included in sitemap XSL stylesheets. + * + * @since 5.5.0 + * + * @return string The CSS. + */ + public function get_stylesheet_css() { + $css = ' + body { + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif; + color: #444; + } + + #sitemap__table { + border: solid 1px #ccc; + border-collapse: collapse; + } + + #sitemap__table tr th { + text-align: left; + } + + #sitemap__table tr td, + #sitemap__table tr th { + padding: 10px; + } + + #sitemap__table tr:nth-child(odd) td { + background-color: #eee; + } + + a:hover { + text-decoration: none; + }'; + + /** + * Filters the css only for the sitemap stylesheet. + * + * @since 5.5.0 + * + * @param string $css CSS to be applied to default xsl file. + */ + return apply_filters( 'wp_sitemaps_stylesheet_css', $css ); + } +} diff --git a/src/wp-includes/sitemaps/class-wp-sitemaps.php b/src/wp-includes/sitemaps/class-wp-sitemaps.php new file mode 100644 index 0000000000000..7e6ce343d742e --- /dev/null +++ b/src/wp-includes/sitemaps/class-wp-sitemaps.php @@ -0,0 +1,252 @@ +registry = new WP_Sitemaps_Registry(); + $this->renderer = new WP_Sitemaps_Renderer(); + $this->index = new WP_Sitemaps_Index( $this->registry ); + } + + /** + * Initiates all sitemap functionality. + * + * @since 5.5.0 + */ + public function init() { + // These will all fire on the init hook. + $this->register_sitemaps(); + + // Add additional action callbacks. + add_action( 'wp_sitemaps_init', array( $this, 'register_rewrites' ) ); + add_action( 'template_redirect', array( $this, 'render_sitemaps' ) ); + add_filter( 'pre_handle_404', array( $this, 'redirect_sitemapxml' ), 10, 2 ); + add_filter( 'robots_txt', array( $this, 'add_robots' ), 0, 2 ); + add_filter( 'redirect_canonical', array( $this, 'redirect_canonical' ) ); + } + + /** + * Registers and sets up the functionality for all supported sitemaps. + * + * @since 5.5.0 + */ + public function register_sitemaps() { + /** + * Filters the list of registered sitemap providers. + * + * @since 5.5.0 + * + * @param array $providers { + * Array of WP_Sitemap_Provider objects keyed by their name. + * + * @type object $posts The WP_Sitemaps_Posts object. + * @type object $taxonomies The WP_Sitemaps_Taxonomies object. + * @type object $users The WP_Sitemaps_Users object. + * } + */ + $providers = apply_filters( + 'wp_sitemaps_register_providers', + array( + 'posts' => new WP_Sitemaps_Posts(), + 'taxonomies' => new WP_Sitemaps_Taxonomies(), + 'users' => new WP_Sitemaps_Users(), + ) + ); + + // Register each supported provider. + /* @var WP_Sitemaps_Provider $provider */ + foreach ( $providers as $name => $provider ) { + $this->registry->add_sitemap( $name, $provider ); + } + } + + /** + * Registers sitemap rewrite tags and routing rules. + * + * @since 5.5.0 + */ + public function register_rewrites() { + // Add rewrite tags. + add_rewrite_tag( '%sitemap%', '([^?]+)' ); + add_rewrite_tag( '%sitemap-sub-type%', '([^?]+)' ); + + // Register index route. + add_rewrite_rule( '^wp-sitemap\.xml$', 'index.php?sitemap=index', 'top' ); + + // Register rewrites for the XSL stylesheet. + add_rewrite_tag( '%sitemap-stylesheet%', '([^?]+)' ); + add_rewrite_rule( '^wp-sitemap\.xsl$', 'index.php?sitemap-stylesheet=sitemap', 'top' ); + add_rewrite_rule( '^wp-sitemap-index\.xsl$', 'index.php?sitemap-stylesheet=index', 'top' ); + + // Register routes for providers. + add_rewrite_rule( + '^wp-sitemap-([a-z]+?)-([a-z\d_-]+?)-(\d+?)\.xml$', + 'index.php?sitemap=$matches[1]&sitemap-sub-type=$matches[2]&paged=$matches[3]', + 'top' + ); + add_rewrite_rule( + '^wp-sitemap-([a-z]+?)-(\d+?)\.xml$', + 'index.php?sitemap=$matches[1]&paged=$matches[2]', + 'top' + ); + } + + /** + * Renders sitemap templates based on rewrite rules. + * + * @since 5.5.0 + */ + public function render_sitemaps() { + global $wp_query; + + $sitemap = sanitize_text_field( get_query_var( 'sitemap' ) ); + $object_subtype = sanitize_text_field( get_query_var( 'sitemap-sub-type' ) ); + $stylesheet_type = sanitize_text_field( get_query_var( 'sitemap-stylesheet' ) ); + $paged = absint( get_query_var( 'paged' ) ); + + // Bail early if this isn't a sitemap or stylesheet route. + if ( ! ( $sitemap || $stylesheet_type ) ) { + return; + } + + // Render stylesheet if this is stylesheet route. + if ( $stylesheet_type ) { + $stylesheet = new WP_Sitemaps_Stylesheet(); + + $stylesheet->render_stylesheet( $stylesheet_type ); + exit; + } + + // Render the index. + if ( 'index' === $sitemap ) { + $sitemap_list = $this->index->get_sitemap_list(); + + $this->renderer->render_index( $sitemap_list ); + exit; + } + + $provider = $this->registry->get_sitemap( $sitemap ); + + if ( ! $provider ) { + return; + } + + if ( empty( $paged ) ) { + $paged = 1; + } + + $url_list = $provider->get_url_list( $paged, $object_subtype ); + + // Force a 404 and bail early if no URLs are present. + if ( empty( $url_list ) ) { + $wp_query->set_404(); + return; + } + + $this->renderer->render_sitemap( $url_list ); + exit; + } + + /** + * Redirects a URL to the wp-sitemap.xml + * + * @since 5.5.0 + * + * @param bool $bypass Pass-through of the pre_handle_404 filter value. + * @param WP_Query $query The WP_Query object. + * @return bool Bypass value. + */ + public function redirect_sitemapxml( $bypass, $query ) { + // If a plugin has already utilized the pre_handle_404 function, return without action to avoid conflicts. + if ( $bypass ) { + return $bypass; + } + + // 'pagename' is for most permalink types, name is for when the %postname% is used as a top-level field. + if ( 'sitemap-xml' === $query->get( 'pagename' ) || + 'sitemap-xml' === $query->get( 'name' ) ) { + wp_safe_redirect( $this->index->get_index_url() ); + exit(); + } + + return $bypass; + } + + /** + * Adds the sitemap index to robots.txt. + * + * @since 5.5.0 + * + * @param string $output robots.txt output. + * @param bool $public Whether the site is public or not. + * @return string The robots.txt output. + */ + public function add_robots( $output, $public ) { + if ( $public ) { + $output .= "\nSitemap: " . esc_url( $this->index->get_index_url() ) . "\n"; + } + + return $output; + } + + /** + * Prevent trailing slashes. + * + * @since 5.5.0 + * + * @param string $redirect The redirect URL currently determined. + * @return bool|string $redirect The canonical redirect URL. + */ + public function redirect_canonical( $redirect ) { + if ( get_query_var( 'sitemap' ) || get_query_var( 'sitemap-stylesheet' ) ) { + return false; + } + + return $redirect; + } +} diff --git a/src/wp-includes/sitemaps/providers/class-wp-sitemaps-posts.php b/src/wp-includes/sitemaps/providers/class-wp-sitemaps-posts.php new file mode 100644 index 0000000000000..b8095c4ab3c9c --- /dev/null +++ b/src/wp-includes/sitemaps/providers/class-wp-sitemaps-posts.php @@ -0,0 +1,211 @@ +name = 'posts'; + $this->object_type = 'post'; + } + + /** + * Returns the public post types, which excludes nav_items and similar types. + * Attachments are also excluded. This includes custom post types with public = true. + * + * @since 5.5.0 + * + * @return array Map of registered post type objects (WP_Post_Type) keyed by their name. + */ + public function get_object_subtypes() { + $post_types = get_post_types( array( 'public' => true ), 'objects' ); + unset( $post_types['attachment'] ); + + /** + * Filters the list of post object sub types available within the sitemap. + * + * @since 5.5.0 + * + * @param array $post_types Map of registered post type objects (WP_Post_Type) keyed by their name. + */ + return apply_filters( 'wp_sitemaps_post_types', $post_types ); + } + + /** + * Gets a URL list for a post type sitemap. + * + * @since 5.5.0 + * + * @param int $page_num Page of results. + * @param string $post_type Optional. Post type name. Default empty. + * @return array $url_list Array of URLs for a sitemap. + */ + public function get_url_list( $page_num, $post_type = '' ) { + // Bail early if the queried post type is not supported. + $supported_types = $this->get_object_subtypes(); + + if ( ! isset( $supported_types[ $post_type ] ) ) { + return array(); + } + + /** + * Filters the posts URL list before it is generated. + * + * Passing a non-null value will effectively short-circuit the generation, + * returning that value instead. + * + * @since 5.5.0 + * + * @param array $url_list The URL list. Default null. + * @param string $post_type Post type name. + * @param int $page_num Page of results. + */ + $url_list = apply_filters( + 'wp_sitemaps_posts_pre_url_list', + null, + $post_type, + $page_num + ); + + if ( null !== $url_list ) { + return $url_list; + } + + $args = $this->get_posts_query_args( $post_type ); + $args['paged'] = $page_num; + + $query = new WP_Query( $args ); + + /** + * Returns an array of posts. + * + * @var array $posts + */ + $posts = $query->get_posts(); + + $url_list = array(); + + /* + * Add a URL for the homepage in the pages sitemap. + * Shows only on the first page if the reading settings are set to display latest posts. + */ + if ( 'page' === $post_type && 1 === $page_num && 'posts' === get_option( 'show_on_front' ) ) { + // Extract the data needed for home URL to add to the array. + $url_list[] = array( + 'loc' => home_url(), + ); + } + + foreach ( $posts as $post ) { + $sitemap_entry = array( + 'loc' => get_permalink( $post ), + ); + + /** + * Filters the sitemap entry for an individual post. + * + * @since 5.5.0 + * + * @param array $sitemap_entry Sitemap entry for the post. + * @param WP_Post $post Post object. + * @param string $post_type Name of the post_type. + */ + $sitemap_entry = apply_filters( 'wp_sitemaps_posts_entry', $sitemap_entry, $post, $post_type ); + $url_list[] = $sitemap_entry; + } + + return $url_list; + } + + /** + * Gets the max number of pages available for the object type. + * + * @since 5.5.0 + * + * @param string $post_type Optional. Post type name. Default empty. + * @return int Total number of pages. + */ + public function get_max_num_pages( $post_type = '' ) { + if ( empty( $post_type ) ) { + return 0; + } + + /** + * Filters the max number of pages before it is generated. + * + * Passing a non-null value will effectively short-circuit the generation, + * returning that value instead. + * + * @since 5.5.0 + * + * @param int $max_num_pages The maximum number of pages. Default null. + * @param string $post_type Post type name. + */ + $max_num_pages = apply_filters( 'wp_sitemaps_posts_pre_max_num_pages', null, $post_type ); + + if ( null !== $max_num_pages ) { + return $max_num_pages; + } + + $args = $this->get_posts_query_args( $post_type ); + $args['fields'] = 'ids'; + $args['no_found_rows'] = false; + + $query = new WP_Query( $args ); + + return isset( $query->max_num_pages ) ? $query->max_num_pages : 1; + } + + /** + * Returns the query args for retrieving posts to list in the sitemap. + * + * @since 5.5.0 + * + * @param string $post_type Post type name. + * @return array $args Array of WP_Query arguments. + */ + protected function get_posts_query_args( $post_type ) { + /** + * Filters the query arguments for post type sitemap queries. + * + * @see WP_Query for a full list of arguments. + * + * @since 5.5.0 + * + * @param array $args Array of WP_Query arguments. + * @param string $post_type Post type name. + */ + $args = apply_filters( + 'wp_sitemaps_posts_query_args', + array( + 'orderby' => 'ID', + 'order' => 'ASC', + 'post_type' => $post_type, + 'posts_per_page' => wp_sitemaps_get_max_urls( $this->object_type ), + 'post_status' => array( 'publish' ), + 'no_found_rows' => true, + 'update_post_term_cache' => false, + 'update_post_meta_cache' => false, + ), + $post_type + ); + + return $args; + } +} diff --git a/src/wp-includes/sitemaps/providers/class-wp-sitemaps-taxonomies.php b/src/wp-includes/sitemaps/providers/class-wp-sitemaps-taxonomies.php new file mode 100644 index 0000000000000..ca49b9f433fcf --- /dev/null +++ b/src/wp-includes/sitemaps/providers/class-wp-sitemaps-taxonomies.php @@ -0,0 +1,193 @@ +name = 'taxonomies'; + $this->object_type = 'term'; + } + + /** + * Returns all public, registered taxonomies. + * + * @since 5.5.0 + * + * @return array Map of registered taxonomy objects keyed by their name. + */ + public function get_object_subtypes() { + $taxonomies = get_taxonomies( array( 'public' => true ), 'objects' ); + + /** + * Filter the list of taxonomy object subtypes available within the sitemap. + * + * @since 5.5.0 + * + * @param array $taxonomies Map of registered taxonomy objects keyed by their name. + */ + return apply_filters( 'wp_sitemaps_taxonomies', $taxonomies ); + } + + /** + * Gets a URL list for a taxonomy sitemap. + * + * @since 5.5.0 + * + * @param int $page_num Page of results. + * @param string $taxonomy Optional. Taxonomy name. Default empty. + * @return array $url_list Array of URLs for a sitemap. + */ + public function get_url_list( $page_num, $taxonomy = '' ) { + $supported_types = $this->get_object_subtypes(); + + // Bail early if the queried taxonomy is not supported. + if ( ! isset( $supported_types[ $taxonomy ] ) ) { + return array(); + } + + /** + * Filters the taxonomies URL list before it is generated. + * + * Passing a non-null value will effectively short-circuit the generation, + * returning that value instead. + * + * @since 5.5.0 + * + * @param array $url_list The URL list. Default null. + * @param string $taxonomy Taxonomy name. + * @param int $page_num Page of results. + */ + $url_list = apply_filters( + 'wp_sitemaps_taxonomies_pre_url_list', + null, + $taxonomy, + $page_num + ); + + if ( null !== $url_list ) { + return $url_list; + } + + $url_list = array(); + + // Offset by how many terms should be included in previous pages. + $offset = ( $page_num - 1 ) * wp_sitemaps_get_max_urls( $this->object_type ); + + $args = $this->get_taxonomies_query_args( $taxonomy ); + $args['offset'] = $offset; + + $taxonomy_terms = new WP_Term_Query( $args ); + + if ( ! empty( $taxonomy_terms->terms ) ) { + foreach ( $taxonomy_terms->terms as $term ) { + $sitemap_entry = array( + 'loc' => get_term_link( $term ), + ); + + /** + * Filters the sitemap entry for an individual term. + * + * @since 5.5.0 + * + * @param array $sitemap_entry Sitemap entry for the term. + * @param WP_Term $term Term object. + * @param string $taxonomy Taxonomy name. + */ + $sitemap_entry = apply_filters( 'wp_sitemaps_taxonomies_entry', $sitemap_entry, $term, $taxonomy ); + $url_list[] = $sitemap_entry; + } + } + + return $url_list; + } + + /** + * Gets the max number of pages available for the object type. + * + * @since 5.5.0 + * + * @param string $taxonomy Taxonomy name. + * @return int Total number of pages. + */ + public function get_max_num_pages( $taxonomy = '' ) { + if ( empty( $taxonomy ) ) { + return 0; + } + + /** + * Filters the max number of pages before it is generated. + * + * Passing a non-null value will effectively short-circuit the generation, + * returning that value instead. + * + * @since 5.5.0 + * + * @param int $max_num_pages The maximum number of pages. Default null. + * @param string $taxonomy Taxonomy name. + */ + $max_num_pages = apply_filters( 'wp_sitemaps_taxonomies_pre_max_num_pages', null, $taxonomy ); + + if ( null !== $max_num_pages ) { + return $max_num_pages; + } + + $term_count = wp_count_terms( $taxonomy, $this->get_taxonomies_query_args( $taxonomy ) ); + + return (int) ceil( $term_count / wp_sitemaps_get_max_urls( $this->object_type ) ); + } + + /** + * Returns the query args for retrieving taxonomy terms to list in the sitemap. + * + * @since 5.5.0 + * + * @param string $taxonomy Taxonomy name. + * @return array $args Array of WP_Term_Query arguments. + */ + protected function get_taxonomies_query_args( $taxonomy ) { + /** + * Filters the taxonomy terms query arguments. + * + * Allows modification of the taxonomy query arguments before querying. + * + * @see WP_Term_Query for a full list of arguments + * + * @since 5.5.0 + * + * @param array $args Array of WP_Term_Query arguments. + * @param string $taxonomy Taxonomy name. + */ + $args = apply_filters( + 'wp_sitemaps_taxonomies_query_args', + array( + 'fields' => 'ids', + 'taxonomy' => $taxonomy, + 'orderby' => 'term_order', + 'number' => wp_sitemaps_get_max_urls( $this->object_type ), + 'hide_empty' => true, + 'hierarchical' => false, + 'update_term_meta_cache' => false, + ), + $taxonomy + ); + + return $args; + } +} diff --git a/src/wp-includes/sitemaps/providers/class-wp-sitemaps-users.php b/src/wp-includes/sitemaps/providers/class-wp-sitemaps-users.php new file mode 100644 index 0000000000000..7c5195b4672b6 --- /dev/null +++ b/src/wp-includes/sitemaps/providers/class-wp-sitemaps-users.php @@ -0,0 +1,163 @@ +name = 'users'; + $this->object_type = 'user'; + } + + /** + * Gets a URL list for a user sitemap. + * + * @since 5.5.0 + * + * @param int $page_num Page of results. + * @param string $object_subtype Optional. Not applicable for Users but + * required for compatibility with the parent + * provider class. Default empty. + * @return array $url_list Array of URLs for a sitemap. + */ + public function get_url_list( $page_num, $object_subtype = '' ) { + /** + * Filters the users URL list before it is generated. + * + * Passing a non-null value will effectively short-circuit the generation, + * returning that value instead. + * + * @since 5.5.0 + * + * @param array $url_list The URL list. Default null. + * @param int $page_num Page of results. + */ + $url_list = apply_filters( + 'wp_sitemaps_users_pre_url_list', + null, + $page_num + ); + + if ( null !== $url_list ) { + return $url_list; + } + + $args = $this->get_users_query_args(); + $args['paged'] = $page_num; + + $query = new WP_User_Query( $args ); + $users = $query->get_results(); + $url_list = array(); + + foreach ( $users as $user ) { + $sitemap_entry = array( + 'loc' => get_author_posts_url( $user->ID ), + ); + + /** + * Filters the sitemap entry for an individual user. + * + * @since 5.5.0 + * + * @param array $sitemap_entry Sitemap entry for the user. + * @param WP_User $user User object. + */ + $sitemap_entry = apply_filters( 'wp_sitemaps_users_entry', $sitemap_entry, $user ); + $url_list[] = $sitemap_entry; + } + + return $url_list; + } + + /** + * Gets the max number of pages available for the object type. + * + * @since 5.5.0 + * + * @see WP_Sitemaps_Provider::max_num_pages + * + * @param string $object_subtype Optional. Not applicable for Users but + * required for compatibility with the parent + * provider class. Default empty. + * @return int Total page count. + */ + public function get_max_num_pages( $object_subtype = '' ) { + /** + * Filters the max number of pages before it is generated. + * + * Passing a non-null value will effectively short-circuit the generation, + * returning that value instead. + * + * @since 5.5.0 + * + * @param int $max_num_pages The maximum number of pages. Default null. + */ + $max_num_pages = apply_filters( 'wp_sitemaps_users_pre_max_num_pages', null ); + + if ( null !== $max_num_pages ) { + return $max_num_pages; + } + + $args = $this->get_users_query_args(); + $query = new WP_User_Query( $args ); + + $total_users = $query->get_total(); + + return (int) ceil( $total_users / wp_sitemaps_get_max_urls( $this->object_type ) ); + } + + /** + * Returns the query args for retrieving users to list in the sitemap. + * + * @since 5.5.0 + * + * @return array $args Array of WP_User_Query arguments. + */ + protected function get_users_query_args() { + $public_post_types = get_post_types( + array( + 'public' => true, + ) + ); + + // We're not supporting sitemaps for author pages for attachments. + unset( $public_post_types['attachment'] ); + + /** + * Filters the query arguments for authors with public posts. + * + * Allows modification of the authors query arguments before querying. + * + * @see WP_User_Query for a full list of arguments + * + * @since 5.5.0 + * + * @param array $args Array of WP_User_Query arguments. + */ + $args = apply_filters( + 'wp_sitemaps_users_query_args', + array( + 'has_published_posts' => array_keys( $public_post_types ), + 'number' => wp_sitemaps_get_max_urls( $this->object_type ), + ) + ); + + return $args; + } +} diff --git a/src/wp-settings.php b/src/wp-settings.php index 1a5f296733719..13e20903e86d7 100644 --- a/src/wp-settings.php +++ b/src/wp-settings.php @@ -263,6 +263,16 @@ require ABSPATH . WPINC . '/rest-api/fields/class-wp-rest-user-meta-fields.php'; require ABSPATH . WPINC . '/rest-api/search/class-wp-rest-search-handler.php'; require ABSPATH . WPINC . '/rest-api/search/class-wp-rest-post-search-handler.php'; +require ABSPATH . WPINC . '/sitemaps.php'; +require ABSPATH . WPINC . '/sitemaps/class-wp-sitemaps.php'; +require ABSPATH . WPINC . '/sitemaps/class-wp-sitemaps-index.php'; +require ABSPATH . WPINC . '/sitemaps/class-wp-sitemaps-provider.php'; +require ABSPATH . WPINC . '/sitemaps/class-wp-sitemaps-registry.php'; +require ABSPATH . WPINC . '/sitemaps/class-wp-sitemaps-renderer.php'; +require ABSPATH . WPINC . '/sitemaps/class-wp-sitemaps-stylesheet.php'; +require ABSPATH . WPINC . '/sitemaps/providers/class-wp-sitemaps-posts.php'; +require ABSPATH . WPINC . '/sitemaps/providers/class-wp-sitemaps-taxonomies.php'; +require ABSPATH . WPINC . '/sitemaps/providers/class-wp-sitemaps-users.php'; require ABSPATH . WPINC . '/class-wp-block-type.php'; require ABSPATH . WPINC . '/class-wp-block-styles-registry.php'; require ABSPATH . WPINC . '/class-wp-block-type-registry.php'; @@ -403,6 +413,9 @@ // Define constants which affect functionality if not already defined. wp_functionality_constants(); +// Define sitemaps-related cconstants. +wp_sitemaps_constants(); + // Add magic quotes and set up $_REQUEST ( $_GET + $_POST ). wp_magic_quotes(); diff --git a/tests/phpunit/includes/bootstrap.php b/tests/phpunit/includes/bootstrap.php index d628d62c09877..817aea3bbe34c 100644 --- a/tests/phpunit/includes/bootstrap.php +++ b/tests/phpunit/includes/bootstrap.php @@ -153,12 +153,15 @@ function wp_tests_options( $value ) { require __DIR__ . '/testcase-xmlrpc.php'; require __DIR__ . '/testcase-ajax.php'; require __DIR__ . '/testcase-canonical.php'; +require __DIR__ . '/testcase-xml.php'; require __DIR__ . '/exceptions.php'; require __DIR__ . '/utils.php'; require __DIR__ . '/spy-rest-server.php'; require __DIR__ . '/class-wp-rest-test-search-handler.php'; require __DIR__ . '/class-wp-rest-test-configurable-controller.php'; require __DIR__ . '/class-wp-fake-block-type.php'; +require __DIR__ . '/class-wp-sitemaps-test-provider.php'; +require __DIR__ . '/class-wp-sitemaps-empty-test-provider.php'; /** * A class to handle additional command line arguments passed to the script. diff --git a/tests/phpunit/includes/class-wp-sitemaps-empty-test-provider.php b/tests/phpunit/includes/class-wp-sitemaps-empty-test-provider.php new file mode 100644 index 0000000000000..247eaefa88340 --- /dev/null +++ b/tests/phpunit/includes/class-wp-sitemaps-empty-test-provider.php @@ -0,0 +1,48 @@ +object_type = $object_type; + } + + /** + * Return the public post types, which excludes nav_items and similar types. + * Attachments are also excluded. This includes custom post types with public = true + * + * @return array Map of object subtype objects (WP_Post_Type) keyed by their name. + */ + public function get_object_subtypes() { + return array(); + } + + /** + * Gets a URL list for a sitemap. + * + * @param int $page_num Page of results. + * @param string $object_subtype Optional. Object subtype name. Default empty. + * @return array List of URLs for a sitemap. + */ + public function get_url_list( $page_num, $object_subtype = '' ) { + return array(); + } + + /** + * Query for determining the number of pages. + * + * @param string $object_subtype Optional. Object subtype. Default empty. + * @return int Total number of pages. + */ + public function get_max_num_pages( $object_subtype = '' ) { + return 0; + } +} diff --git a/tests/phpunit/includes/class-wp-sitemaps-test-provider.php b/tests/phpunit/includes/class-wp-sitemaps-test-provider.php new file mode 100644 index 0000000000000..389bb6c7bf3d5 --- /dev/null +++ b/tests/phpunit/includes/class-wp-sitemaps-test-provider.php @@ -0,0 +1,52 @@ +object_type = $object_type; + } + + /** + * Return the public post types, which excludes nav_items and similar types. + * Attachments are also excluded. This includes custom post types with public = true + * + * @return array Map of object subtype objects (WP_Post_Type) keyed by their name. + */ + public function get_object_subtypes() { + return array( + 'type-1' => (object) array( 'name' => 'type-1' ), + 'type-2' => (object) array( 'name' => 'type-2' ), + 'type-3' => (object) array( 'name' => 'type-3' ), + ); + } + + /** + * Gets a URL list for a sitemap. + * + * @param int $page_num Page of results. + * @param string $object_subtype Optional. Object subtype name. Default empty. + * @return array List of URLs for a sitemap. + */ + public function get_url_list( $page_num, $object_subtype = '' ) { + return array(); + } + + /** + * Query for determining the number of pages. + * + * @param string $object_subtype Optional. Object subtype. Default empty. + * @return int Total number of pages. + */ + public function get_max_num_pages( $object_subtype = '' ) { + return 4; + } +} diff --git a/tests/phpunit/includes/normalize-xml.xsl b/tests/phpunit/includes/normalize-xml.xsl new file mode 100644 index 0000000000000..135556c61b3cb --- /dev/null +++ b/tests/phpunit/includes/normalize-xml.xsl @@ -0,0 +1,76 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/phpunit/includes/testcase-xml.php b/tests/phpunit/includes/testcase-xml.php new file mode 100644 index 0000000000000..6f64783bff470 --- /dev/null +++ b/tests/phpunit/includes/testcase-xml.php @@ -0,0 +1,88 @@ +loadXML( $xml, $options ); + $libxml_last_error = libxml_get_last_error(); + + $this->assertFalse( + isset( $libxml_last_error->message ), + isset( $libxml_last_error->message ) ? sprintf( 'Non-well-formed XML: %s.', $libxml_last_error->message ) : '' + ); + + // Restore default error handler. + libxml_use_internal_errors( $internal ); + libxml_clear_errors(); + + return $xml_dom; + } + + /** + * Normalize an XML document to make comparing two documents easier. + * + * @param string $xml + * @param int $options Bitwise OR of the {@link https://www.php.net/manual/en/libxml.constants.php libxml option constants}. + * Default is 0. + * @return string The normalized form of `$xml`. + */ + public function normalizeXML( $xml, $options = 0 ) { + static $xslt_proc; + + if ( ! $xslt_proc ) { + $xslt_proc = new XSLTProcessor(); + $xslt_proc->importStyleSheet( simplexml_load_file( __DIR__ . '/normalize-xml.xsl' ) ); + } + + return $xslt_proc->transformToXML( $this->loadXML( $xml, $options ) ); + } + + /** + * Reports an error identified by `$message` if the namespace normalized form of the XML document in `$actualXml` + * is equal to the namespace normalized form of the XML document in `$expectedXml`. + * + * This is similar to {@link https://phpunit.de/manual/6.5/en/appendixes.assertions.html#appendixes.assertions.assertXmlStringEqualsXmlString assertXmlStringEqualsXmlString()} + * except that differences in namespace prefixes are normalized away, such that given + * `$actualXml = "";` and + * `$expectedXml = "";` + * then `$this->assertXMLEquals( $expectedXml, $actualXml )` will succeed. + * + * @param string $expectedXml + * @param string $actualXml + * @param string $message Optional. Message to display when the assertion fails. + */ + public function assertXMLEquals( $expectedXml, $actualXml, $message = '' ) { // phpcs:ignore WordPress.NamingConventions.ValidVariableName.VariableNotSnakeCase + $this->assertEquals( $this->normalizeXML( $expectedXml ), $this->normalizeXML( $actualXml ), $message ); //phpcs:ignore WordPress.NamingConventions.ValidVariableName.VariableNotSnakeCase + } + + /** + * Reports an error identified by `$message` if the namespace normalized form of the XML document in `$actualXml` + * is not equal to the namespace normalized form of the XML document in `$expectedXml`. + * + * This is similar to {@link https://phpunit.de/manual/6.5/en/appendixes.assertions.html#appendixes.assertions.assertXmlStringEqualsXmlString assertXmlStringNotEqualsXmlString()} + * except that differences in namespace prefixes are normalized away, such that given + * `$actualXml = "";` and + * `$expectedXml = "";` + * then `$this->assertXMLNotEquals( $expectedXml, $actualXml )` will fail. + * + * @param string $expectedXml + * @param string $actualXml + * @param string $message Optional. Message to display when the assertion fails. + */ + public function assertXMLNotEquals( $expectedXml, $actualXml, $message = '' ) { //phpcs:ignore WordPress.NamingConventions.ValidVariableName.VariableNotSnakeCase + $this->assertNotEquals( $this->normalizeXML( $expectedXml ), $this->normalizeXML( $actualXml ), $message ); //phpcs:ignore WordPress.NamingConventions.ValidVariableName.VariableNotSnakeCase + } +} diff --git a/tests/phpunit/tests/formatting/EscXml.php b/tests/phpunit/tests/formatting/EscXml.php new file mode 100644 index 0000000000000..daa56ab0b289e --- /dev/null +++ b/tests/phpunit/tests/formatting/EscXml.php @@ -0,0 +1,135 @@ +assertEquals( $expected, $actual ); + } + + /** + * Data provider for `test_esc_xml_basics()`. + * + * @return array { + * @type string $source The source string to be escaped. + * @type string $expected The expected escaped value of `$source`. + * } + */ + public function _test_esc_xml_basics_dataprovider() { + return array( + // Simple string. + array( + 'The quick brown fox.', + 'The quick brown fox.', + ), + // URL with &. + array( + 'http://localhost/trunk/wp-login.php?action=logout&_wpnonce=cd57d75985', + 'http://localhost/trunk/wp-login.php?action=logout&_wpnonce=cd57d75985', + ), + // SQL query w/ single quotes. + array( + "SELECT meta_key, meta_value FROM wp_trunk_sitemeta WHERE meta_key IN ('site_name', 'siteurl', 'active_sitewide_plugins', '_site_transient_timeout_theme_roots', '_site_transient_theme_roots', 'site_admins', 'can_compress_scripts', 'global_terms_enabled') AND site_id = 1", + 'SELECT meta_key, meta_value FROM wp_trunk_sitemeta WHERE meta_key IN ('site_name', 'siteurl', 'active_sitewide_plugins', '_site_transient_timeout_theme_roots', '_site_transient_theme_roots', 'site_admins', 'can_compress_scripts', 'global_terms_enabled') AND site_id = 1', + ), + ); + } + + public function test_escapes_ampersands() { + $source = 'penn & teller & at&t'; + $expected = 'penn & teller & at&t'; + $actual = esc_xml( $source ); + $this->assertEquals( $expected, $actual ); + } + + public function test_escapes_greater_and_less_than() { + $source = 'this > that < that '; + $expected = 'this > that < that <randomhtml />'; + $actual = esc_xml( $source ); + $this->assertEquals( $expected, $actual ); + } + + public function test_escapes_html_named_entities() { + $source = 'this & is a … followed by › and more and a &nonexistent; entity'; + $expected = 'this & is a … followed by › and more and a &nonexistent; entity'; + $actual = esc_xml( $source ); + $this->assertEquals( $expected, $actual ); + } + + public function test_ignores_existing_entities() { + $source = '& £ " &'; + // note that _wp_specialchars() strips leading 0's from numeric character references. + $expected = '& £ " &'; + $actual = esc_xml( $source ); + $this->assertEquals( $expected, $actual ); + } + + /** + * Test that CDATA Sections are not escaped. + * + * @dataProvider _test_ignores_cdata_sections_dataprovider + * + * @param string $source The source string to be escaped. + * @param string $expected The expected escaped value of `$source`. + */ + public function test_ignores_cdata_sections( $source, $expected ) { + $actual = esc_xml( $source ); + $this->assertEquals( $expected, $actual ); + } + + /** + * Data provider for `test_ignores_cdata_sections()`. + * + * @return array { + * @type string $source The source string to be escaped. + * @type string $expected The expected escaped value of `$source`. + * } + */ + public function _test_ignores_cdata_sections_dataprovider() { + return array( + // basic CDATA Section containing chars that would otherwise be escaped if not in a CDATA Section + // not to mention the CDATA Section markup itself :-) + // $source contains embedded newlines to test that the regex that ignores CDATA Sections + // correctly handles that case. + array( + "This is\na]]>\nbroadcast system", + "This is\na]]>\nbroadcast system", + ), + // string with chars that should be escaped as well as a CDATA Section that should be not be. + array( + 'This is … a ]]> broadcast ', + 'This is … a ]]> broadcast <system />', + ), + // Same as above, but with the CDATA Section at the start of the string. + array( + ']]> This is … a broadcast ', + ']]> This is … a broadcast <system />', + ), + // Same as above, but with the CDATA Section at the end of the string. + array( + 'This is … a broadcast ]]>', + 'This is … a broadcast <system />]]>', + ), + // Multiple CDATA Sections. + array( + 'This is … a ]]> &broadcast; ]]>', + 'This is … a ]]> &broadcast; ]]>', + ), + // Ensure that ']]>' that does not mark the end of a CDATA Section is escaped. + array( + ']]>', + ']]>', + ), + ); + } +} diff --git a/tests/phpunit/tests/sitemaps/functions.php b/tests/phpunit/tests/sitemaps/functions.php new file mode 100644 index 0000000000000..a0195f7e0f763 --- /dev/null +++ b/tests/phpunit/tests/sitemaps/functions.php @@ -0,0 +1,61 @@ +assertEquals( $expected_posts, 300, 'Can not confirm max URL number for posts.' ); + $this->assertEquals( $expected_taxonomies, 50, 'Can not confirm max URL number for taxonomies.' ); + $this->assertEquals( $expected_users, 1, 'Can not confirm max URL number for users.' ); + } + + /** + * Callback function for testing the `sitemaps_max_urls` filter. + * + * @param int $max_urls The maximum number of URLs included in a sitemap. Default 2000. + * @param string $type Optional. The type of sitemap to be filtered. Default empty. + * @return int The maximum number of URLs. + */ + public function _filter_max_url_value( $max_urls, $type ) { + switch ( $type ) { + case 'post': + return 300; + case 'term': + return 50; + case 'user': + return 1; + default: + return $max_urls; + } + } + + /** + * Test wp_get_sitemaps default functionality + */ + public function test_wp_get_sitemaps() { + $sitemaps = wp_get_sitemaps(); + + $expected = array( + 'posts' => 'WP_Sitemaps_Posts', + 'taxonomies' => 'WP_Sitemaps_Taxonomies', + 'users' => 'WP_Sitemaps_Users', + ); + + $this->assertEquals( array_keys( $expected ), array_keys( $sitemaps ), 'Unable to confirm default sitemap types are registered.' ); + + foreach ( $expected as $name => $provider ) { + $this->assertTrue( is_a( $sitemaps[ $name ], $provider ), "Default $name sitemap is not a $provider object." ); + } + } +} diff --git a/tests/phpunit/tests/sitemaps/sitemaps-index.php b/tests/phpunit/tests/sitemaps/sitemaps-index.php new file mode 100644 index 0000000000000..89f64078e3735 --- /dev/null +++ b/tests/phpunit/tests/sitemaps/sitemaps-index.php @@ -0,0 +1,51 @@ +add_sitemap( 'foo', new WP_Sitemaps_Test_Provider( 'foo' ) ); + $registry->add_sitemap( 'bar', new WP_Sitemaps_Test_Provider( 'bar' ) ); + + $sitemap_index = new WP_Sitemaps_Index( $registry ); + $this->assertCount( 24, $sitemap_index->get_sitemap_list() ); + } + + public function test_get_sitemap_list_no_entries() { + $registry = new WP_Sitemaps_Registry(); + + $registry->add_sitemap( 'foo', new Core_WP_Empty_Test_Provider( 'foo' ) ); + + $sitemap_index = new WP_Sitemaps_Index( $registry ); + $this->assertCount( 0, $sitemap_index->get_sitemap_list() ); + } + + public function test_get_index_url() { + $sitemap_index = new WP_Sitemaps_Index( new WP_Sitemaps_Registry() ); + $index_url = $sitemap_index->get_index_url(); + + $this->assertStringEndsWith( '/?sitemap=index', $index_url ); + } + + public function test_get_index_url_pretty_permalinks() { + // Set permalinks for testing. + $this->set_permalink_structure( '/%year%/%postname%/' ); + + $sitemap_index = new WP_Sitemaps_Index( new WP_Sitemaps_Registry() ); + $index_url = $sitemap_index->get_index_url(); + + // Clean up permalinks. + $this->set_permalink_structure(); + + $this->assertStringEndsWith( '/wp-sitemap.xml', $index_url ); + } +} diff --git a/tests/phpunit/tests/sitemaps/sitemaps-posts.php b/tests/phpunit/tests/sitemaps/sitemaps-posts.php new file mode 100644 index 0000000000000..4e81d814bbe8e --- /dev/null +++ b/tests/phpunit/tests/sitemaps/sitemaps-posts.php @@ -0,0 +1,19 @@ +get_object_subtypes(); + + $this->assertEquals( array(), $subtypes, 'Could not filter posts subtypes.' ); + } +} diff --git a/tests/phpunit/tests/sitemaps/sitemaps-registry.php b/tests/phpunit/tests/sitemaps/sitemaps-registry.php new file mode 100644 index 0000000000000..1b231945b23c9 --- /dev/null +++ b/tests/phpunit/tests/sitemaps/sitemaps-registry.php @@ -0,0 +1,44 @@ +add_sitemap( 'foo', $provider ); + $sitemaps = $registry->get_sitemaps(); + + $this->assertTrue( $actual ); + $this->assertCount( 1, $sitemaps ); + $this->assertSame( $sitemaps['foo'], $provider, 'Can not confirm sitemap registration is working.' ); + } + + public function test_add_sitemap_prevent_duplicates() { + $provider1 = new WP_Sitemaps_Test_Provider(); + $provider2 = new WP_Sitemaps_Test_Provider(); + $registry = new WP_Sitemaps_Registry(); + + $actual1 = $registry->add_sitemap( 'foo', $provider1 ); + $actual2 = $registry->add_sitemap( 'foo', $provider2 ); + $sitemaps = $registry->get_sitemaps(); + + $this->assertTrue( $actual1 ); + $this->assertFalse( $actual2 ); + $this->assertCount( 1, $sitemaps ); + $this->assertSame( $sitemaps['foo'], $provider1, 'Can not confirm sitemap registration is working.' ); + } + + public function test_add_sitemap_invalid_type() { + $provider = null; + $registry = new WP_Sitemaps_Registry(); + + $actual = $registry->add_sitemap( 'foo', $provider ); + $sitemaps = $registry->get_sitemaps(); + + $this->assertFalse( $actual ); + $this->assertCount( 0, $sitemaps ); + } +} diff --git a/tests/phpunit/tests/sitemaps/sitemaps-renderer.php b/tests/phpunit/tests/sitemaps/sitemaps-renderer.php new file mode 100644 index 0000000000000..f9f4eeddd627f --- /dev/null +++ b/tests/phpunit/tests/sitemaps/sitemaps-renderer.php @@ -0,0 +1,206 @@ +get_sitemap_stylesheet_url(); + + $this->assertStringEndsWith( '/?sitemap-stylesheet=sitemap', $stylesheet_url ); + } + + public function test_get_sitemap_stylesheet_url_pretty_permalinks() { + // Set permalinks for testing. + $this->set_permalink_structure( '/%year%/%postname%/' ); + + $sitemap_renderer = new WP_Sitemaps_Renderer(); + $stylesheet_url = $sitemap_renderer->get_sitemap_stylesheet_url(); + + // Clean up permalinks. + $this->set_permalink_structure(); + + $this->assertStringEndsWith( '/wp-sitemap.xsl', $stylesheet_url ); + } + + public function test_get_sitemap_index_stylesheet_url() { + $sitemap_renderer = new WP_Sitemaps_Renderer(); + $stylesheet_url = $sitemap_renderer->get_sitemap_index_stylesheet_url(); + + $this->assertStringEndsWith( '/?sitemap-stylesheet=index', $stylesheet_url ); + } + + public function test_get_sitemap_index_stylesheet_url_pretty_permalinks() { + // Set permalinks for testing. + $this->set_permalink_structure( '/%year%/%postname%/' ); + + $sitemap_renderer = new WP_Sitemaps_Renderer(); + $stylesheet_url = $sitemap_renderer->get_sitemap_index_stylesheet_url(); + + // Clean up permalinks. + $this->set_permalink_structure(); + + $this->assertStringEndsWith( '/wp-sitemap-index.xsl', $stylesheet_url ); + } + + /** + * Test XML output for the sitemap index renderer. + */ + public function test_get_sitemap_index_xml() { + $entries = array( + array( + 'loc' => 'http://' . WP_TESTS_DOMAIN . '/wp-sitemap-posts-post-1.xml', + ), + array( + 'loc' => 'http://' . WP_TESTS_DOMAIN . '/wp-sitemap-posts-page-1.xml', + ), + array( + 'loc' => 'http://' . WP_TESTS_DOMAIN . '/wp-sitemap-taxonomies-category-1.xml', + ), + array( + 'loc' => 'http://' . WP_TESTS_DOMAIN . '/wp-sitemap-taxonomies-post_tag-1.xml', + ), + array( + 'loc' => 'http://' . WP_TESTS_DOMAIN . '/wp-sitemap-users-1.xml', + ), + ); + + $renderer = new WP_Sitemaps_Renderer(); + + $actual = $renderer->get_sitemap_index_xml( $entries ); + $expected = '' . + '' . + '' . + 'http://' . WP_TESTS_DOMAIN . '/wp-sitemap-posts-post-1.xml' . + 'http://' . WP_TESTS_DOMAIN . '/wp-sitemap-posts-page-1.xml' . + 'http://' . WP_TESTS_DOMAIN . '/wp-sitemap-taxonomies-category-1.xml' . + 'http://' . WP_TESTS_DOMAIN . '/wp-sitemap-taxonomies-post_tag-1.xml' . + 'http://' . WP_TESTS_DOMAIN . '/wp-sitemap-users-1.xml' . + ''; + + $this->assertXMLEquals( $expected, $actual, 'Sitemap index markup incorrect.' ); + } + + /** + * Test XML output for the sitemap index renderer when stylesheet is disabled. + */ + public function test_get_sitemap_index_xml_without_stylsheet() { + $entries = array( + array( + 'loc' => 'http://' . WP_TESTS_DOMAIN . '/wp-sitemap-posts-post-1.xml', + ), + ); + + add_filter( 'wp_sitemaps_stylesheet_index_url', '__return_false' ); + + $renderer = new WP_Sitemaps_Renderer(); + + $xml_dom = $this->loadXML( $renderer->get_sitemap_index_xml( $entries ) ); + $xpath = new DOMXPath( $xml_dom ); + + $this->assertSame( + 0, + $xpath->query( '//processing-instruction( "xml-stylesheet" )' )->length, + 'Sitemap index incorrectly contains the xml-stylesheet processing instruction.' + ); + } + + /** + * Test XML output for the sitemap page renderer. + */ + public function test_get_sitemap_xml() { + $url_list = array( + array( + 'loc' => 'http://' . WP_TESTS_DOMAIN . '/2019/10/post-1', + ), + array( + 'loc' => 'http://' . WP_TESTS_DOMAIN . '/2019/10/post-2', + ), + array( + 'loc' => 'http://' . WP_TESTS_DOMAIN . '/2019/10/post-3', + ), + array( + 'loc' => 'http://' . WP_TESTS_DOMAIN . '/2019/10/post-4', + ), + array( + 'loc' => 'http://' . WP_TESTS_DOMAIN . '/2019/10/post-5', + ), + ); + + $renderer = new WP_Sitemaps_Renderer(); + + $actual = $renderer->get_sitemap_xml( $url_list ); + $expected = '' . + '' . + '' . + 'http://' . WP_TESTS_DOMAIN . '/2019/10/post-1' . + 'http://' . WP_TESTS_DOMAIN . '/2019/10/post-2' . + 'http://' . WP_TESTS_DOMAIN . '/2019/10/post-3' . + 'http://' . WP_TESTS_DOMAIN . '/2019/10/post-4' . + 'http://' . WP_TESTS_DOMAIN . '/2019/10/post-5' . + ''; + + $this->assertXMLEquals( $expected, $actual, 'Sitemap page markup incorrect.' ); + } + + /** + * Test XML output for the sitemap page renderer when stylesheet is disabled. + */ + public function test_get_sitemap_xml_without_stylsheet() { + $url_list = array( + array( + 'loc' => 'http://' . WP_TESTS_DOMAIN . '/2019/10/post-1', + ), + ); + + add_filter( 'wp_sitemaps_stylesheet_url', '__return_false' ); + + $renderer = new WP_Sitemaps_Renderer(); + + $xml_dom = $this->loadXML( $renderer->get_sitemap_xml( $url_list ) ); + $xpath = new DOMXPath( $xml_dom ); + + $this->assertSame( + 0, + $xpath->query( '//processing-instruction( "xml-stylesheet" )' )->length, + 'Sitemap incorrectly contains the xml-stylesheet processing instruction.' + ); + } + + /** + * Test that all children of Q{http://www.sitemaps.org/schemas/sitemap/0.9}url in the rendered XML + * defined in the Sitemaps spec (i.e., loc, lastmod, changefreq, priority). + * + * Note that when a means of adding elements in extension namespaces is settled on, + * this test will need to be updated accordingly. + * + * @expectedIncorrectUsage WP_Sitemaps_Renderer::get_sitemap_xml + */ + public function test_get_sitemap_xml_extra_elements() { + $url_list = array( + array( + 'loc' => 'http://' . WP_TESTS_DOMAIN . '/2019/10/post-1', + 'string' => 'value', + 'number' => 200, + ), + array( + 'loc' => 'http://' . WP_TESTS_DOMAIN . '/2019/10/post-2', + 'string' => 'another value', + 'number' => 300, + ), + ); + + $renderer = new WP_Sitemaps_Renderer(); + + $xml_dom = $this->loadXML( $renderer->get_sitemap_xml( $url_list ) ); + $xpath = new DOMXPath( $xml_dom ); + $xpath->registerNamespace( 'sitemap', 'http://www.sitemaps.org/schemas/sitemap/0.9' ); + + $this->assertEquals( + 0, + $xpath->evaluate( "count( /sitemap:urlset/sitemap:url/*[ namespace-uri() != 'http://www.sitemaps.org/schemas/sitemap/0.9' or not( local-name() = 'loc' or local-name() = 'lastmod' or local-name() = 'changefreq' or local-name() = 'priority' ) ] )" ), + 'Invalid child of "sitemap:url" in rendered XML.' + ); + } +} diff --git a/tests/phpunit/tests/sitemaps/sitemaps-stylesheet.php b/tests/phpunit/tests/sitemaps/sitemaps-stylesheet.php new file mode 100644 index 0000000000000..3e2432cbfe6b5 --- /dev/null +++ b/tests/phpunit/tests/sitemaps/sitemaps-stylesheet.php @@ -0,0 +1,42 @@ +get_sitemap_stylesheet(); + + $this->assertSame( '', $content, 'Could not filter stylesheet content' ); + } + + /** + * Test that sitemap index stylesheet content can be filtered. + */ + public function test_filter_sitemaps_index_stylesheet_content() { + $stylesheet = new WP_Sitemaps_Stylesheet(); + + add_filter( 'wp_sitemaps_index_stylesheet_content', '__return_empty_string' ); + $content = $stylesheet->get_sitemap_index_stylesheet(); + + $this->assertSame( '', $content, 'Could not filter sitemap index stylesheet content' ); + } + + /** + * Test that sitemap stylesheet CSS can be filtered. + */ + public function test_filter_sitemaps_stylesheet_css() { + $stylesheet = new WP_Sitemaps_Stylesheet(); + + add_filter( 'wp_sitemaps_stylesheet_css', '__return_empty_string' ); + $css = $stylesheet->get_stylesheet_css(); + + $this->assertSame( '', $css, 'Could not filter sitemap stylesheet CSS' ); + } +} diff --git a/tests/phpunit/tests/sitemaps/sitemaps-taxonomies.php b/tests/phpunit/tests/sitemaps/sitemaps-taxonomies.php new file mode 100644 index 0000000000000..38aae2c77150f --- /dev/null +++ b/tests/phpunit/tests/sitemaps/sitemaps-taxonomies.php @@ -0,0 +1,192 @@ +term->create_many( 10, array( 'taxonomy' => 'category' ) ); + self::$post_tags = $factory->term->create_many( 10 ); + self::$editor_id = $factory->user->create( array( 'role' => 'editor' ) ); + } + + /** + * Test getting a URL list for default taxonomies via + * WP_Sitemaps_Taxonomies::get_url_list(). + */ + public function test_get_url_list_taxonomies() { + // Add the default category to the list of categories we're testing. + $categories = array_merge( array( 1 ), self::$cats ); + + // Create a test post to calculate update times. + $post = self::factory()->post->create_and_get( + array( + 'tags_input' => self::$post_tags, + 'post_category' => $categories, + ) + ); + + $tax_provider = new WP_Sitemaps_Taxonomies(); + + $cat_list = $tax_provider->get_url_list( 1, 'category' ); + + $expected_cats = array_map( + static function ( $id ) use ( $post ) { + return array( + 'loc' => get_term_link( $id, 'category' ), + ); + }, + $categories + ); + + $this->assertSame( $expected_cats, $cat_list, 'Category URL list does not match.' ); + + $tag_list = $tax_provider->get_url_list( 1, 'post_tag' ); + + $expected_tags = array_map( + static function ( $id ) use ( $post ) { + return array( + 'loc' => get_term_link( $id, 'post_tag' ), + ); + }, + self::$post_tags + ); + + $this->assertSame( $expected_tags, $tag_list, 'Post Tags URL list does not match.' ); + } + + /** + * Test getting a URL list for a custom taxonomy via + * WP_Sitemaps_Taxonomies::get_url_list(). + */ + public function test_get_url_list_custom_taxonomy() { + wp_set_current_user( self::$editor_id ); + + // Create a custom taxonomy for this test. + $taxonomy = 'test_taxonomy'; + register_taxonomy( $taxonomy, 'post' ); + + // Create test terms in the custom taxonomy. + $terms = self::factory()->term->create_many( 10, array( 'taxonomy' => $taxonomy ) ); + + // Create a test post applied to all test terms. + $post = self::factory()->post->create_and_get( array( 'tax_input' => array( $taxonomy => $terms ) ) ); + + $expected = array_map( + static function ( $id ) use ( $taxonomy, $post ) { + return array( + 'loc' => get_term_link( $id, $taxonomy ), + ); + }, + $terms + ); + + $tax_provider = new WP_Sitemaps_Taxonomies(); + + $post_list = $tax_provider->get_url_list( 1, $taxonomy ); + + // Clean up. + unregister_taxonomy_for_object_type( $taxonomy, 'post' ); + + $this->assertEquals( $expected, $post_list, 'Custom taxonomy term links are not visible.' ); + } + + /** + * Test getting a URL list for a private custom taxonomy via + * WP_Sitemaps_Taxonomies::get_url_list(). + */ + public function test_get_url_list_custom_taxonomy_private() { + // Create a custom taxonomy for this test. + $taxonomy = 'private_taxonomy'; + register_taxonomy( $taxonomy, 'post', array( 'public' => false ) ); + + // Create test terms in the custom taxonomy. + $terms = self::factory()->term->create_many( 10, array( 'taxonomy' => $taxonomy ) ); + + // Create a test post applied to all test terms. + self::factory()->post->create( array( 'tax_input' => array( $taxonomy => $terms ) ) ); + + $tax_provider = new WP_Sitemaps_Taxonomies(); + + $post_list = $tax_provider->get_url_list( 1, $taxonomy ); + + // Clean up. + unregister_taxonomy_for_object_type( $taxonomy, 'post' ); + + $this->assertEmpty( $post_list, 'Private taxonomy term links are visible.' ); + } + + /** + * Test sitemap index entries with public and private taxonomies. + */ + public function test_get_sitemap_entries_custom_taxonomies() { + wp_set_current_user( self::$editor_id ); + + // Create a custom public and private taxonomies for this test. + register_taxonomy( 'public_taxonomy', 'post' ); + register_taxonomy( 'private_taxonomy', 'post', array( 'public' => false ) ); + + // Create test terms in the custom taxonomy. + $public_term = self::factory()->term->create( array( 'taxonomy' => 'public_taxonomy' ) ); + $private_term = self::factory()->term->create( array( 'taxonomy' => 'private_taxonomy' ) ); + + // Create a test post applied to all test terms. + self::factory()->post->create_and_get( + array( + 'tax_input' => array( + 'public_taxonomy' => array( $public_term ), + 'private_taxonomy' => array( $private_term ), + ), + ) + ); + + $tax_provider = new WP_Sitemaps_Taxonomies(); + $entries = wp_list_pluck( $tax_provider->get_sitemap_entries(), 'loc' ); + + // Clean up. + unregister_taxonomy_for_object_type( 'public_taxonomy', 'post' ); + unregister_taxonomy_for_object_type( 'private_taxonomy', 'post' ); + + $this->assertContains( 'http://' . WP_TESTS_DOMAIN . '/?sitemap=taxonomies&sitemap-sub-type=public_taxonomy&paged=1', $entries, 'Public Taxonomies are not in the index.' ); + $this->assertNotContains( 'http://' . WP_TESTS_DOMAIN . '/?sitemap=taxonomies&sitemap-sub-type=private_taxonomy&paged=1', $entries, 'Private Taxonomies are visible in the index.' ); + } + + /** + * Test ability to filter object subtypes. + */ + public function test_filter_sitemaps_taxonomies() { + $taxonomies_provider = new WP_Sitemaps_Taxonomies(); + + // Return an empty array to show that the list of subtypes is filterable. + add_filter( 'wp_sitemaps_taxonomies', '__return_empty_array' ); + $subtypes = $taxonomies_provider->get_object_subtypes(); + + $this->assertEquals( array(), $subtypes, 'Could not filter taxonomies subtypes.' ); + } +} diff --git a/tests/phpunit/tests/sitemaps/sitemaps-users.php b/tests/phpunit/tests/sitemaps/sitemaps-users.php new file mode 100644 index 0000000000000..5c49b8bd9f51e --- /dev/null +++ b/tests/phpunit/tests/sitemaps/sitemaps-users.php @@ -0,0 +1,57 @@ +user->create_many( 10, array( 'role' => 'editor' ) ); + self::$editor_id = self::$users[0]; + } + + /** + * Test getting a URL list for a users sitemap page via + * WP_Sitemaps_Users::get_url_list(). + */ + public function test_get_url_list_users() { + // Set up the user to an editor to assign posts to other users. + wp_set_current_user( self::$editor_id ); + + // Create a set of posts for each user and generate the expected URL list data. + $expected = array_map( + static function ( $user_id ) { + $post = self::factory()->post->create_and_get( array( 'post_author' => $user_id ) ); + + return array( + 'loc' => get_author_posts_url( $user_id ), + ); + }, + self::$users + ); + + $user_provider = new WP_Sitemaps_Users(); + + $url_list = $user_provider->get_url_list( 1 ); + + $this->assertEqualSets( $expected, $url_list ); + } +} diff --git a/tests/phpunit/tests/sitemaps/sitemaps.php b/tests/phpunit/tests/sitemaps/sitemaps.php new file mode 100644 index 0000000000000..3d6f879947509 --- /dev/null +++ b/tests/phpunit/tests/sitemaps/sitemaps.php @@ -0,0 +1,396 @@ +user->create_many( 10 ); + self::$post_tags = $factory->term->create_many( 10 ); + self::$cats = $factory->term->create_many( 10, array( 'taxonomy' => 'category' ) ); + self::$pages = $factory->post->create_many( 10, array( 'post_type' => 'page' ) ); + + // Create a set of posts pre-assigned to tags and authors. + self::$posts = $factory->post->create_many( + 10, + array( + 'tags_input' => self::$post_tags, + 'post_author' => reset( self::$users ), + ) + ); + + // Create a user with an editor role to complete some tests. + self::$editor_id = $factory->user->create( array( 'role' => 'editor' ) ); + + self::$test_provider = new WP_Sitemaps_Test_Provider(); + } + + /** + * Helper function to get all sitemap entries data. + * + * @return array A list of sitemap entires. + */ + public function _get_sitemap_entries() { + $entries = array(); + + $providers = wp_get_sitemaps(); + + foreach ( $providers as $provider ) { + // Using `array_push` is more efficient than `array_merge` in the loop. + array_push( $entries, ...$provider->get_sitemap_entries() ); + } + + return $entries; + } + + /** + * Test default sitemap entries. + */ + public function test_get_sitemap_entries() { + $entries = $this->_get_sitemap_entries(); + + $expected = array( + array( + 'loc' => 'http://' . WP_TESTS_DOMAIN . '/?sitemap=posts&sitemap-sub-type=post&paged=1', + ), + array( + 'loc' => 'http://' . WP_TESTS_DOMAIN . '/?sitemap=posts&sitemap-sub-type=page&paged=1', + ), + array( + 'loc' => 'http://' . WP_TESTS_DOMAIN . '/?sitemap=taxonomies&sitemap-sub-type=category&paged=1', + ), + array( + 'loc' => 'http://' . WP_TESTS_DOMAIN . '/?sitemap=taxonomies&sitemap-sub-type=post_tag&paged=1', + ), + array( + 'loc' => 'http://' . WP_TESTS_DOMAIN . '/?sitemap=users&paged=1', + ), + ); + + $this->assertSame( $expected, $entries ); + } + + /** + * Test default sitemap entries with permalinks on. + */ + public function test_get_sitemap_entries_post_with_permalinks() { + $this->set_permalink_structure( '/%year%/%postname%/' ); + + $entries = $this->_get_sitemap_entries(); + + $expected = array( + array( + 'loc' => 'http://' . WP_TESTS_DOMAIN . '/wp-sitemap-posts-post-1.xml', + ), + array( + 'loc' => 'http://' . WP_TESTS_DOMAIN . '/wp-sitemap-posts-page-1.xml', + ), + array( + 'loc' => 'http://' . WP_TESTS_DOMAIN . '/wp-sitemap-taxonomies-category-1.xml', + ), + array( + 'loc' => 'http://' . WP_TESTS_DOMAIN . '/wp-sitemap-taxonomies-post_tag-1.xml', + ), + array( + 'loc' => 'http://' . WP_TESTS_DOMAIN . '/wp-sitemap-users-1.xml', + ), + ); + + // Clean up permalinks. + $this->set_permalink_structure(); + + $this->assertSame( $expected, $entries ); + } + + /** + * Test sitemap index entries with public and private custom post types. + */ + public function test_get_sitemap_entries_custom_post_types() { + // Register and create a public post type post. + register_post_type( 'public_cpt', array( 'public' => true ) ); + self::factory()->post->create( array( 'post_type' => 'public_cpt' ) ); + + // Register and create a private post type post. + register_post_type( 'private_cpt', array( 'public' => false ) ); + self::factory()->post->create( array( 'post_type' => 'private_cpt' ) ); + + $entries = wp_list_pluck( $this->_get_sitemap_entries(), 'loc' ); + + // Clean up. + unregister_post_type( 'public_cpt' ); + unregister_post_type( 'private_cpt' ); + + $this->assertContains( 'http://' . WP_TESTS_DOMAIN . '/?sitemap=posts&sitemap-sub-type=public_cpt&paged=1', $entries, 'Public CPTs are not in the index.' ); + $this->assertNotContains( 'http://' . WP_TESTS_DOMAIN . '/?sitemap=posts&sitemap-sub-type=private_cpt&paged=1', $entries, 'Private CPTs are visible in the index.' ); + } + + /** + * Tests getting a URL list for post type post. + */ + public function test_get_url_list_post() { + $providers = wp_get_sitemaps(); + + $post_list = $providers['posts']->get_url_list( 1, 'post' ); + + $expected = $this->_get_expected_url_list( 'post', self::$posts ); + + $this->assertEquals( $expected, $post_list ); + } + + /** + * Tests getting a URL list for post type page. + */ + public function test_get_url_list_page() { + // Short circuit the show on front option. + add_filter( 'pre_option_show_on_front', '__return_true' ); + + $providers = wp_get_sitemaps(); + + $post_list = $providers['posts']->get_url_list( 1, 'page' ); + + $expected = $this->_get_expected_url_list( 'page', self::$pages ); + + $this->assertEquals( $expected, $post_list ); + } + + /** + * Tests getting a URL list for post type page with included home page. + */ + public function test_get_url_list_page_with_home() { + $providers = wp_get_sitemaps(); + + $post_list = $providers['posts']->get_url_list( 1, 'page' ); + + $expected = $this->_get_expected_url_list( 'page', self::$pages ); + + // Add the homepage to the front of the URL list. + array_unshift( + $expected, + array( + 'loc' => home_url(), + ) + ); + + $this->assertEquals( $expected, $post_list ); + } + + /** + * Tests getting a URL list for post with private post. + */ + public function test_get_url_list_private_post() { + wp_set_current_user( self::$editor_id ); + + $providers = wp_get_sitemaps(); + + $post_list_before = $providers['posts']->get_url_list( 1, 'post' ); + + $private_post_id = self::factory()->post->create( array( 'post_status' => 'private' ) ); + + $post_list_after = $providers['posts']->get_url_list( 1, 'post' ); + + $private_post = array( + 'loc' => get_permalink( $private_post_id ), + ); + + $this->assertNotContains( $private_post, $post_list_after ); + $this->assertEqualSets( $post_list_before, $post_list_after ); + } + + /** + * Tests getting a URL list for a custom post type. + */ + public function test_get_url_list_cpt() { + $post_type = 'custom_type'; + + // Registered post types are private unless explicitly set to public. + register_post_type( $post_type, array( 'public' => true ) ); + + $ids = self::factory()->post->create_many( 10, array( 'post_type' => $post_type ) ); + + $providers = wp_get_sitemaps(); + + $post_list = $providers['posts']->get_url_list( 1, $post_type ); + + $expected = $this->_get_expected_url_list( $post_type, $ids ); + + // Clean up. + unregister_post_type( $post_type ); + + $this->assertEquals( $expected, $post_list, 'Custom post type posts are not visible.' ); + } + + /** + * Tests getting a URL list for a private custom post type. + */ + public function test_get_url_list_cpt_private() { + $post_type = 'private_type'; + + // Create a private post type for testing against data leaking. + register_post_type( $post_type, array( 'public' => false ) ); + + self::factory()->post->create_many( 10, array( 'post_type' => $post_type ) ); + + $providers = wp_get_sitemaps(); + + $post_list = $providers['posts']->get_url_list( 1, $post_type ); + + // Clean up. + unregister_post_type( $post_type ); + + $this->assertEmpty( $post_list, 'Private post types may be returned by the post provider.' ); + } + + /** + * Helper function for building an expected url list. + * + * @param string $type An object sub type, e.g., post type. + * @param array $ids Array of object IDs. + * @return array A formed URL list. + */ + public function _get_expected_url_list( $type, $ids ) { + $posts = get_posts( + array( + 'include' => $ids, + 'orderby' => 'ID', + 'order' => 'ASC', + 'post_type' => $type, + ) + ); + + return array_map( + static function ( $post ) { + return array( + 'loc' => get_permalink( $post ), + ); + }, + $posts + ); + } + + /** + * Test functionality that adds a new sitemap provider to the registry. + */ + public function test_register_sitemap_provider() { + wp_register_sitemap( 'test_sitemap', self::$test_provider ); + + $sitemaps = wp_get_sitemaps(); + + $this->assertEquals( $sitemaps['test_sitemap'], self::$test_provider, 'Can not confirm sitemap registration is working.' ); + } + + /** + * Test robots.txt output. + */ + public function test_robots_text() { + // Get the text added to the default robots text output. + $robots_text = apply_filters( 'robots_txt', '', true ); + $sitemap_string = 'Sitemap: http://' . WP_TESTS_DOMAIN . '/?sitemap=index'; + + $this->assertContains( $sitemap_string, $robots_text, 'Sitemap URL not included in robots text.' ); + } + + /** + * Test robots.txt output for a private site. + */ + public function test_robots_text_private_site() { + $robots_text = apply_filters( 'robots_txt', '', false ); + $sitemap_string = 'Sitemap: http://' . WP_TESTS_DOMAIN . '/?sitemap=index'; + + $this->assertNotContains( $sitemap_string, $robots_text ); + } + + /** + * Test robots.txt output with permalinks set. + */ + public function test_robots_text_with_permalinks() { + // Set permalinks for testing. + $this->set_permalink_structure( '/%year%/%postname%/' ); + + // Get the text added to the default robots text output. + $robots_text = apply_filters( 'robots_txt', '', true ); + $sitemap_string = 'Sitemap: http://' . WP_TESTS_DOMAIN . '/wp-sitemap.xml'; + + // Clean up permalinks. + $this->set_permalink_structure(); + + $this->assertContains( $sitemap_string, $robots_text, 'Sitemap URL not included in robots text.' ); + } + + /** + * Test robots.txt output with line feed prefix. + */ + public function test_robots_text_prefixed_with_line_feed() { + // Get the text added to the default robots text output. + $robots_text = apply_filters( 'robots_txt', '', true ); + $sitemap_string = "\nSitemap: "; + + $this->assertContains( $sitemap_string, $robots_text, 'Sitemap URL not prefixed with "\n".' ); + } +} From e64139a16ace7e535d961c40064afe82a2575207 Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Wed, 3 Jun 2020 12:32:02 +0200 Subject: [PATCH 02/18] sitemap-sub-type -> sitemap-subtype --- .../sitemaps/class-wp-sitemaps-provider.php | 6 +++--- src/wp-includes/sitemaps/class-wp-sitemaps.php | 6 +++--- tests/phpunit/tests/sitemaps/sitemaps-taxonomies.php | 4 ++-- tests/phpunit/tests/sitemaps/sitemaps.php | 12 ++++++------ 4 files changed, 14 insertions(+), 14 deletions(-) diff --git a/src/wp-includes/sitemaps/class-wp-sitemaps-provider.php b/src/wp-includes/sitemaps/class-wp-sitemaps-provider.php index dba1724c5baf6..5ef34fb463bb9 100644 --- a/src/wp-includes/sitemaps/class-wp-sitemaps-provider.php +++ b/src/wp-includes/sitemaps/class-wp-sitemaps-provider.php @@ -136,9 +136,9 @@ public function get_sitemap_url( $name, $page ) { // Accounts for cases where name is not included, ex: sitemaps-users-1.xml. array_filter( array( - 'sitemap' => $this->name, - 'sitemap-sub-type' => $name, - 'paged' => $page, + 'sitemap' => $this->name, + 'sitemap-subtype' => $name, + 'paged' => $page, ) ), home_url( '/' ) diff --git a/src/wp-includes/sitemaps/class-wp-sitemaps.php b/src/wp-includes/sitemaps/class-wp-sitemaps.php index 7e6ce343d742e..cd93ed59aec03 100644 --- a/src/wp-includes/sitemaps/class-wp-sitemaps.php +++ b/src/wp-includes/sitemaps/class-wp-sitemaps.php @@ -113,7 +113,7 @@ public function register_sitemaps() { public function register_rewrites() { // Add rewrite tags. add_rewrite_tag( '%sitemap%', '([^?]+)' ); - add_rewrite_tag( '%sitemap-sub-type%', '([^?]+)' ); + add_rewrite_tag( '%sitemap-subtype%', '([^?]+)' ); // Register index route. add_rewrite_rule( '^wp-sitemap\.xml$', 'index.php?sitemap=index', 'top' ); @@ -126,7 +126,7 @@ public function register_rewrites() { // Register routes for providers. add_rewrite_rule( '^wp-sitemap-([a-z]+?)-([a-z\d_-]+?)-(\d+?)\.xml$', - 'index.php?sitemap=$matches[1]&sitemap-sub-type=$matches[2]&paged=$matches[3]', + 'index.php?sitemap=$matches[1]&sitemap-subtype=$matches[2]&paged=$matches[3]', 'top' ); add_rewrite_rule( @@ -145,7 +145,7 @@ public function render_sitemaps() { global $wp_query; $sitemap = sanitize_text_field( get_query_var( 'sitemap' ) ); - $object_subtype = sanitize_text_field( get_query_var( 'sitemap-sub-type' ) ); + $object_subtype = sanitize_text_field( get_query_var( 'sitemap-subtype' ) ); $stylesheet_type = sanitize_text_field( get_query_var( 'sitemap-stylesheet' ) ); $paged = absint( get_query_var( 'paged' ) ); diff --git a/tests/phpunit/tests/sitemaps/sitemaps-taxonomies.php b/tests/phpunit/tests/sitemaps/sitemaps-taxonomies.php index 38aae2c77150f..35077e26c58a4 100644 --- a/tests/phpunit/tests/sitemaps/sitemaps-taxonomies.php +++ b/tests/phpunit/tests/sitemaps/sitemaps-taxonomies.php @@ -173,8 +173,8 @@ public function test_get_sitemap_entries_custom_taxonomies() { unregister_taxonomy_for_object_type( 'public_taxonomy', 'post' ); unregister_taxonomy_for_object_type( 'private_taxonomy', 'post' ); - $this->assertContains( 'http://' . WP_TESTS_DOMAIN . '/?sitemap=taxonomies&sitemap-sub-type=public_taxonomy&paged=1', $entries, 'Public Taxonomies are not in the index.' ); - $this->assertNotContains( 'http://' . WP_TESTS_DOMAIN . '/?sitemap=taxonomies&sitemap-sub-type=private_taxonomy&paged=1', $entries, 'Private Taxonomies are visible in the index.' ); + $this->assertContains( 'http://' . WP_TESTS_DOMAIN . '/?sitemap=taxonomies&sitemap-subtype=public_taxonomy&paged=1', $entries, 'Public Taxonomies are not in the index.' ); + $this->assertNotContains( 'http://' . WP_TESTS_DOMAIN . '/?sitemap=taxonomies&sitemap-subtype=private_taxonomy&paged=1', $entries, 'Private Taxonomies are visible in the index.' ); } /** diff --git a/tests/phpunit/tests/sitemaps/sitemaps.php b/tests/phpunit/tests/sitemaps/sitemaps.php index 3d6f879947509..1ec79152326de 100644 --- a/tests/phpunit/tests/sitemaps/sitemaps.php +++ b/tests/phpunit/tests/sitemaps/sitemaps.php @@ -118,16 +118,16 @@ public function test_get_sitemap_entries() { $expected = array( array( - 'loc' => 'http://' . WP_TESTS_DOMAIN . '/?sitemap=posts&sitemap-sub-type=post&paged=1', + 'loc' => 'http://' . WP_TESTS_DOMAIN . '/?sitemap=posts&sitemap-subtype=post&paged=1', ), array( - 'loc' => 'http://' . WP_TESTS_DOMAIN . '/?sitemap=posts&sitemap-sub-type=page&paged=1', + 'loc' => 'http://' . WP_TESTS_DOMAIN . '/?sitemap=posts&sitemap-subtype=page&paged=1', ), array( - 'loc' => 'http://' . WP_TESTS_DOMAIN . '/?sitemap=taxonomies&sitemap-sub-type=category&paged=1', + 'loc' => 'http://' . WP_TESTS_DOMAIN . '/?sitemap=taxonomies&sitemap-subtype=category&paged=1', ), array( - 'loc' => 'http://' . WP_TESTS_DOMAIN . '/?sitemap=taxonomies&sitemap-sub-type=post_tag&paged=1', + 'loc' => 'http://' . WP_TESTS_DOMAIN . '/?sitemap=taxonomies&sitemap-subtype=post_tag&paged=1', ), array( 'loc' => 'http://' . WP_TESTS_DOMAIN . '/?sitemap=users&paged=1', @@ -187,8 +187,8 @@ public function test_get_sitemap_entries_custom_post_types() { unregister_post_type( 'public_cpt' ); unregister_post_type( 'private_cpt' ); - $this->assertContains( 'http://' . WP_TESTS_DOMAIN . '/?sitemap=posts&sitemap-sub-type=public_cpt&paged=1', $entries, 'Public CPTs are not in the index.' ); - $this->assertNotContains( 'http://' . WP_TESTS_DOMAIN . '/?sitemap=posts&sitemap-sub-type=private_cpt&paged=1', $entries, 'Private CPTs are visible in the index.' ); + $this->assertContains( 'http://' . WP_TESTS_DOMAIN . '/?sitemap=posts&sitemap-subtype=public_cpt&paged=1', $entries, 'Public CPTs are not in the index.' ); + $this->assertNotContains( 'http://' . WP_TESTS_DOMAIN . '/?sitemap=posts&sitemap-subtype=private_cpt&paged=1', $entries, 'Private CPTs are visible in the index.' ); } /** From 770b9b5378dbc631d6c1a93d99e8e2d19760b7c0 Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Wed, 3 Jun 2020 12:33:29 +0200 Subject: [PATCH 03/18] Better canonical integration --- src/wp-includes/canonical.php | 5 +++ .../sitemaps/class-wp-sitemaps.php | 19 +--------- tests/phpunit/tests/canonical/sitemaps.php | 35 +++++++++++++++++++ 3 files changed, 41 insertions(+), 18 deletions(-) create mode 100644 tests/phpunit/tests/canonical/sitemaps.php diff --git a/src/wp-includes/canonical.php b/src/wp-includes/canonical.php index 42d6dcf44a276..5a1f3b814b2eb 100644 --- a/src/wp-includes/canonical.php +++ b/src/wp-includes/canonical.php @@ -673,6 +673,11 @@ function redirect_canonical( $requested_url = null, $do_redirect = true ) { $redirect['host'] = $original['host']; } + // Remove trailing slash for sitemaps requests. + if ( get_query_var( 'sitemap' ) || get_query_var( 'sitemap-stylesheet' ) ) { + $redirect['path'] = untrailingslashit( $redirect['path'] ); + } + $compare_original = array( $original['host'], $original['path'] ); if ( ! empty( $original['port'] ) ) { diff --git a/src/wp-includes/sitemaps/class-wp-sitemaps.php b/src/wp-includes/sitemaps/class-wp-sitemaps.php index cd93ed59aec03..73137fa7f6041 100644 --- a/src/wp-includes/sitemaps/class-wp-sitemaps.php +++ b/src/wp-includes/sitemaps/class-wp-sitemaps.php @@ -60,14 +60,13 @@ public function __construct() { */ public function init() { // These will all fire on the init hook. + $this->register_rewrites(); $this->register_sitemaps(); // Add additional action callbacks. - add_action( 'wp_sitemaps_init', array( $this, 'register_rewrites' ) ); add_action( 'template_redirect', array( $this, 'render_sitemaps' ) ); add_filter( 'pre_handle_404', array( $this, 'redirect_sitemapxml' ), 10, 2 ); add_filter( 'robots_txt', array( $this, 'add_robots' ), 0, 2 ); - add_filter( 'redirect_canonical', array( $this, 'redirect_canonical' ) ); } /** @@ -233,20 +232,4 @@ public function add_robots( $output, $public ) { return $output; } - - /** - * Prevent trailing slashes. - * - * @since 5.5.0 - * - * @param string $redirect The redirect URL currently determined. - * @return bool|string $redirect The canonical redirect URL. - */ - public function redirect_canonical( $redirect ) { - if ( get_query_var( 'sitemap' ) || get_query_var( 'sitemap-stylesheet' ) ) { - return false; - } - - return $redirect; - } } diff --git a/tests/phpunit/tests/canonical/sitemaps.php b/tests/phpunit/tests/canonical/sitemaps.php new file mode 100644 index 0000000000000..9ee964242e526 --- /dev/null +++ b/tests/phpunit/tests/canonical/sitemaps.php @@ -0,0 +1,35 @@ +set_permalink_structure( '/%postname%/' ); + $this->assertCanonical('/wp-sitemap.xml', '/wp-sitemap.xml' ); + $this->assertCanonical('/wp-sitemap.xml/', '/wp-sitemap.xml' ); + } + + public function test_remove_trailing_slashes_for_sitemap_index_stylesheet_requests() { + $this->set_permalink_structure( '/%postname%/' ); + $this->assertCanonical('/wp-sitemap-index.xsl', '/wp-sitemap-index.xsl' ); + $this->assertCanonical('/wp-sitemap-index.xsl/', '/wp-sitemap-index.xsl' ); + } + + public function test_remove_trailing_slashes_for_sitemap_requests() { + $this->set_permalink_structure( '/%postname%/' ); + $this->assertCanonical('/wp-sitemap-posts-post-1.xml', '/wp-sitemap-posts-post-1.xml' ); + $this->assertCanonical('/wp-sitemap-posts-post-1.xml/', '/wp-sitemap-posts-post-1.xml' ); + $this->assertCanonical('/wp-sitemap-users-1.xml', '/wp-sitemap-users-1.xml' ); + $this->assertCanonical('/wp-sitemap-users-1.xml/', '/wp-sitemap-users-1.xml' ); + } + + public function test_remove_trailing_slashes_for_sitemap_stylesheet_requests() { + $this->set_permalink_structure( '/%postname%/' ); + $this->assertCanonical('/wp-sitemap.xsl', '/wp-sitemap.xsl' ); + $this->assertCanonical('/wp-sitemap.xsl/', '/wp-sitemap.xsl' ); + } +} From 4097dcdde87410626d9880a67e07ef10d64bb6cf Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Wed, 3 Jun 2020 13:21:14 +0200 Subject: [PATCH 04/18] Lint fixes --- tests/phpunit/tests/canonical/sitemaps.php | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/tests/phpunit/tests/canonical/sitemaps.php b/tests/phpunit/tests/canonical/sitemaps.php index 9ee964242e526..4c26a080f973d 100644 --- a/tests/phpunit/tests/canonical/sitemaps.php +++ b/tests/phpunit/tests/canonical/sitemaps.php @@ -9,27 +9,27 @@ class Tests_Canonical_Sitemaps extends WP_Canonical_UnitTestCase { public function test_remove_trailing_slashes_for_sitemap_index_requests() { $this->set_permalink_structure( '/%postname%/' ); - $this->assertCanonical('/wp-sitemap.xml', '/wp-sitemap.xml' ); - $this->assertCanonical('/wp-sitemap.xml/', '/wp-sitemap.xml' ); + $this->assertCanonical( '/wp-sitemap.xml', '/wp-sitemap.xml' ); + $this->assertCanonical( '/wp-sitemap.xml/', '/wp-sitemap.xml' ); } public function test_remove_trailing_slashes_for_sitemap_index_stylesheet_requests() { $this->set_permalink_structure( '/%postname%/' ); - $this->assertCanonical('/wp-sitemap-index.xsl', '/wp-sitemap-index.xsl' ); - $this->assertCanonical('/wp-sitemap-index.xsl/', '/wp-sitemap-index.xsl' ); + $this->assertCanonical( '/wp-sitemap-index.xsl', '/wp-sitemap-index.xsl' ); + $this->assertCanonical( '/wp-sitemap-index.xsl/', '/wp-sitemap-index.xsl' ); } public function test_remove_trailing_slashes_for_sitemap_requests() { $this->set_permalink_structure( '/%postname%/' ); - $this->assertCanonical('/wp-sitemap-posts-post-1.xml', '/wp-sitemap-posts-post-1.xml' ); - $this->assertCanonical('/wp-sitemap-posts-post-1.xml/', '/wp-sitemap-posts-post-1.xml' ); - $this->assertCanonical('/wp-sitemap-users-1.xml', '/wp-sitemap-users-1.xml' ); - $this->assertCanonical('/wp-sitemap-users-1.xml/', '/wp-sitemap-users-1.xml' ); + $this->assertCanonical( '/wp-sitemap-posts-post-1.xml', '/wp-sitemap-posts-post-1.xml' ); + $this->assertCanonical( '/wp-sitemap-posts-post-1.xml/', '/wp-sitemap-posts-post-1.xml' ); + $this->assertCanonical( '/wp-sitemap-users-1.xml', '/wp-sitemap-users-1.xml' ); + $this->assertCanonical( '/wp-sitemap-users-1.xml/', '/wp-sitemap-users-1.xml' ); } public function test_remove_trailing_slashes_for_sitemap_stylesheet_requests() { $this->set_permalink_structure( '/%postname%/' ); - $this->assertCanonical('/wp-sitemap.xsl', '/wp-sitemap.xsl' ); - $this->assertCanonical('/wp-sitemap.xsl/', '/wp-sitemap.xsl' ); + $this->assertCanonical( '/wp-sitemap.xsl', '/wp-sitemap.xsl' ); + $this->assertCanonical( '/wp-sitemap.xsl/', '/wp-sitemap.xsl' ); } } From e12ac982c409e6331e6f4f7c1976a7bac31e2e89 Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Wed, 3 Jun 2020 13:22:31 +0200 Subject: [PATCH 05/18] =?UTF-8?q?Don=E2=80=99t=20add=20WP=5FSITEMAPS=5FMAX?= =?UTF-8?q?=5FURLS=20constant?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The value is filterable anyway --- src/wp-includes/default-constants.php | 15 --------------- src/wp-includes/sitemaps.php | 2 +- src/wp-settings.php | 3 --- 3 files changed, 1 insertion(+), 19 deletions(-) diff --git a/src/wp-includes/default-constants.php b/src/wp-includes/default-constants.php index 2798a350cd13b..a2df8adef7665 100644 --- a/src/wp-includes/default-constants.php +++ b/src/wp-includes/default-constants.php @@ -409,18 +409,3 @@ function wp_templating_constants() { } -/** - * Defines sitemaps-related WordPress constants. - * - * @since 5.5.0 - */ -function wp_sitemaps_constants() { - /** - * Maximum number of URLs included in a sitemap. - * - * @sine 5.5.0 - */ - if ( ! defined( 'WP_SITEMAPS_MAX_URLS' ) ) { - define( 'WP_SITEMAPS_MAX_URLS', 2000 ); - } -} diff --git a/src/wp-includes/sitemaps.php b/src/wp-includes/sitemaps.php index 4464276fdd244..542340b05e65c 100644 --- a/src/wp-includes/sitemaps.php +++ b/src/wp-includes/sitemaps.php @@ -115,5 +115,5 @@ function wp_sitemaps_get_max_urls( $object_type ) { * @param int $max_urls The maximum number of URLs included in a sitemap. Default 2000. * @param string $object_type Object type for sitemap to be filtered (e.g. 'post', 'term', 'user'). */ - return apply_filters( 'wp_sitemaps_max_urls', WP_SITEMAPS_MAX_URLS, $object_type ); + return apply_filters( 'wp_sitemaps_max_urls', 2000, $object_type ); } diff --git a/src/wp-settings.php b/src/wp-settings.php index 13e20903e86d7..a78be1c0918c0 100644 --- a/src/wp-settings.php +++ b/src/wp-settings.php @@ -413,9 +413,6 @@ // Define constants which affect functionality if not already defined. wp_functionality_constants(); -// Define sitemaps-related cconstants. -wp_sitemaps_constants(); - // Add magic quotes and set up $_REQUEST ( $_GET + $_POST ). wp_magic_quotes(); From 29ba5dfbe1c8f65e49515f08291f5d831d038655 Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Wed, 3 Jun 2020 13:24:48 +0200 Subject: [PATCH 06/18] Remove l10n helpers for now --- src/wp-includes/l10n.php | 53 ---------------------------------------- 1 file changed, 53 deletions(-) diff --git a/src/wp-includes/l10n.php b/src/wp-includes/l10n.php index fe7f05921c270..d7e6acd55b1ca 100644 --- a/src/wp-includes/l10n.php +++ b/src/wp-includes/l10n.php @@ -336,59 +336,6 @@ function esc_html_e( $text, $domain = 'default' ) { echo esc_html( translate( $text, $domain ) ); } -/** - * Retrieve the translation of $text and escapes it for safe use in XML output. - * - * If there is no translation, or the text domain isn't loaded, the original text - * is escaped and returned. - * - * @since 5.5.0 - * - * @param string $text Text to translate. - * @param string $domain Optional. Text domain. Unique identifier for retrieving translated strings. - * Default 'default'. - * @return string Translated text. - */ -function esc_xml__( $text, $domain = 'default' ) { - return esc_xml( translate( $text, $domain ) ); -} - -/** - * Display translated text that has been escaped for safe use in XML output. - * - * If there is no translation, or the text domain isn't loaded, the original text - * is escaped and displayed. - * - * If you need the value for use in PHP, use esc_xml__(). - * - * @since 5.5.0 - * - * @param string $text Text to translate. - * @param string $domain Optional. Text domain. Unique identifier for retrieving translated strings. - * Default 'default'. - */ -function esc_xml_e( $text, $domain = 'default' ) { - echo esc_xml( translate( $text, $domain ) ); -} - -/** - * Translate string with gettext context, and escapes it for safe use in XML output. - * - * If there is no translation, or the text domain isn't loaded, the original text - * is escaped and returned. - * - * @since 5.5.0 - * - * @param string $text Text to translate. - * @param string $context Context information for the translators. - * @param string $domain Optional. Text domain. Unique identifier for retrieving translated strings. - * Default 'default'. - * @return string Translated text. - */ -function esc_xml_x( $text, $context, $domain = 'default' ) { - return esc_xml( translate_with_gettext_context( $text, $context, $domain ) ); -} - /** * Retrieve translated string with gettext context. * From e28056c0940c8d52ab05c793d5e9aea302324cd6 Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Wed, 3 Jun 2020 14:00:50 +0200 Subject: [PATCH 07/18] Update redirect_canonical integration --- src/wp-includes/canonical.php | 15 ++++++++++----- tests/phpunit/tests/canonical/sitemaps.php | 6 ++++++ 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/src/wp-includes/canonical.php b/src/wp-includes/canonical.php index 5a1f3b814b2eb..befe58e684b72 100644 --- a/src/wp-includes/canonical.php +++ b/src/wp-includes/canonical.php @@ -509,6 +509,11 @@ function redirect_canonical( $requested_url = null, $do_redirect = true ) { $redirect['path'] = trailingslashit( $redirect['path'] ) . $addl_path; } + // Remove trailing slash for sitemaps requests. + if ( ! empty( get_query_var( 'sitemap' ) ) ) { + $redirect['path'] = untrailingslashit( $redirect['path'] ); + } + $redirect_url = $redirect['scheme'] . '://' . $redirect['host'] . $redirect['path']; } @@ -651,6 +656,11 @@ function redirect_canonical( $requested_url = null, $do_redirect = true ) { $redirect['path'] = trailingslashit( $redirect['path'] ); } + // Remove trailing slash for sitemaps requests. + if ( ! empty( get_query_var( 'sitemap' ) ) || ! empty( get_query_var( 'sitemap-stylesheet' ) ) ) { + $redirect['path'] = untrailingslashit( $redirect['path'] ); + } + // Strip multiple slashes out of the URL. if ( strpos( $redirect['path'], '//' ) > -1 ) { $redirect['path'] = preg_replace( '|/+|', '/', $redirect['path'] ); @@ -673,11 +683,6 @@ function redirect_canonical( $requested_url = null, $do_redirect = true ) { $redirect['host'] = $original['host']; } - // Remove trailing slash for sitemaps requests. - if ( get_query_var( 'sitemap' ) || get_query_var( 'sitemap-stylesheet' ) ) { - $redirect['path'] = untrailingslashit( $redirect['path'] ); - } - $compare_original = array( $original['host'], $original['path'] ); if ( ! empty( $original['port'] ) ) { diff --git a/tests/phpunit/tests/canonical/sitemaps.php b/tests/phpunit/tests/canonical/sitemaps.php index 4c26a080f973d..579247a33cd29 100644 --- a/tests/phpunit/tests/canonical/sitemaps.php +++ b/tests/phpunit/tests/canonical/sitemaps.php @@ -7,6 +7,12 @@ * @group sitemaps */ class Tests_Canonical_Sitemaps extends WP_Canonical_UnitTestCase { + public function setUp() { + parent::setUp(); + $wp_sitemaps = new WP_Sitemaps(); + $wp_sitemaps->init(); + } + public function test_remove_trailing_slashes_for_sitemap_index_requests() { $this->set_permalink_structure( '/%postname%/' ); $this->assertCanonical( '/wp-sitemap.xml', '/wp-sitemap.xml' ); From aba9a3595446c4f6ae8ee316c74efc14626651bc Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Wed, 3 Jun 2020 16:29:53 +0200 Subject: [PATCH 08/18] Minor fixes --- phpcs.xml.dist | 1 + src/wp-includes/default-constants.php | 1 - .../class-wp-sitemaps-empty-test-provider.php | 16 ++------- tests/phpunit/includes/testcase-xml.php | 4 +++ tests/phpunit/tests/formatting/EscXml.php | 4 +-- tests/phpunit/tests/sitemaps/functions.php | 4 +-- .../phpunit/tests/sitemaps/sitemaps-index.php | 6 ++-- .../tests/sitemaps/sitemaps-registry.php | 2 +- .../tests/sitemaps/sitemaps-renderer.php | 34 +++++++++---------- .../tests/sitemaps/sitemaps-taxonomies.php | 24 ++++++------- .../phpunit/tests/sitemaps/sitemaps-users.php | 4 +-- tests/phpunit/tests/sitemaps/sitemaps.php | 18 +++++----- 12 files changed, 56 insertions(+), 62 deletions(-) diff --git a/phpcs.xml.dist b/phpcs.xml.dist index 55924b04cd30e..7b804d3adb6d5 100644 --- a/phpcs.xml.dist +++ b/phpcs.xml.dist @@ -247,6 +247,7 @@ + diff --git a/src/wp-includes/default-constants.php b/src/wp-includes/default-constants.php index a2df8adef7665..98c8e48369f92 100644 --- a/src/wp-includes/default-constants.php +++ b/src/wp-includes/default-constants.php @@ -408,4 +408,3 @@ function wp_templating_constants() { } } - diff --git a/tests/phpunit/includes/class-wp-sitemaps-empty-test-provider.php b/tests/phpunit/includes/class-wp-sitemaps-empty-test-provider.php index 247eaefa88340..c772c3941c077 100644 --- a/tests/phpunit/includes/class-wp-sitemaps-empty-test-provider.php +++ b/tests/phpunit/includes/class-wp-sitemaps-empty-test-provider.php @@ -1,13 +1,13 @@ object_type = $object_type; } - /** - * Return the public post types, which excludes nav_items and similar types. - * Attachments are also excluded. This includes custom post types with public = true - * - * @return array Map of object subtype objects (WP_Post_Type) keyed by their name. - */ - public function get_object_subtypes() { - return array(); - } - /** * Gets a URL list for a sitemap. * diff --git a/tests/phpunit/includes/testcase-xml.php b/tests/phpunit/includes/testcase-xml.php index 6f64783bff470..c7b78b2e06ab2 100644 --- a/tests/phpunit/includes/testcase-xml.php +++ b/tests/phpunit/includes/testcase-xml.php @@ -40,6 +40,10 @@ public function loadXML( $xml, $options = 0 ) { * @return string The normalized form of `$xml`. */ public function normalizeXML( $xml, $options = 0 ) { + if ( ! class_exists( 'XSLTProcessor' ) ) { + $this->markTestSkipped( 'This test requires the XSL extension.' ); + } + static $xslt_proc; if ( ! $xslt_proc ) { diff --git a/tests/phpunit/tests/formatting/EscXml.php b/tests/phpunit/tests/formatting/EscXml.php index daa56ab0b289e..28ab84fc8a12a 100644 --- a/tests/phpunit/tests/formatting/EscXml.php +++ b/tests/phpunit/tests/formatting/EscXml.php @@ -13,7 +13,7 @@ class Tests_Formatting_EscXml extends WP_UnitTestCase { * @param string $expected The expected escaped value of `$source`. */ public function test_esc_xml_basics( $source, $expected ) { - $actual = esc_xml( $source ); + $actual = esc_xml( $source ); $this->assertEquals( $expected, $actual ); } @@ -67,7 +67,7 @@ public function test_escapes_html_named_entities() { } public function test_ignores_existing_entities() { - $source = '& £ " &'; + $source = '& £ " &'; // note that _wp_specialchars() strips leading 0's from numeric character references. $expected = '& £ " &'; $actual = esc_xml( $source ); diff --git a/tests/phpunit/tests/sitemaps/functions.php b/tests/phpunit/tests/sitemaps/functions.php index a0195f7e0f763..dd4796b4384ed 100644 --- a/tests/phpunit/tests/sitemaps/functions.php +++ b/tests/phpunit/tests/sitemaps/functions.php @@ -11,9 +11,9 @@ public function test_wp_sitemaps_get_max_urls() { // Apply a filter to test filterable values. add_filter( 'wp_sitemaps_max_urls', array( $this, '_filter_max_url_value' ), 10, 2 ); - $expected_posts = wp_sitemaps_get_max_urls( 'post' ); + $expected_posts = wp_sitemaps_get_max_urls( 'post' ); $expected_taxonomies = wp_sitemaps_get_max_urls( 'term' ); - $expected_users = wp_sitemaps_get_max_urls( 'user' ); + $expected_users = wp_sitemaps_get_max_urls( 'user' ); $this->assertEquals( $expected_posts, 300, 'Can not confirm max URL number for posts.' ); $this->assertEquals( $expected_taxonomies, 50, 'Can not confirm max URL number for taxonomies.' ); diff --git a/tests/phpunit/tests/sitemaps/sitemaps-index.php b/tests/phpunit/tests/sitemaps/sitemaps-index.php index 89f64078e3735..5df604af8a6d4 100644 --- a/tests/phpunit/tests/sitemaps/sitemaps-index.php +++ b/tests/phpunit/tests/sitemaps/sitemaps-index.php @@ -23,7 +23,7 @@ public function test_get_sitemap_list() { public function test_get_sitemap_list_no_entries() { $registry = new WP_Sitemaps_Registry(); - $registry->add_sitemap( 'foo', new Core_WP_Empty_Test_Provider( 'foo' ) ); + $registry->add_sitemap( 'foo', new WP_Sitemaps_Empty_Test_Provider( 'foo' ) ); $sitemap_index = new WP_Sitemaps_Index( $registry ); $this->assertCount( 0, $sitemap_index->get_sitemap_list() ); @@ -31,7 +31,7 @@ public function test_get_sitemap_list_no_entries() { public function test_get_index_url() { $sitemap_index = new WP_Sitemaps_Index( new WP_Sitemaps_Registry() ); - $index_url = $sitemap_index->get_index_url(); + $index_url = $sitemap_index->get_index_url(); $this->assertStringEndsWith( '/?sitemap=index', $index_url ); } @@ -41,7 +41,7 @@ public function test_get_index_url_pretty_permalinks() { $this->set_permalink_structure( '/%year%/%postname%/' ); $sitemap_index = new WP_Sitemaps_Index( new WP_Sitemaps_Registry() ); - $index_url = $sitemap_index->get_index_url(); + $index_url = $sitemap_index->get_index_url(); // Clean up permalinks. $this->set_permalink_structure(); diff --git a/tests/phpunit/tests/sitemaps/sitemaps-registry.php b/tests/phpunit/tests/sitemaps/sitemaps-registry.php index 1b231945b23c9..3baffaa9f0846 100644 --- a/tests/phpunit/tests/sitemaps/sitemaps-registry.php +++ b/tests/phpunit/tests/sitemaps/sitemaps-registry.php @@ -19,7 +19,7 @@ public function test_add_sitemap() { public function test_add_sitemap_prevent_duplicates() { $provider1 = new WP_Sitemaps_Test_Provider(); $provider2 = new WP_Sitemaps_Test_Provider(); - $registry = new WP_Sitemaps_Registry(); + $registry = new WP_Sitemaps_Registry(); $actual1 = $registry->add_sitemap( 'foo', $provider1 ); $actual2 = $registry->add_sitemap( 'foo', $provider2 ); diff --git a/tests/phpunit/tests/sitemaps/sitemaps-renderer.php b/tests/phpunit/tests/sitemaps/sitemaps-renderer.php index f9f4eeddd627f..c4be3dee73c16 100644 --- a/tests/phpunit/tests/sitemaps/sitemaps-renderer.php +++ b/tests/phpunit/tests/sitemaps/sitemaps-renderer.php @@ -88,7 +88,7 @@ public function test_get_sitemap_index_xml() { public function test_get_sitemap_index_xml_without_stylsheet() { $entries = array( array( - 'loc' => 'http://' . WP_TESTS_DOMAIN . '/wp-sitemap-posts-post-1.xml', + 'loc' => 'http://' . WP_TESTS_DOMAIN . '/wp-sitemap-posts-post-1.xml', ), ); @@ -96,8 +96,8 @@ public function test_get_sitemap_index_xml_without_stylsheet() { $renderer = new WP_Sitemaps_Renderer(); - $xml_dom = $this->loadXML( $renderer->get_sitemap_index_xml( $entries ) ); - $xpath = new DOMXPath( $xml_dom ); + $xml_dom = $this->loadXML( $renderer->get_sitemap_index_xml( $entries ) ); + $xpath = new DOMXPath( $xml_dom ); $this->assertSame( 0, @@ -112,19 +112,19 @@ public function test_get_sitemap_index_xml_without_stylsheet() { public function test_get_sitemap_xml() { $url_list = array( array( - 'loc' => 'http://' . WP_TESTS_DOMAIN . '/2019/10/post-1', + 'loc' => 'http://' . WP_TESTS_DOMAIN . '/2019/10/post-1', ), array( - 'loc' => 'http://' . WP_TESTS_DOMAIN . '/2019/10/post-2', + 'loc' => 'http://' . WP_TESTS_DOMAIN . '/2019/10/post-2', ), array( - 'loc' => 'http://' . WP_TESTS_DOMAIN . '/2019/10/post-3', + 'loc' => 'http://' . WP_TESTS_DOMAIN . '/2019/10/post-3', ), array( - 'loc' => 'http://' . WP_TESTS_DOMAIN . '/2019/10/post-4', + 'loc' => 'http://' . WP_TESTS_DOMAIN . '/2019/10/post-4', ), array( - 'loc' => 'http://' . WP_TESTS_DOMAIN . '/2019/10/post-5', + 'loc' => 'http://' . WP_TESTS_DOMAIN . '/2019/10/post-5', ), ); @@ -150,7 +150,7 @@ public function test_get_sitemap_xml() { public function test_get_sitemap_xml_without_stylsheet() { $url_list = array( array( - 'loc' => 'http://' . WP_TESTS_DOMAIN . '/2019/10/post-1', + 'loc' => 'http://' . WP_TESTS_DOMAIN . '/2019/10/post-1', ), ); @@ -158,8 +158,8 @@ public function test_get_sitemap_xml_without_stylsheet() { $renderer = new WP_Sitemaps_Renderer(); - $xml_dom = $this->loadXML( $renderer->get_sitemap_xml( $url_list ) ); - $xpath = new DOMXPath( $xml_dom ); + $xml_dom = $this->loadXML( $renderer->get_sitemap_xml( $url_list ) ); + $xpath = new DOMXPath( $xml_dom ); $this->assertSame( 0, @@ -180,14 +180,14 @@ public function test_get_sitemap_xml_without_stylsheet() { public function test_get_sitemap_xml_extra_elements() { $url_list = array( array( - 'loc' => 'http://' . WP_TESTS_DOMAIN . '/2019/10/post-1', - 'string' => 'value', - 'number' => 200, + 'loc' => 'http://' . WP_TESTS_DOMAIN . '/2019/10/post-1', + 'string' => 'value', + 'number' => 200, ), array( - 'loc' => 'http://' . WP_TESTS_DOMAIN . '/2019/10/post-2', - 'string' => 'another value', - 'number' => 300, + 'loc' => 'http://' . WP_TESTS_DOMAIN . '/2019/10/post-2', + 'string' => 'another value', + 'number' => 300, ), ); diff --git a/tests/phpunit/tests/sitemaps/sitemaps-taxonomies.php b/tests/phpunit/tests/sitemaps/sitemaps-taxonomies.php index 35077e26c58a4..044e868e74ed2 100644 --- a/tests/phpunit/tests/sitemaps/sitemaps-taxonomies.php +++ b/tests/phpunit/tests/sitemaps/sitemaps-taxonomies.php @@ -31,9 +31,9 @@ class Test_WP_Sitemaps_Taxonomies extends WP_UnitTestCase { * @param WP_UnitTest_Factory $factory A WP_UnitTest_Factory object. */ public static function wpSetUpBeforeClass( $factory ) { - self::$cats = $factory->term->create_many( 10, array( 'taxonomy' => 'category' ) ); + self::$cats = $factory->term->create_many( 10, array( 'taxonomy' => 'category' ) ); self::$post_tags = $factory->term->create_many( 10 ); - self::$editor_id = $factory->user->create( array( 'role' => 'editor' ) ); + self::$editor_id = $factory->user->create( array( 'role' => 'editor' ) ); } /** @@ -47,19 +47,19 @@ public function test_get_url_list_taxonomies() { // Create a test post to calculate update times. $post = self::factory()->post->create_and_get( array( - 'tags_input' => self::$post_tags, + 'tags_input' => self::$post_tags, 'post_category' => $categories, ) ); $tax_provider = new WP_Sitemaps_Taxonomies(); - $cat_list = $tax_provider->get_url_list( 1, 'category' ); + $cat_list = $tax_provider->get_url_list( 1, 'category' ); $expected_cats = array_map( static function ( $id ) use ( $post ) { return array( - 'loc' => get_term_link( $id, 'category' ), + 'loc' => get_term_link( $id, 'category' ), ); }, $categories @@ -72,7 +72,7 @@ static function ( $id ) use ( $post ) { $expected_tags = array_map( static function ( $id ) use ( $post ) { return array( - 'loc' => get_term_link( $id, 'post_tag' ), + 'loc' => get_term_link( $id, 'post_tag' ), ); }, self::$post_tags @@ -93,7 +93,7 @@ public function test_get_url_list_custom_taxonomy() { register_taxonomy( $taxonomy, 'post' ); // Create test terms in the custom taxonomy. - $terms = self::factory()->term->create_many( 10, array( 'taxonomy' => $taxonomy ) ); + $terms = self::factory()->term->create_many( 10, array( 'taxonomy' => $taxonomy ) ); // Create a test post applied to all test terms. $post = self::factory()->post->create_and_get( array( 'tax_input' => array( $taxonomy => $terms ) ) ); @@ -101,7 +101,7 @@ public function test_get_url_list_custom_taxonomy() { $expected = array_map( static function ( $id ) use ( $taxonomy, $post ) { return array( - 'loc' => get_term_link( $id, $taxonomy ), + 'loc' => get_term_link( $id, $taxonomy ), ); }, $terms @@ -127,7 +127,7 @@ public function test_get_url_list_custom_taxonomy_private() { register_taxonomy( $taxonomy, 'post', array( 'public' => false ) ); // Create test terms in the custom taxonomy. - $terms = self::factory()->term->create_many( 10, array( 'taxonomy' => $taxonomy ) ); + $terms = self::factory()->term->create_many( 10, array( 'taxonomy' => $taxonomy ) ); // Create a test post applied to all test terms. self::factory()->post->create( array( 'tax_input' => array( $taxonomy => $terms ) ) ); @@ -153,8 +153,8 @@ public function test_get_sitemap_entries_custom_taxonomies() { register_taxonomy( 'private_taxonomy', 'post', array( 'public' => false ) ); // Create test terms in the custom taxonomy. - $public_term = self::factory()->term->create( array( 'taxonomy' => 'public_taxonomy' ) ); - $private_term = self::factory()->term->create( array( 'taxonomy' => 'private_taxonomy' ) ); + $public_term = self::factory()->term->create( array( 'taxonomy' => 'public_taxonomy' ) ); + $private_term = self::factory()->term->create( array( 'taxonomy' => 'private_taxonomy' ) ); // Create a test post applied to all test terms. self::factory()->post->create_and_get( @@ -167,7 +167,7 @@ public function test_get_sitemap_entries_custom_taxonomies() { ); $tax_provider = new WP_Sitemaps_Taxonomies(); - $entries = wp_list_pluck( $tax_provider->get_sitemap_entries(), 'loc' ); + $entries = wp_list_pluck( $tax_provider->get_sitemap_entries(), 'loc' ); // Clean up. unregister_taxonomy_for_object_type( 'public_taxonomy', 'post' ); diff --git a/tests/phpunit/tests/sitemaps/sitemaps-users.php b/tests/phpunit/tests/sitemaps/sitemaps-users.php index 5c49b8bd9f51e..975462044cf63 100644 --- a/tests/phpunit/tests/sitemaps/sitemaps-users.php +++ b/tests/phpunit/tests/sitemaps/sitemaps-users.php @@ -24,7 +24,7 @@ class Test_WP_Sitemaps_Users extends WP_UnitTestCase { * @param WP_UnitTest_Factory $factory A WP_UnitTest_Factory object. */ public static function wpSetUpBeforeClass( $factory ) { - self::$users = $factory->user->create_many( 10, array( 'role' => 'editor' ) ); + self::$users = $factory->user->create_many( 10, array( 'role' => 'editor' ) ); self::$editor_id = self::$users[0]; } @@ -42,7 +42,7 @@ static function ( $user_id ) { $post = self::factory()->post->create_and_get( array( 'post_author' => $user_id ) ); return array( - 'loc' => get_author_posts_url( $user_id ), + 'loc' => get_author_posts_url( $user_id ), ); }, self::$users diff --git a/tests/phpunit/tests/sitemaps/sitemaps.php b/tests/phpunit/tests/sitemaps/sitemaps.php index 1ec79152326de..16a259c298646 100644 --- a/tests/phpunit/tests/sitemaps/sitemaps.php +++ b/tests/phpunit/tests/sitemaps/sitemaps.php @@ -74,20 +74,20 @@ class Test_Sitemaps extends WP_UnitTestCase { public static function wpSetUpBeforeClass( $factory ) { self::$users = $factory->user->create_many( 10 ); self::$post_tags = $factory->term->create_many( 10 ); - self::$cats = $factory->term->create_many( 10, array( 'taxonomy' => 'category' ) ); + self::$cats = $factory->term->create_many( 10, array( 'taxonomy' => 'category' ) ); self::$pages = $factory->post->create_many( 10, array( 'post_type' => 'page' ) ); // Create a set of posts pre-assigned to tags and authors. self::$posts = $factory->post->create_many( 10, array( - 'tags_input' => self::$post_tags, + 'tags_input' => self::$post_tags, 'post_author' => reset( self::$users ), ) ); // Create a user with an editor role to complete some tests. - self::$editor_id = $factory->user->create( array( 'role' => 'editor' ) ); + self::$editor_id = $factory->user->create( array( 'role' => 'editor' ) ); self::$test_provider = new WP_Sitemaps_Test_Provider(); } @@ -234,7 +234,7 @@ public function test_get_url_list_page_with_home() { array_unshift( $expected, array( - 'loc' => home_url(), + 'loc' => home_url(), ) ); @@ -327,7 +327,7 @@ public function _get_expected_url_list( $type, $ids ) { return array_map( static function ( $post ) { return array( - 'loc' => get_permalink( $post ), + 'loc' => get_permalink( $post ), ); }, $posts @@ -350,7 +350,7 @@ public function test_register_sitemap_provider() { */ public function test_robots_text() { // Get the text added to the default robots text output. - $robots_text = apply_filters( 'robots_txt', '', true ); + $robots_text = apply_filters( 'robots_txt', '', true ); $sitemap_string = 'Sitemap: http://' . WP_TESTS_DOMAIN . '/?sitemap=index'; $this->assertContains( $sitemap_string, $robots_text, 'Sitemap URL not included in robots text.' ); @@ -360,7 +360,7 @@ public function test_robots_text() { * Test robots.txt output for a private site. */ public function test_robots_text_private_site() { - $robots_text = apply_filters( 'robots_txt', '', false ); + $robots_text = apply_filters( 'robots_txt', '', false ); $sitemap_string = 'Sitemap: http://' . WP_TESTS_DOMAIN . '/?sitemap=index'; $this->assertNotContains( $sitemap_string, $robots_text ); @@ -374,7 +374,7 @@ public function test_robots_text_with_permalinks() { $this->set_permalink_structure( '/%year%/%postname%/' ); // Get the text added to the default robots text output. - $robots_text = apply_filters( 'robots_txt', '', true ); + $robots_text = apply_filters( 'robots_txt', '', true ); $sitemap_string = 'Sitemap: http://' . WP_TESTS_DOMAIN . '/wp-sitemap.xml'; // Clean up permalinks. @@ -388,7 +388,7 @@ public function test_robots_text_with_permalinks() { */ public function test_robots_text_prefixed_with_line_feed() { // Get the text added to the default robots text output. - $robots_text = apply_filters( 'robots_txt', '', true ); + $robots_text = apply_filters( 'robots_txt', '', true ); $sitemap_string = "\nSitemap: "; $this->assertContains( $sitemap_string, $robots_text, 'Sitemap URL not prefixed with "\n".' ); From 3aed523d9ae7be3c3d85d12d73afa5159cf99f94 Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Wed, 3 Jun 2020 18:51:33 +0200 Subject: [PATCH 09/18] Update from plugin --- .../sitemaps/class-wp-sitemaps-provider.php | 19 ++++- .../sitemaps/class-wp-sitemaps-renderer.php | 22 ++++- .../sitemaps/class-wp-sitemaps-stylesheet.php | 13 +++ .../sitemaps/class-wp-sitemaps.php | 2 +- .../providers/class-wp-sitemaps-posts.php | 8 +- .../class-wp-sitemaps-taxonomies.php | 4 +- .../providers/class-wp-sitemaps-users.php | 10 +-- .../tests/sitemaps/sitemaps-renderer.php | 85 ++++++++++++++++++- 8 files changed, 143 insertions(+), 20 deletions(-) diff --git a/src/wp-includes/sitemaps/class-wp-sitemaps-provider.php b/src/wp-includes/sitemaps/class-wp-sitemaps-provider.php index 5ef34fb463bb9..02dcacce1235b 100644 --- a/src/wp-includes/sitemaps/class-wp-sitemaps-provider.php +++ b/src/wp-includes/sitemaps/class-wp-sitemaps-provider.php @@ -109,9 +109,24 @@ public function get_sitemap_entries() { foreach ( $sitemap_types as $type ) { for ( $page = 1; $page <= $type['pages']; $page ++ ) { $loc = $this->get_sitemap_url( $type['name'], $page ); - $sitemaps[] = array( - 'loc' => $loc, + $sitemap_entry = array( + 'loc' => $this->get_sitemap_url( $type['name'], $page ), ); + + /** + * Filters the sitemap entry for the sitemap index. + * + * @since 5.5.0 + * + * @param array $sitemap_entry Sitemap entry for the post. + * @param string $object_type Object empty name. + * @param string $object_subtype Object subtype name. + * Empty string if the object type does not support subtypes. + * @param string $page Page of results. + */ + $sitemap_entry = apply_filters( 'wp_sitemaps_index_entry', $sitemap_entry, $this->object_type, $type['name'], $page ); + + $sitemaps[] = $sitemap_entry; } } diff --git a/src/wp-includes/sitemaps/class-wp-sitemaps-renderer.php b/src/wp-includes/sitemaps/class-wp-sitemaps-renderer.php index 59e2025473ddb..91734866d190a 100644 --- a/src/wp-includes/sitemaps/class-wp-sitemaps-renderer.php +++ b/src/wp-includes/sitemaps/class-wp-sitemaps-renderer.php @@ -150,7 +150,25 @@ public function get_sitemap_index_xml( $sitemaps ) { foreach ( $sitemaps as $entry ) { $sitemap = $sitemap_index->addChild( 'sitemap' ); - $sitemap->addChild( 'loc', esc_url( $entry['loc'] ) ); + + // Add each element as a child node to the entry. + foreach ( $entry as $name => $value ) { + if ( 'loc' === $name ) { + $sitemap->addChild( $name, esc_url( $value ) ); + } elseif ( 'lastmod' === $name ) { + $sitemap->addChild( $name, esc_attr( $value ) ); + } else { + _doing_it_wrong( + __METHOD__, + /* translators: %s: list of element names */ + sprintf( + __( 'Fields other than %s are not currently supported for the sitemap index.' ), + implode( ',', array( 'loc', 'lastmod' ) ) + ), + '5.5.0' + ); + } + } } return $sitemap_index->asXML(); @@ -198,7 +216,7 @@ public function get_sitemap_xml( $url_list ) { foreach ( $url_list as $url_item ) { $url = $urlset->addChild( 'url' ); - // Add each element as a child node to the URL entry. + // Add each element as a child node to the entry. foreach ( $url_item as $name => $value ) { if ( 'loc' === $name ) { $url->addChild( $name, esc_url( $value ) ); diff --git a/src/wp-includes/sitemaps/class-wp-sitemaps-stylesheet.php b/src/wp-includes/sitemaps/class-wp-sitemaps-stylesheet.php index 191d0bba0d5db..7cbbb89112534 100644 --- a/src/wp-includes/sitemaps/class-wp-sitemaps-stylesheet.php +++ b/src/wp-includes/sitemaps/class-wp-sitemaps-stylesheet.php @@ -163,6 +163,7 @@ public function get_sitemap_index_stylesheet() { ); $lang = get_language_attributes( 'html' ); $url = esc_html__( 'URL' ); + $lastmod = esc_html__( 'Last Modified' ); $xsl_content = << @@ -174,6 +175,12 @@ public function get_sitemap_index_stylesheet() { > + + + @@ -192,12 +199,18 @@ public function get_sitemap_index_stylesheet() { {$url} + + {$lastmod} + + + + diff --git a/src/wp-includes/sitemaps/class-wp-sitemaps.php b/src/wp-includes/sitemaps/class-wp-sitemaps.php index 73137fa7f6041..6c6a795af8c64 100644 --- a/src/wp-includes/sitemaps/class-wp-sitemaps.php +++ b/src/wp-includes/sitemaps/class-wp-sitemaps.php @@ -81,7 +81,7 @@ public function register_sitemaps() { * @since 5.5.0 * * @param array $providers { - * Array of WP_Sitemap_Provider objects keyed by their name. + * Array of WP_Sitemaps_Provider objects keyed by their name. * * @type object $posts The WP_Sitemaps_Posts object. * @type object $taxonomies The WP_Sitemaps_Taxonomies object. diff --git a/src/wp-includes/sitemaps/providers/class-wp-sitemaps-posts.php b/src/wp-includes/sitemaps/providers/class-wp-sitemaps-posts.php index b8095c4ab3c9c..b3d7b592d654d 100644 --- a/src/wp-includes/sitemaps/providers/class-wp-sitemaps-posts.php +++ b/src/wp-includes/sitemaps/providers/class-wp-sitemaps-posts.php @@ -87,7 +87,7 @@ public function get_url_list( $page_num, $post_type = '' ) { return $url_list; } - $args = $this->get_posts_query_args( $post_type ); + $args = $this->get_posts_query_args( $post_type ); $args['paged'] = $page_num; $query = new WP_Query( $args ); @@ -127,7 +127,7 @@ public function get_url_list( $page_num, $post_type = '' ) { * @param string $post_type Name of the post_type. */ $sitemap_entry = apply_filters( 'wp_sitemaps_posts_entry', $sitemap_entry, $post, $post_type ); - $url_list[] = $sitemap_entry; + $url_list[] = $sitemap_entry; } return $url_list; @@ -163,8 +163,8 @@ public function get_max_num_pages( $post_type = '' ) { return $max_num_pages; } - $args = $this->get_posts_query_args( $post_type ); - $args['fields'] = 'ids'; + $args = $this->get_posts_query_args( $post_type ); + $args['fields'] = 'ids'; $args['no_found_rows'] = false; $query = new WP_Query( $args ); diff --git a/src/wp-includes/sitemaps/providers/class-wp-sitemaps-taxonomies.php b/src/wp-includes/sitemaps/providers/class-wp-sitemaps-taxonomies.php index ca49b9f433fcf..45c65145b296e 100644 --- a/src/wp-includes/sitemaps/providers/class-wp-sitemaps-taxonomies.php +++ b/src/wp-includes/sitemaps/providers/class-wp-sitemaps-taxonomies.php @@ -90,7 +90,7 @@ public function get_url_list( $page_num, $taxonomy = '' ) { // Offset by how many terms should be included in previous pages. $offset = ( $page_num - 1 ) * wp_sitemaps_get_max_urls( $this->object_type ); - $args = $this->get_taxonomies_query_args( $taxonomy ); + $args = $this->get_taxonomies_query_args( $taxonomy ); $args['offset'] = $offset; $taxonomy_terms = new WP_Term_Query( $args ); @@ -111,7 +111,7 @@ public function get_url_list( $page_num, $taxonomy = '' ) { * @param string $taxonomy Taxonomy name. */ $sitemap_entry = apply_filters( 'wp_sitemaps_taxonomies_entry', $sitemap_entry, $term, $taxonomy ); - $url_list[] = $sitemap_entry; + $url_list[] = $sitemap_entry; } } diff --git a/src/wp-includes/sitemaps/providers/class-wp-sitemaps-users.php b/src/wp-includes/sitemaps/providers/class-wp-sitemaps-users.php index 7c5195b4672b6..1be9a81db1445 100644 --- a/src/wp-includes/sitemaps/providers/class-wp-sitemaps-users.php +++ b/src/wp-includes/sitemaps/providers/class-wp-sitemaps-users.php @@ -58,16 +58,16 @@ public function get_url_list( $page_num, $object_subtype = '' ) { return $url_list; } - $args = $this->get_users_query_args(); + $args = $this->get_users_query_args(); $args['paged'] = $page_num; - $query = new WP_User_Query( $args ); + $query = new WP_User_Query( $args ); $users = $query->get_results(); $url_list = array(); foreach ( $users as $user ) { $sitemap_entry = array( - 'loc' => get_author_posts_url( $user->ID ), + 'loc' => get_author_posts_url( $user->ID ), ); /** @@ -79,7 +79,7 @@ public function get_url_list( $page_num, $object_subtype = '' ) { * @param WP_User $user User object. */ $sitemap_entry = apply_filters( 'wp_sitemaps_users_entry', $sitemap_entry, $user ); - $url_list[] = $sitemap_entry; + $url_list[] = $sitemap_entry; } return $url_list; @@ -114,7 +114,7 @@ public function get_max_num_pages( $object_subtype = '' ) { return $max_num_pages; } - $args = $this->get_users_query_args(); + $args = $this->get_users_query_args(); $query = new WP_User_Query( $args ); $total_users = $query->get_total(); diff --git a/tests/phpunit/tests/sitemaps/sitemaps-renderer.php b/tests/phpunit/tests/sitemaps/sitemaps-renderer.php index c4be3dee73c16..f2ee1de1c7607 100644 --- a/tests/phpunit/tests/sitemaps/sitemaps-renderer.php +++ b/tests/phpunit/tests/sitemaps/sitemaps-renderer.php @@ -82,10 +82,87 @@ public function test_get_sitemap_index_xml() { $this->assertXMLEquals( $expected, $actual, 'Sitemap index markup incorrect.' ); } + /** + * Test XML output for the sitemap index renderer with lastmod attributes. + */ + public function test_get_sitemap_index_xml_with_lastmod() { + $entries = array( + array( + 'loc' => 'http://' . WP_TESTS_DOMAIN . '/wp-sitemap-posts-post-1.xml', + 'lastmod' => '2005-01-01', + ), + array( + 'loc' => 'http://' . WP_TESTS_DOMAIN . '/wp-sitemap-posts-page-1.xml', + 'lastmod' => '2005-01-01', + ), + array( + 'loc' => 'http://' . WP_TESTS_DOMAIN . '/wp-sitemap-taxonomies-category-1.xml', + 'lastmod' => '2005-01-01', + ), + array( + 'loc' => 'http://' . WP_TESTS_DOMAIN . '/wp-sitemap-taxonomies-post_tag-1.xml', + 'lastmod' => '2005-01-01', + ), + array( + 'loc' => 'http://' . WP_TESTS_DOMAIN . '/wp-sitemap-users-1.xml', + 'lastmod' => '2005-01-01', + ), + ); + + $renderer = new WP_Sitemaps_Renderer(); + + $actual = $renderer->get_sitemap_index_xml( $entries ); + $expected = '' . + '' . + '' . + 'http://' . WP_TESTS_DOMAIN . '/wp-sitemap-posts-post-1.xml2005-01-01' . + 'http://' . WP_TESTS_DOMAIN . '/wp-sitemap-posts-page-1.xml2005-01-01' . + 'http://' . WP_TESTS_DOMAIN . '/wp-sitemap-taxonomies-category-1.xml2005-01-01' . + 'http://' . WP_TESTS_DOMAIN . '/wp-sitemap-taxonomies-post_tag-1.xml2005-01-01' . + 'http://' . WP_TESTS_DOMAIN . '/wp-sitemap-users-1.xml2005-01-01' . + ''; + + $this->assertXMLEquals( $expected, $actual, 'Sitemap index markup incorrect.' ); + } + + /** + * Test that all children of Q{http://www.sitemaps.org/schemas/sitemap/0.9}sitemap in the + * rendered index XML are defined in the Sitemaps spec (i.e., loc, lastmod). + * + * Note that when a means of adding elements in extension namespaces is settled on, + * this test will need to be updated accordingly. + * + * @expectedIncorrectUsage WP_Sitemaps_Renderer::get_sitemap_index_xml + */ + public function test_get_sitemap_index_xml_extra_elements() { + $url_list = array( + array( + 'loc' => 'http://' . WP_TESTS_DOMAIN . '/wp-sitemap-posts-post-1.xml', + 'unknown' => 'this is a test', + ), + array( + 'loc' => 'http://' . WP_TESTS_DOMAIN . '/wp-sitemap-posts-page-1.xml', + 'unknown' => 'that was a test', + ), + ); + + $renderer = new WP_Sitemaps_Renderer(); + + $xml_dom = $this->loadXML( $renderer->get_sitemap_index_xml( $url_list ) ); + $xpath = new DOMXPath( $xml_dom ); + $xpath->registerNamespace( 'sitemap', 'http://www.sitemaps.org/schemas/sitemap/0.9' ); + + $this->assertEquals( + 0, + $xpath->evaluate( "count( /sitemap:sitemapindex/sitemap:sitemap/*[ namespace-uri() != 'http://www.sitemaps.org/schemas/sitemap/0.9' or not( local-name() = 'loc' or local-name() = 'lastmod' ) ] )" ), + 'Invalid child of "sitemap:sitemap" in rendered index XML.' + ); + } + /** * Test XML output for the sitemap index renderer when stylesheet is disabled. */ - public function test_get_sitemap_index_xml_without_stylsheet() { + public function test_get_sitemap_index_xml_without_stylesheet() { $entries = array( array( 'loc' => 'http://' . WP_TESTS_DOMAIN . '/wp-sitemap-posts-post-1.xml', @@ -147,7 +224,7 @@ public function test_get_sitemap_xml() { /** * Test XML output for the sitemap page renderer when stylesheet is disabled. */ - public function test_get_sitemap_xml_without_stylsheet() { + public function test_get_sitemap_xml_without_stylesheet() { $url_list = array( array( 'loc' => 'http://' . WP_TESTS_DOMAIN . '/2019/10/post-1', @@ -169,8 +246,8 @@ public function test_get_sitemap_xml_without_stylsheet() { } /** - * Test that all children of Q{http://www.sitemaps.org/schemas/sitemap/0.9}url in the rendered XML - * defined in the Sitemaps spec (i.e., loc, lastmod, changefreq, priority). + * Test that all children of Q{http://www.sitemaps.org/schemas/sitemap/0.9}url in the + * rendered sitemap XML are defined in the Sitemaps spec (i.e., loc, lastmod, changefreq, priority). * * Note that when a means of adding elements in extension namespaces is settled on, * this test will need to be updated accordingly. From 7e8e4d3db4c4c36a758bba92e5c1404aee758f7d Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Thu, 4 Jun 2020 13:07:59 +0200 Subject: [PATCH 10/18] Add type hints --- src/wp-includes/sitemaps.php | 2 +- src/wp-includes/sitemaps/class-wp-sitemaps-index.php | 2 +- src/wp-includes/sitemaps/class-wp-sitemaps-registry.php | 6 +----- 3 files changed, 3 insertions(+), 7 deletions(-) diff --git a/src/wp-includes/sitemaps.php b/src/wp-includes/sitemaps.php index 542340b05e65c..6c927632703c7 100644 --- a/src/wp-includes/sitemaps.php +++ b/src/wp-includes/sitemaps.php @@ -88,7 +88,7 @@ function wp_get_sitemaps() { * @param WP_Sitemaps_Provider $provider The `Sitemaps_Provider` instance implementing the sitemap. * @return bool Returns true if the sitemap was added. False on failure. */ -function wp_register_sitemap( $name, $provider ) { +function wp_register_sitemap( $name, WP_Sitemaps_Provider $provider ) { $sitemaps = wp_sitemaps_get_server(); if ( ! $sitemaps ) { diff --git a/src/wp-includes/sitemaps/class-wp-sitemaps-index.php b/src/wp-includes/sitemaps/class-wp-sitemaps-index.php index 98c2135e7768a..7745ad630bb79 100644 --- a/src/wp-includes/sitemaps/class-wp-sitemaps-index.php +++ b/src/wp-includes/sitemaps/class-wp-sitemaps-index.php @@ -32,7 +32,7 @@ class WP_Sitemaps_Index { * * @param WP_Sitemaps_Registry $registry Sitemap provider registry. */ - public function __construct( $registry ) { + public function __construct( WP_Sitemaps_Registry $registry ) { $this->registry = $registry; } diff --git a/src/wp-includes/sitemaps/class-wp-sitemaps-registry.php b/src/wp-includes/sitemaps/class-wp-sitemaps-registry.php index 8870e40adb5ad..fe1bad398b2d5 100644 --- a/src/wp-includes/sitemaps/class-wp-sitemaps-registry.php +++ b/src/wp-includes/sitemaps/class-wp-sitemaps-registry.php @@ -42,15 +42,11 @@ class WP_Sitemaps_Registry { * @param WP_Sitemaps_Provider $provider Instance of a WP_Sitemaps_Provider. * @return bool True if the sitemap was added, false if it is already registered. */ - public function add_sitemap( $name, $provider ) { + public function add_sitemap( $name, WP_Sitemaps_Provider $provider ) { if ( isset( $this->sitemaps[ $name ] ) ) { return false; } - if ( ! $provider instanceof WP_Sitemaps_Provider ) { - return false; - } - $this->sitemaps[ $name ] = $provider; return true; From b9b5bc22466af082ccb7b3009a3ccdc6507d56de Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Thu, 4 Jun 2020 13:20:46 +0200 Subject: [PATCH 11/18] Downstream changes --- .../sitemaps/class-wp-sitemaps-renderer.php | 6 +- .../sitemaps/class-wp-sitemaps-stylesheet.php | 57 +++++++++++-------- .../tests/sitemaps/sitemaps-registry.php | 11 ---- .../tests/sitemaps/sitemaps-stylesheet.php | 4 +- 4 files changed, 37 insertions(+), 41 deletions(-) diff --git a/src/wp-includes/sitemaps/class-wp-sitemaps-renderer.php b/src/wp-includes/sitemaps/class-wp-sitemaps-renderer.php index 91734866d190a..7af96eb3e887b 100644 --- a/src/wp-includes/sitemaps/class-wp-sitemaps-renderer.php +++ b/src/wp-includes/sitemaps/class-wp-sitemaps-renderer.php @@ -156,7 +156,7 @@ public function get_sitemap_index_xml( $sitemaps ) { if ( 'loc' === $name ) { $sitemap->addChild( $name, esc_url( $value ) ); } elseif ( 'lastmod' === $name ) { - $sitemap->addChild( $name, esc_attr( $value ) ); + $sitemap->addChild( $name, esc_xml( $value ) ); } else { _doing_it_wrong( __METHOD__, @@ -221,7 +221,7 @@ public function get_sitemap_xml( $url_list ) { if ( 'loc' === $name ) { $url->addChild( $name, esc_url( $value ) ); } elseif ( in_array( $name, array( 'lastmod', 'changefreq', 'priority' ), true ) ) { - $url->addChild( $name, esc_attr( $value ) ); + $url->addChild( $name, esc_xml( $value ) ); } else { _doing_it_wrong( __METHOD__, @@ -255,7 +255,7 @@ static function () { wp_die( sprintf( - /* translators: %s: SimpleXML */ + /* translators: %s: SimpleXML */ __( 'Could not generate XML sitemap due to missing %s extension' ), 'SimpleXML' ), diff --git a/src/wp-includes/sitemaps/class-wp-sitemaps-stylesheet.php b/src/wp-includes/sitemaps/class-wp-sitemaps-stylesheet.php index 7cbbb89112534..3a80a7e2cdfc5 100644 --- a/src/wp-includes/sitemaps/class-wp-sitemaps-stylesheet.php +++ b/src/wp-includes/sitemaps/class-wp-sitemaps-stylesheet.php @@ -43,23 +43,25 @@ public function render_stylesheet( $type ) { */ public function get_sitemap_stylesheet() { $css = $this->get_stylesheet_css(); - $title = esc_html__( 'XML Sitemap' ); - $description = sprintf( - /* translators: %s: URL to sitemaps documentation. */ - __( 'This XML Sitemap is generated by WordPress to make your content more visible for search engines. Learn more about XML sitemaps on sitemaps.org.' ), - __( 'https://www.sitemaps.org/' ) + $title = esc_xml( __( 'XML Sitemap' ) ); + $description = esc_xml( __( 'This XML Sitemap is generated by WordPress to make your content more visible for search engines.' ) ); + $learn_more = sprintf( + '%s', + esc_url( __( 'https://www.sitemaps.org/' ) ), + esc_xml( __( 'Learn more about XML sitemaps.' ) ) ); - $text = sprintf( + + $text = sprintf( /* translators: %s: number of URLs. */ - __( 'Number of URLs in this XML Sitemap: %s.' ), + esc_xml( __( 'Number of URLs in this XML Sitemap: %s.' ) ), '' ); $lang = get_language_attributes( 'html' ); - $url = esc_html__( 'URL' ); - $lastmod = esc_html__( 'Last Modified' ); - $changefreq = esc_html__( 'Change Frequency' ); - $priority = esc_html__( 'Priority' ); + $url = esc_xml( __( 'URL' ) ); + $lastmod = esc_xml( __( 'Last Modified' ) ); + $changefreq = esc_xml( __( 'Change Frequency' ) ); + $priority = esc_xml( __( 'Priority' ) ); $xsl_content = << @@ -90,6 +92,7 @@ public function get_sitemap_stylesheet() {

{$title}

{$description}

+

{$learn_more}

{$text}

@@ -149,21 +152,24 @@ public function get_sitemap_stylesheet() { * @since 5.5.0 */ public function get_sitemap_index_stylesheet() { - $css = $this->get_stylesheet_css(); - $title = esc_html__( 'XML Sitemap' ); - $description = sprintf( - /* translators: %s: URL to sitemaps documentation. */ - __( 'This XML Sitemap is generated by WordPress to make your content more visible for search engines. Learn more about XML sitemaps on sitemaps.org.' ), - __( 'https://www.sitemaps.org/' ) + $css = $this->get_stylesheet_css(); + $title = esc_xml( __( 'XML Sitemap' ) ); + $description = esc_xml( __( 'This XML Sitemap is generated by WordPress to make your content more visible for search engines.' ) ); + $learn_more = sprintf( + '%s', + esc_url( __( 'https://www.sitemaps.org/' ) ), + esc_xml( __( 'Learn more about XML sitemaps.' ) ) ); - $text = sprintf( + + $text = sprintf( /* translators: %s: number of URLs. */ - __( 'Number of URLs in this XML Sitemap: %s.' ), + esc_xml( __( 'Number of URLs in this XML Sitemap: %s.' ) ), '' ); - $lang = get_language_attributes( 'html' ); - $url = esc_html__( 'URL' ); - $lastmod = esc_html__( 'Last Modified' ); + + $lang = get_language_attributes( 'html' ); + $url = esc_xml( __( 'URL' ) ); + $lastmod = esc_xml( __( 'Last Modified' ) ); $xsl_content = << @@ -175,12 +181,12 @@ public function get_sitemap_index_stylesheet() { > - + - + @@ -192,6 +198,7 @@ public function get_sitemap_index_stylesheet() {

{$title}

{$description}

+

{$learn_more}

{$text}

@@ -230,7 +237,7 @@ public function get_sitemap_index_stylesheet() { * * @param string $xsl Full content for the xml stylesheet. */ - return apply_filters( 'wp_sitemaps_index_stylesheet_content', $xsl_content ); + return apply_filters( 'wp_sitemaps_stylesheet_index_content', $xsl_content ); } /** diff --git a/tests/phpunit/tests/sitemaps/sitemaps-registry.php b/tests/phpunit/tests/sitemaps/sitemaps-registry.php index 3baffaa9f0846..c6c664a65beeb 100644 --- a/tests/phpunit/tests/sitemaps/sitemaps-registry.php +++ b/tests/phpunit/tests/sitemaps/sitemaps-registry.php @@ -30,15 +30,4 @@ public function test_add_sitemap_prevent_duplicates() { $this->assertCount( 1, $sitemaps ); $this->assertSame( $sitemaps['foo'], $provider1, 'Can not confirm sitemap registration is working.' ); } - - public function test_add_sitemap_invalid_type() { - $provider = null; - $registry = new WP_Sitemaps_Registry(); - - $actual = $registry->add_sitemap( 'foo', $provider ); - $sitemaps = $registry->get_sitemaps(); - - $this->assertFalse( $actual ); - $this->assertCount( 0, $sitemaps ); - } } diff --git a/tests/phpunit/tests/sitemaps/sitemaps-stylesheet.php b/tests/phpunit/tests/sitemaps/sitemaps-stylesheet.php index 3e2432cbfe6b5..09aacf04407e7 100644 --- a/tests/phpunit/tests/sitemaps/sitemaps-stylesheet.php +++ b/tests/phpunit/tests/sitemaps/sitemaps-stylesheet.php @@ -19,10 +19,10 @@ public function test_filter_sitemaps_stylesheet_content() { /** * Test that sitemap index stylesheet content can be filtered. */ - public function test_filter_sitemaps_index_stylesheet_content() { + public function test_filter_sitemaps_stylesheet_index_content() { $stylesheet = new WP_Sitemaps_Stylesheet(); - add_filter( 'wp_sitemaps_index_stylesheet_content', '__return_empty_string' ); + add_filter( 'wp_sitemaps_stylesheet_index_content', '__return_empty_string' ); $content = $stylesheet->get_sitemap_index_stylesheet(); $this->assertSame( '', $content, 'Could not filter sitemap index stylesheet content' ); From a19379ec56f8d4d9b400b6242910ed7e3c0ac9db Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Thu, 4 Jun 2020 14:37:47 +0200 Subject: [PATCH 12/18] Try getting new docker images --- .travis.yml | 4 ++++ docker-compose.yml | 6 +++--- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/.travis.yml b/.travis.yml index 37258aff9b761..3416010f7390d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -26,6 +26,9 @@ env: - PHP_FPM_UID: "`id -u`" - PHP_FPM_GID: "`id -g`" - PUPPETEER_SKIP_CHROMIUM_DOWNLOAD: true + - GITHUB_USERNAME: swissspidy + # GITHUB_TOKEN + - secure: RorHhmE4oo4x80H/+xWChcFxrUY4t1CiE8KrSUZZoxVUWP2FMClb7WFGli4rkAYjw127QV71nE4bWFAvMpACHK17WhrD6fGpx/GJPXCmQlKt6ROB0rECwfqhHN3GCBEdpceFZ06+HK4Ligcumbee/swmLxGNozvI5qzzpzJCHlv+GOEpsd83Eu1lZUwJO6DNULlcZYzwifMBlhA0clzwb5qBOJrUtWlLAGdVM58LJxgdIvIUqwavowLZfGidPs1NPAEFFWOzPS5Jzg8SCt41qDyirTCAHZKpBm47p12P65XAfN84pjXdGtYdMfEz8rBdFPO/lxS2cqNTYV/tikLJa7U6sR1YxbtLvymM+KnBDnjywdO70+tWE9+NHULAzVkt6KIs1+ma9C5iBdXG7rKzal7DcfHVmun9PYmh9joVtKiW8RAuai8UKCmNJz/HeNZvZFh0QKecrSVV3e1SPpscbbohICPLWK/MZypqeGuwUBDWl5JxdQE6DYrcFC+0QKgl5tozWfPGW/tA1gJvt4IeNXo4NRRrfOuT9Yx5BxM6WJeq4Cv7g8hctde5/SypcrZ3Iv6yoyoUq8G1fQw84rBDNbLkzztogjJFWU4Gj/g24N0E5XwWdP8VTsn+SGopMtPeB5XC9M0mYpiCfyYKybVnGct2DAGPAE8nV8sVJRICsPg= jobs: include: @@ -58,6 +61,7 @@ jobs: fast_finish: true before_install: +- echo "$GITHUB_TOKEN" | docker login docker.pkg.github.com -u "$GITHUB_USERNAME" --password-stdin - | if [[ "$WP_TRAVISCI" == "test:php" ]]; then travis_retry svn checkout -r 2170172 https://plugins.svn.wordpress.org/wordpress-importer/trunk/ tests/phpunit/data/plugins/wordpress-importer diff --git a/docker-compose.yml b/docker-compose.yml index 3d0ff62cd5700..48e295417ba9d 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -31,7 +31,7 @@ services: # The PHP container. ## php: - image: wordpressdevelop/php:${LOCAL_PHP-latest} + image: docker.pkg.github.com/wordpress/wpdev-docker-images/php:${LOCAL_PHP-latest}-31 networks: - wpdevnet @@ -75,7 +75,7 @@ services: # The WP CLI container. ## cli: - image: wordpressdevelop/cli:${LOCAL_PHP-latest} + image: docker.pkg.github.com/wordpress/wpdev-docker-images/cli:${LOCAL_PHP-latest}-31 networks: - wpdevnet @@ -96,7 +96,7 @@ services: # The PHPUnit container. ## phpunit: - image: wordpressdevelop/phpunit:${LOCAL_PHP-latest} + image: docker.pkg.github.com/wordpress/wpdev-docker-images/phpunit:${LOCAL_PHP-latest}-31 networks: - wpdevnet From 5cda6d52353cd057637d94d91fc4bb354fc9a9f9 Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Thu, 4 Jun 2020 20:34:42 +0200 Subject: [PATCH 13/18] Downstream changes --- src/wp-includes/sitemaps/class-wp-sitemaps-index.php | 1 - src/wp-includes/sitemaps/class-wp-sitemaps-provider.php | 1 - src/wp-includes/sitemaps/class-wp-sitemaps-renderer.php | 4 ++-- 3 files changed, 2 insertions(+), 4 deletions(-) diff --git a/src/wp-includes/sitemaps/class-wp-sitemaps-index.php b/src/wp-includes/sitemaps/class-wp-sitemaps-index.php index 7745ad630bb79..1cf363fec99bf 100644 --- a/src/wp-includes/sitemaps/class-wp-sitemaps-index.php +++ b/src/wp-includes/sitemaps/class-wp-sitemaps-index.php @@ -16,7 +16,6 @@ * @since 5.5.0 */ class WP_Sitemaps_Index { - /** * The main registry of supported sitemaps. * diff --git a/src/wp-includes/sitemaps/class-wp-sitemaps-provider.php b/src/wp-includes/sitemaps/class-wp-sitemaps-provider.php index 02dcacce1235b..f89d9c9d33824 100644 --- a/src/wp-includes/sitemaps/class-wp-sitemaps-provider.php +++ b/src/wp-includes/sitemaps/class-wp-sitemaps-provider.php @@ -15,7 +15,6 @@ * @since 5.5.0 */ abstract class WP_Sitemaps_Provider { - /** * Provider name. * diff --git a/src/wp-includes/sitemaps/class-wp-sitemaps-renderer.php b/src/wp-includes/sitemaps/class-wp-sitemaps-renderer.php index 7af96eb3e887b..860f74242dfa8 100644 --- a/src/wp-includes/sitemaps/class-wp-sitemaps-renderer.php +++ b/src/wp-includes/sitemaps/class-wp-sitemaps-renderer.php @@ -256,10 +256,10 @@ static function () { wp_die( sprintf( /* translators: %s: SimpleXML */ - __( 'Could not generate XML sitemap due to missing %s extension' ), + esc_xml( __( 'Could not generate XML sitemap due to missing %s extension' ) ), 'SimpleXML' ), - __( 'WordPress › Error' ), + esc_xml( __( 'WordPress › Error' ) ), array( 'response' => 501, // "Not implemented". ) From faed912ca28fe784efe2147492e8d4ac97e9fdf7 Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Mon, 8 Jun 2020 22:47:59 +0200 Subject: [PATCH 14/18] Add `wp_sitemaps_posts_show_on_front_entry` filter --- .../providers/class-wp-sitemaps-posts.php | 12 +++++++- .../phpunit/tests/sitemaps/sitemaps-posts.php | 30 +++++++++++++++++++ 2 files changed, 41 insertions(+), 1 deletion(-) diff --git a/src/wp-includes/sitemaps/providers/class-wp-sitemaps-posts.php b/src/wp-includes/sitemaps/providers/class-wp-sitemaps-posts.php index b3d7b592d654d..4cca6ea6ec436 100644 --- a/src/wp-includes/sitemaps/providers/class-wp-sitemaps-posts.php +++ b/src/wp-includes/sitemaps/providers/class-wp-sitemaps-posts.php @@ -107,9 +107,19 @@ public function get_url_list( $page_num, $post_type = '' ) { */ if ( 'page' === $post_type && 1 === $page_num && 'posts' === get_option( 'show_on_front' ) ) { // Extract the data needed for home URL to add to the array. - $url_list[] = array( + $sitemap_entry = array( 'loc' => home_url(), ); + + /** + * Filters the sitemap entry for the home page when the 'show_on_front' option equals 'posts'. + * + * @since 5.5.0 + * + * @param array $sitemap_entry Sitemap entry for the home page. + */ + $sitemap_entry = apply_filters( 'wp_sitemaps_posts_show_on_front_entry', $sitemap_entry ); + $url_list[] = $sitemap_entry; } foreach ( $posts as $post ) { diff --git a/tests/phpunit/tests/sitemaps/sitemaps-posts.php b/tests/phpunit/tests/sitemaps/sitemaps-posts.php index 4e81d814bbe8e..8d173942f417e 100644 --- a/tests/phpunit/tests/sitemaps/sitemaps-posts.php +++ b/tests/phpunit/tests/sitemaps/sitemaps-posts.php @@ -16,4 +16,34 @@ public function test_filter_sitemaps_post_types() { $this->assertEquals( array(), $subtypes, 'Could not filter posts subtypes.' ); } + + /** + * Test `wp_sitemaps_posts_show_on_front_entry` filter. + */ + public function test_posts_show_on_front_entry() { + $posts_provider = new WP_Sitemaps_Posts(); + update_option( 'show_on_front', 'page' ); + + add_filter( 'wp_sitemaps_posts_show_on_front_entry', array( $this, '_show_on_front_entry' ) ); + + $url_list = $posts_provider->get_url_list( 1, 'page' ); + + $this->assertEquals( array(), $url_list ); + + update_option( 'show_on_front', 'posts' ); + + $url_list = $posts_provider->get_url_list( 1, 'page' ); + $sitemap_entry = array_shift( $url_list ); + + $this->assertTrue( isset( $sitemap_entry['lastmod'] ) ); + } + + /** + * Callback for 'wp_sitemaps_posts_show_on_front_entry' filter. + */ + public function _show_on_front_entry( $sitemap_entry ) { + $sitemap_entry['lastmod'] = wp_date( DATE_W3C, time() ); + + return $sitemap_entry; + } } From c22a018ab0d4e803e220eb91d0a621277a50dd5d Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Tue, 9 Jun 2020 14:52:53 +0200 Subject: [PATCH 15/18] Revert temp changes to local env --- .travis.yml | 4 ---- docker-compose.yml | 6 +++--- 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/.travis.yml b/.travis.yml index 3416010f7390d..37258aff9b761 100644 --- a/.travis.yml +++ b/.travis.yml @@ -26,9 +26,6 @@ env: - PHP_FPM_UID: "`id -u`" - PHP_FPM_GID: "`id -g`" - PUPPETEER_SKIP_CHROMIUM_DOWNLOAD: true - - GITHUB_USERNAME: swissspidy - # GITHUB_TOKEN - - secure: RorHhmE4oo4x80H/+xWChcFxrUY4t1CiE8KrSUZZoxVUWP2FMClb7WFGli4rkAYjw127QV71nE4bWFAvMpACHK17WhrD6fGpx/GJPXCmQlKt6ROB0rECwfqhHN3GCBEdpceFZ06+HK4Ligcumbee/swmLxGNozvI5qzzpzJCHlv+GOEpsd83Eu1lZUwJO6DNULlcZYzwifMBlhA0clzwb5qBOJrUtWlLAGdVM58LJxgdIvIUqwavowLZfGidPs1NPAEFFWOzPS5Jzg8SCt41qDyirTCAHZKpBm47p12P65XAfN84pjXdGtYdMfEz8rBdFPO/lxS2cqNTYV/tikLJa7U6sR1YxbtLvymM+KnBDnjywdO70+tWE9+NHULAzVkt6KIs1+ma9C5iBdXG7rKzal7DcfHVmun9PYmh9joVtKiW8RAuai8UKCmNJz/HeNZvZFh0QKecrSVV3e1SPpscbbohICPLWK/MZypqeGuwUBDWl5JxdQE6DYrcFC+0QKgl5tozWfPGW/tA1gJvt4IeNXo4NRRrfOuT9Yx5BxM6WJeq4Cv7g8hctde5/SypcrZ3Iv6yoyoUq8G1fQw84rBDNbLkzztogjJFWU4Gj/g24N0E5XwWdP8VTsn+SGopMtPeB5XC9M0mYpiCfyYKybVnGct2DAGPAE8nV8sVJRICsPg= jobs: include: @@ -61,7 +58,6 @@ jobs: fast_finish: true before_install: -- echo "$GITHUB_TOKEN" | docker login docker.pkg.github.com -u "$GITHUB_USERNAME" --password-stdin - | if [[ "$WP_TRAVISCI" == "test:php" ]]; then travis_retry svn checkout -r 2170172 https://plugins.svn.wordpress.org/wordpress-importer/trunk/ tests/phpunit/data/plugins/wordpress-importer diff --git a/docker-compose.yml b/docker-compose.yml index 48e295417ba9d..3d0ff62cd5700 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -31,7 +31,7 @@ services: # The PHP container. ## php: - image: docker.pkg.github.com/wordpress/wpdev-docker-images/php:${LOCAL_PHP-latest}-31 + image: wordpressdevelop/php:${LOCAL_PHP-latest} networks: - wpdevnet @@ -75,7 +75,7 @@ services: # The WP CLI container. ## cli: - image: docker.pkg.github.com/wordpress/wpdev-docker-images/cli:${LOCAL_PHP-latest}-31 + image: wordpressdevelop/cli:${LOCAL_PHP-latest} networks: - wpdevnet @@ -96,7 +96,7 @@ services: # The PHPUnit container. ## phpunit: - image: docker.pkg.github.com/wordpress/wpdev-docker-images/phpunit:${LOCAL_PHP-latest}-31 + image: wordpressdevelop/phpunit:${LOCAL_PHP-latest} networks: - wpdevnet From 8053ef779c757900a7b68a2b97bd07c41b9e3bf9 Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Wed, 10 Jun 2020 00:08:11 +0200 Subject: [PATCH 16/18] Try: improve esc_xml Props @pbiron --- src/wp-includes/formatting.php | 52 ++++++------------- src/wp-includes/kses.php | 63 +++++++++++++++++++++-- tests/phpunit/tests/formatting/EscXml.php | 2 +- 3 files changed, 77 insertions(+), 40 deletions(-) diff --git a/src/wp-includes/formatting.php b/src/wp-includes/formatting.php index 185170abd86fd..531e7a608bd17 100644 --- a/src/wp-includes/formatting.php +++ b/src/wp-includes/formatting.php @@ -935,6 +935,7 @@ function seems_utf8( $str ) { * ", or ENT_QUOTES to do both. Default is ENT_NOQUOTES where no quotes are encoded. * * @since 1.2.2 + * @since 5.5.0 `$quote_style` also accepts '`ENT_XML1`. * @access private * * @staticvar string $_charset @@ -942,7 +943,10 @@ function seems_utf8( $str ) { * @param string $string The text which is to be encoded. * @param int|string $quote_style Optional. Converts double quotes if set to ENT_COMPAT, * both single and double if set to ENT_QUOTES or none if set to ENT_NOQUOTES. - * Also compatible with old values; converting single quotes if set to 'single', + * Converts single and double quotes, as well as converting HTML + * named entities (that are not also XML named entities) to their + * code points if set to ENT_XML1. Also compatible with old values; + * converting single quotes if set to 'single', * double if set to 'double' or both if otherwise set. * Default is ENT_NOQUOTES. * @param false|string $charset Optional. The character encoding of the string. Default is false. @@ -964,7 +968,9 @@ function _wp_specialchars( $string, $quote_style = ENT_NOQUOTES, $charset = fals // Account for the previous behaviour of the function when the $quote_style is not an accepted value. if ( empty( $quote_style ) ) { $quote_style = ENT_NOQUOTES; - } elseif ( ! in_array( $quote_style, array( 0, 2, 3, 'single', 'double' ), true ) ) { + } elseif ( ENT_XML1 === $quote_style ) { + $quote_style = ENT_QUOTES | ENT_XML1; + } elseif ( ! in_array( $quote_style, array( ENT_NOQUOTES, ENT_COMPAT, ENT_QUOTES, 'single', 'double' ), true ) ) { $quote_style = ENT_QUOTES; } @@ -994,7 +1000,7 @@ function _wp_specialchars( $string, $quote_style = ENT_NOQUOTES, $charset = fals if ( ! $double_encode ) { // Guarantee every &entity; is valid, convert &garbage; into &garbage; // This is required for PHP < 5.4.0 because ENT_HTML401 flag is unavailable. - $string = wp_kses_normalize_entities( $string ); + $string = wp_kses_normalize_entities( $string, ( $quote_style & ENT_XML1 ) ? 'xml' : 'html' ); } $string = htmlspecialchars( $string, $quote_style, $charset, $double_encode ); @@ -4557,20 +4563,23 @@ function esc_xml( $text ) { | # alternative (?(.*)) # non-CDATA Section -/sx +/sx EOF; + $safe_text = (string) preg_replace_callback( $regex, - function( $matches ) { + static function( $matches ) { if ( ! $matches[0] ) { return ''; - } elseif ( ! empty( $matches['non_cdata'] ) ) { + } + + if ( ! empty( $matches['non_cdata'] ) ) { // escape HTML entities in the non-CDATA Section. - return _esc_xml_non_cdata_section( $matches['non_cdata'] ); + return _wp_specialchars( $matches['non_cdata'], ENT_XML1 ); } // Return the CDATA Section unchanged, escape HTML entities in the rest. - return _esc_xml_non_cdata_section( $matches['non_cdata_followed_by_cdata'] ) . $matches['cdata']; + return _wp_specialchars( $matches['non_cdata_followed_by_cdata'], ENT_XML1 ) . $matches['cdata']; }, $safe_text ); @@ -4590,33 +4599,6 @@ function( $matches ) { return apply_filters( 'esc_xml', $safe_text, $text ); } -/** - * Escaping for non-CDATA Section XML blocks. - * - * @access private - * @since 5.5.0 - * - * @param string $text Text to escape. - * @return string - */ -function _esc_xml_non_cdata_section( $text ) { - global $allowedentitynames; - - $safe_text = _wp_specialchars( $text, ENT_QUOTES ); - // Replace HTML entities with their Unicode codepoints, - // without doing the same for the 5 XML entities. - $html_only_entities = array_diff( $allowedentitynames, array( 'amp', 'lt', 'gt', 'apos', 'quot' ) ); - $safe_text = (string) preg_replace_callback( - '/&(' . implode( '|', $html_only_entities ) . ');/', - function( $matches ) { - return html_entity_decode( $matches[0], ENT_HTML5 ); - }, - $safe_text - ); - - return $safe_text; -} - /** * Escape an HTML tag name. * diff --git a/src/wp-includes/kses.php b/src/wp-includes/kses.php index 9c9d094d4633c..703d7c0002101 100644 --- a/src/wp-includes/kses.php +++ b/src/wp-includes/kses.php @@ -47,7 +47,7 @@ // Ensure that these variables are added to the global namespace // (e.g. if using namespaces / autoload in the current PHP environment). -global $allowedposttags, $allowedtags, $allowedentitynames; +global $allowedposttags, $allowedtags, $allowedentitynames, $allowedxmlentitynames; if ( ! CUSTOM_TAGS ) { /** @@ -704,6 +704,18 @@ 'there4', ); + /** + * @var string[] $allowedxmlentitynames Array of KSES allowed XML entitity names. + * @since 5.5.0 + */ + $allowedxmlnamedentities = array( + 'amp', + 'lt', + 'gt', + 'apos', + 'quot', + ); + $allowedposttags = array_map( '_wp_add_global_attributes', $allowedposttags ); } else { $allowedtags = wp_kses_array_lc( $allowedtags ); @@ -1745,17 +1757,27 @@ function wp_kses_bad_protocol_once2( $string, $allowed_protocols ) { * This function normalizes HTML entities. It will convert `AT&T` to the correct * `AT&T`, `:` to `:`, `&#XYZZY;` to `&#XYZZY;` and so on. * + * When `$context` is set to 'xml', HTML entities are converted to their code points. For + * example, `AT&T…&#XYZZY;` is converted to `AT&T…&#XYZZY;`. + * * @since 1.0.0 + * @since 5.5.0 Added `$context` parameter. * - * @param string $string Content to normalize entities. + * @param string $string Content to normalize entities. + * @param string $context Context for normalization. Can be either 'html' or 'xml'. + * Default 'html'. * @return string Content with normalized entities. */ -function wp_kses_normalize_entities( $string ) { +function wp_kses_normalize_entities( $string, $context = 'html' ) { // Disarm all entities by converting & to & $string = str_replace( '&', '&', $string ); // Change back the allowed entities in our entity whitelist. - $string = preg_replace_callback( '/&([A-Za-z]{2,8}[0-9]{0,2});/', 'wp_kses_named_entities', $string ); + if ( 'xml' === $context ) { + $string = preg_replace_callback( '/&([A-Za-z]{2,8}[0-9]{0,2});/', 'wp_kses_xml_named_entities', $string ); + } else { + $string = preg_replace_callback( '/&([A-Za-z]{2,8}[0-9]{0,2});/', 'wp_kses_named_entities', $string ); + } $string = preg_replace_callback( '/&#(0*[0-9]{1,7});/', 'wp_kses_normalize_entities2', $string ); $string = preg_replace_callback( '/&#[Xx](0*[0-9A-Fa-f]{1,6});/', 'wp_kses_normalize_entities3', $string ); @@ -1786,6 +1808,39 @@ function wp_kses_named_entities( $matches ) { return ( ! in_array( $i, $allowedentitynames, true ) ) ? "&$i;" : "&$i;"; } +/** + * Callback for `wp_kses_normalize_entities()` regular expression. + * + * This function only accepts valid named entity references, which are finite, + * case-sensitive, and highly scrutinized by XML validators. HTML named entity + * references are converted to their code points. + * + * @since 5.5.0 + * + * @global array $allowedentitynames + * @global array $allowedxmlnamedentities + * + * @param array $matches preg_replace_callback() matches array. + * @return string Correctly encoded entity. + */ +function wp_kses_xml_named_entities( $matches ) { + global $allowedentitynames, $allowedxmlnamedentities; + + if ( empty( $matches[1] ) ) { + return ''; + } + + $i = $matches[1]; + + if ( in_array( $i, $allowedxmlnamedentities, true ) ) { + return "&$i;"; + } elseif ( in_array( $i, $allowedentitynames, true ) ) { + return html_entity_decode( "&$i;", ENT_HTML5 ); + } + + return "&$i;"; +} + /** * Callback for `wp_kses_normalize_entities()` regular expression. * diff --git a/tests/phpunit/tests/formatting/EscXml.php b/tests/phpunit/tests/formatting/EscXml.php index 28ab84fc8a12a..9e188e3fcba73 100644 --- a/tests/phpunit/tests/formatting/EscXml.php +++ b/tests/phpunit/tests/formatting/EscXml.php @@ -40,7 +40,7 @@ public function _test_esc_xml_basics_dataprovider() { // SQL query w/ single quotes. array( "SELECT meta_key, meta_value FROM wp_trunk_sitemeta WHERE meta_key IN ('site_name', 'siteurl', 'active_sitewide_plugins', '_site_transient_timeout_theme_roots', '_site_transient_theme_roots', 'site_admins', 'can_compress_scripts', 'global_terms_enabled') AND site_id = 1", - 'SELECT meta_key, meta_value FROM wp_trunk_sitemeta WHERE meta_key IN ('site_name', 'siteurl', 'active_sitewide_plugins', '_site_transient_timeout_theme_roots', '_site_transient_theme_roots', 'site_admins', 'can_compress_scripts', 'global_terms_enabled') AND site_id = 1', + 'SELECT meta_key, meta_value FROM wp_trunk_sitemeta WHERE meta_key IN ('site_name', 'siteurl', 'active_sitewide_plugins', '_site_transient_timeout_theme_roots', '_site_transient_theme_roots', 'site_admins', 'can_compress_scripts', 'global_terms_enabled') AND site_id = 1', ), ); } From fa95dc102b8d80d04625cbbb4ba6aa81730bf5b1 Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Wed, 10 Jun 2020 10:13:04 +0200 Subject: [PATCH 17/18] Simplify `\WP_Sitemaps_Index::get_index_url()` --- src/wp-includes/sitemaps/class-wp-sitemaps-index.php | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/wp-includes/sitemaps/class-wp-sitemaps-index.php b/src/wp-includes/sitemaps/class-wp-sitemaps-index.php index 1cf363fec99bf..abaf9e539ca7d 100644 --- a/src/wp-includes/sitemaps/class-wp-sitemaps-index.php +++ b/src/wp-includes/sitemaps/class-wp-sitemaps-index.php @@ -73,12 +73,10 @@ public function get_index_url() { /* @var WP_Rewrite $wp_rewrite */ global $wp_rewrite; - $url = home_url( '/wp-sitemap.xml' ); - if ( ! $wp_rewrite->using_permalinks() ) { - $url = add_query_arg( 'sitemap', 'index', home_url( '/' ) ); + return add_query_arg( 'sitemap', 'index', home_url( '/' ) ); } - return $url; + return home_url( '/wp-sitemap.xml' ); } } From 8acb95bd339d7c2fa66ad3e19bfe545b002d2240 Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Tue, 16 Jun 2020 13:27:28 +0200 Subject: [PATCH 18/18] Update phpdoc for esc_xml --- src/wp-includes/formatting.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/wp-includes/formatting.php b/src/wp-includes/formatting.php index 531e7a608bd17..843dc897b9c3d 100644 --- a/src/wp-includes/formatting.php +++ b/src/wp-includes/formatting.php @@ -4548,7 +4548,7 @@ function esc_textarea( $text ) { * @since 5.5.0 * * @param string $text Text to escape. - * @return string + * @return string Escaped text. */ function esc_xml( $text ) { $safe_text = wp_check_invalid_utf8( $text );