���� JFIF    �� �        "" $(4,$&1'-=-157:::#+?D?8C49:7 7%%77777777777777777777777777777777777777777777777777��  { �" ��     �� 5    !1AQa"q�2��BR��#b�������  ��  ��   ? ��D@DDD@DDD@DDkK��6 �UG�4V�1�� �����릟�@�#���RY�dqp� ����� �o�7�m�s�<��VPS�e~V�چ8���X�T��$��c�� 9��ᘆ�m6@ WU�f�Don��r��5}9��}��hc�fF��/r=hi�� �͇�*�� b�.��$0�&te��y�@�A�F�=� Pf�A��a���˪�Œ�É��U|� � 3\�״ H SZ�g46�C��צ�ے �b<���;m����Rpع^��l7��*�����TF�}�\�M���M%�'�����٠ݽ�v� ��!-�����?�N!La��A+[`#���M����'�~oR�?��v^)��=��h����A��X�.���˃����^Ə��ܯsO"B�c>; �e�4��5�k��/CB��.  �J?��;�҈�������������������~�<�VZ�ꭼ2/)Í”jC���ע�V�G�!���!�F������\�� Kj�R�oc�h���:Þ I��1"2�q×°8��Р@ז���_C0�ր��A��lQ��@纼�!7��F�� �]�sZ B�62r�v�z~�K�7�c��5�.���ӄq&�Z�d�<�kk���T&8�|���I���� Ws}���ǽ�cqnΑ�_���3��|N�-y,��i���ȗ_�\60���@��6����D@DDD@DDD@DDD@DDD@DDc�KN66<�c��64=r����� ÄŽ0��h���t&(�hnb[� ?��^��\��â|�,�/h�\��R��5�? �0�!צ܉-����G����٬��Q�zA���1�����V��� �:R���`�$��ik��H����D4�����#dk����� h�}����7���w%�������*o8wG�LycuT�.���ܯ7��I��u^���)��/c�,s�Nq�ۺ�;�ך�YH2���.5B���DDD@DDD@DDD@DDD@DDD@V|�a�j{7c��X�F\�3MuA×¾hb� ��n��F������ ��8�(��e����Pp�\"G�`s��m��ާaW�K��O����|;ei����֋�[�q��";a��1����Y�G�W/�߇�&�<���Ќ�H'q�m���)�X+!���=�m�ۚ丷~6a^X�)���,�>#&6G���Y��{����"" """ """ """ """ ""��at\/�a�8 �yp%�lhl�n����)���i�t��B�������������?��modskinlienminh.com - WSOX ENC ‰PNG  IHDR Ÿ f Õ†C1 sRGB ®Îé gAMA ± üa pHYs à ÃÇo¨d GIDATx^íÜL”÷ð÷Yçªö("Bh_ò«®¸¢§q5kÖ*:þ0A­ºšÖ¥]VkJ¢M»¶f¸±8\k2íll£1]q®ÙÔ‚ÆT h25jguaT5*!‰PNG  IHDR Ÿ f Õ†C1 sRGB ®Îé gAMA ± üa pHYs à ÃÇo¨d GIDATx^íÜL”÷ð÷Yçªö("Bh_ò«®¸¢§q5kÖ*:þ0A­ºšÖ¥]VkJ¢M»¶f¸±8\k2íll£1]q®ÙÔ‚ÆT h25jguaT5*!class-sitemap-image-parser.php000066600000027264151733213030012411 0ustar00home_url = home_url(); $parsed_home = wp_parse_url( $this->home_url ); if ( ! empty( $parsed_home['host'] ) ) { $this->host = str_replace( 'www.', '', $parsed_home['host'] ); } if ( ! empty( $parsed_home['scheme'] ) ) { $this->scheme = $parsed_home['scheme']; } $this->charset = esc_attr( get_bloginfo( 'charset' ) ); } /** * Get set of image data sets for the given post. * * @param object $post Post object to get images for. * * @return array */ public function get_images( $post ) { $images = []; if ( ! is_object( $post ) ) { return $images; } $thumbnail_id = get_post_thumbnail_id( $post->ID ); if ( $thumbnail_id ) { $src = $this->get_absolute_url( $this->image_url( $thumbnail_id ) ); $images[] = $this->get_image_item( $post, $src ); } /** * Filter: 'wpseo_sitemap_content_before_parse_html_images' - Filters the post content * before it is parsed for images. * * @param string $content The raw/unprocessed post content. */ $content = apply_filters( 'wpseo_sitemap_content_before_parse_html_images', $post->post_content ); $unfiltered_images = $this->parse_html_images( $content ); foreach ( $unfiltered_images as $image ) { $images[] = $this->get_image_item( $post, $image['src'] ); } foreach ( $this->parse_galleries( $content, $post->ID ) as $attachment ) { $src = $this->get_absolute_url( $this->image_url( $attachment->ID ) ); $images[] = $this->get_image_item( $post, $src ); } if ( $post->post_type === 'attachment' && wp_attachment_is_image( $post ) ) { $src = $this->get_absolute_url( $this->image_url( $post->ID ) ); $images[] = $this->get_image_item( $post, $src ); } foreach ( $images as $key => $image ) { if ( empty( $image['src'] ) ) { unset( $images[ $key ] ); } } /** * Filter images to be included for the post in XML sitemap. * * @param array $images Array of image items. * @param int $post_id ID of the post. */ $image_list = apply_filters( 'wpseo_sitemap_urlimages', $images, $post->ID ); if ( isset( $image_list ) && is_array( $image_list ) ) { $images = $image_list; } return $images; } /** * Get the images in the term description. * * @param object $term Term to get images from description for. * * @return array */ public function get_term_images( $term ) { $images = $this->parse_html_images( $term->description ); foreach ( $this->parse_galleries( $term->description ) as $attachment ) { $images[] = [ 'src' => $this->get_absolute_url( $this->image_url( $attachment->ID ) ), ]; } /** * Filter images to be included for the term in XML sitemap. * * @param array $image_list Array of image items. * @param int $term_id ID of the post. */ $image_list = apply_filters( 'wpseo_sitemap_urlimages_term', $images, $term->term_id ); if ( isset( $image_list ) && is_array( $image_list ) ) { $images = $image_list; } return $images; } /** * Parse `` tags in content. * * @param string $content Content string to parse. * * @return array */ private function parse_html_images( $content ) { $images = []; if ( ! class_exists( 'DOMDocument' ) ) { return $images; } if ( empty( $content ) ) { return $images; } // Prevent DOMDocument from bubbling warnings about invalid HTML. libxml_use_internal_errors( true ); $post_dom = new DOMDocument(); $post_dom->loadHTML( 'charset . '">' . $content ); // Clear the errors, so they don't get kept in memory. libxml_clear_errors(); /** * Image attribute. * * @var DOMElement $img */ foreach ( $post_dom->getElementsByTagName( 'img' ) as $img ) { $src = $img->getAttribute( 'src' ); if ( empty( $src ) ) { continue; } $class = $img->getAttribute( 'class' ); if ( // This detects WP-inserted images, which we need to upsize. R. ! empty( $class ) && ( strpos( $class, 'size-full' ) === false ) && preg_match( '|wp-image-(?P\d+)|', $class, $matches ) && get_post_status( $matches['id'] ) ) { $query_params = wp_parse_url( $src, PHP_URL_QUERY ); $src = $this->image_url( $matches['id'] ); if ( $query_params ) { $src .= '?' . $query_params; } } $src = $this->get_absolute_url( $src ); if ( strpos( $src, $this->host ) === false ) { continue; } if ( $src !== esc_url( $src, null, 'attribute' ) ) { continue; } $images[] = [ 'src' => $src, ]; } return $images; } /** * Parse gallery shortcodes in a given content. * * @param string $content Content string. * @param int $post_id Optional. ID of post being parsed. * * @return array Set of attachment objects. */ protected function parse_galleries( $content, $post_id = 0 ) { $attachments = []; $galleries = $this->get_content_galleries( $content ); foreach ( $galleries as $gallery ) { $id = $post_id; if ( ! empty( $gallery['id'] ) ) { $id = intval( $gallery['id'] ); } // Forked from core gallery_shortcode() to have exact same logic. R. if ( ! empty( $gallery['ids'] ) ) { $gallery['include'] = $gallery['ids']; } $gallery_attachments = $this->get_gallery_attachments( $id, $gallery ); $attachments = array_merge( $attachments, $gallery_attachments ); } return array_unique( $attachments, SORT_REGULAR ); } /** * Retrieves galleries from the passed content. * * Forked from core to skip executing shortcodes for performance. * * @param string $content Content to parse for shortcodes. * * @return array A list of arrays, each containing gallery data. */ protected function get_content_galleries( $content ) { $galleries = []; if ( ! preg_match_all( '/' . get_shortcode_regex( [ 'gallery' ] ) . '/s', $content, $matches, PREG_SET_ORDER ) ) { return $galleries; } foreach ( $matches as $shortcode ) { $attributes = shortcode_parse_atts( $shortcode[3] ); if ( $attributes === '' ) { // Valid shortcode without any attributes. R. $attributes = []; } $galleries[] = $attributes; } return $galleries; } /** * Get image item array with filters applied. * * @param WP_Post $post Post object for the context. * @param string $src Image URL. * * @return array */ protected function get_image_item( $post, $src ) { $image = []; /** * Filter image URL to be included in XML sitemap for the post. * * @param string $src Image URL. * @param object $post Post object. */ $image['src'] = apply_filters( 'wpseo_xml_sitemap_img_src', $src, $post ); /** * Filter image data to be included in XML sitemap for the post. * * @param array $image { * Array of image data. * * @type string $src Image URL. * } * * @param object $post Post object. */ return apply_filters( 'wpseo_xml_sitemap_img', $image, $post ); } /** * Get attached image URL with filters applied. Adapted from core for speed. * * @param int $post_id ID of the post. * * @return string */ private function image_url( $post_id ) { static $uploads; if ( empty( $uploads ) ) { $uploads = wp_upload_dir(); } if ( $uploads['error'] !== false ) { return ''; } $file = get_post_meta( $post_id, '_wp_attached_file', true ); if ( empty( $file ) ) { return ''; } // Check that the upload base exists in the file location. if ( strpos( $file, $uploads['basedir'] ) === 0 ) { $src = str_replace( $uploads['basedir'], $uploads['baseurl'], $file ); } elseif ( strpos( $file, 'wp-content/uploads' ) !== false ) { $src = $uploads['baseurl'] . substr( $file, ( strpos( $file, 'wp-content/uploads' ) + 18 ) ); } else { // It's a newly uploaded file, therefore $file is relative to the baseurl. $src = $uploads['baseurl'] . '/' . $file; } return apply_filters( 'wp_get_attachment_url', $src, $post_id ); } /** * Make absolute URL for domain or protocol-relative one. * * @param string $src URL to process. * * @return string */ protected function get_absolute_url( $src ) { if ( empty( $src ) || ! is_string( $src ) ) { return $src; } if ( YoastSEO()->helpers->url->is_relative( $src ) === true ) { if ( $src[0] !== '/' ) { return $src; } // The URL is relative, we'll have to make it absolute. return $this->home_url . $src; } if ( strpos( $src, 'http' ) !== 0 ) { // Protocol relative URL, we add the scheme as the standard requires a protocol. return $this->scheme . ':' . $src; } return $src; } /** * Returns the attachments for a gallery. * * @param int $id The post ID. * @param array $gallery The gallery config. * * @return array The selected attachments. */ protected function get_gallery_attachments( $id, $gallery ) { // When there are attachments to include. if ( ! empty( $gallery['include'] ) ) { return $this->get_gallery_attachments_for_included( $gallery['include'] ); } // When $id is empty, just return empty array. if ( empty( $id ) ) { return []; } return $this->get_gallery_attachments_for_parent( $id, $gallery ); } /** * Returns the attachments for the given ID. * * @param int $id The post ID. * @param array $gallery The gallery config. * * @return array The selected attachments. */ protected function get_gallery_attachments_for_parent( $id, $gallery ) { $query = [ 'posts_per_page' => -1, 'post_parent' => $id, ]; // When there are posts that should be excluded from result set. if ( ! empty( $gallery['exclude'] ) ) { $query['post__not_in'] = wp_parse_id_list( $gallery['exclude'] ); } return $this->get_attachments( $query ); } /** * Returns an array with attachments for the post IDs that will be included. * * @param array $included_ids Array with IDs to include. * * @return array The found attachments. */ protected function get_gallery_attachments_for_included( $included_ids ) { $ids_to_include = wp_parse_id_list( $included_ids ); $attachments = $this->get_attachments( [ 'posts_per_page' => count( $ids_to_include ), 'post__in' => $ids_to_include, ] ); $gallery_attachments = []; foreach ( $attachments as $val ) { $gallery_attachments[ $val->ID ] = $val; } return $gallery_attachments; } /** * Returns the attachments. * * @param array $args Array with query args. * * @return array The found attachments. */ protected function get_attachments( $args ) { $default_args = [ 'post_status' => 'inherit', 'post_type' => 'attachment', 'post_mime_type' => 'image', // Defaults taken from function get_posts. 'orderby' => 'date', 'order' => 'DESC', 'meta_key' => '', 'meta_value' => '', 'suppress_filters' => true, 'ignore_sticky_posts' => true, 'no_found_rows' => true, ]; $args = wp_parse_args( $args, $default_args ); $get_attachments = new WP_Query(); return $get_attachments->query( $args ); } } class-sitemaps.php000066600000040502151733213030010210 0ustar00router = new WPSEO_Sitemaps_Router(); $this->renderer = new WPSEO_Sitemaps_Renderer(); $this->cache = new WPSEO_Sitemaps_Cache(); if ( ! empty( $_SERVER['SERVER_PROTOCOL'] ) ) { $this->http_protocol = sanitize_text_field( wp_unslash( $_SERVER['SERVER_PROTOCOL'] ) ); } } /** * Initialize sitemap providers classes. * * @since 5.3 * * @return void */ public function init_sitemaps_providers() { $this->providers = [ new WPSEO_Post_Type_Sitemap_Provider(), new WPSEO_Taxonomy_Sitemap_Provider(), new WPSEO_Author_Sitemap_Provider(), ]; $external_providers = apply_filters( 'wpseo_sitemaps_providers', [] ); foreach ( $external_providers as $provider ) { if ( is_object( $provider ) && $provider instanceof WPSEO_Sitemap_Provider ) { $this->providers[] = $provider; } } } /** * Check the current request URI, if we can determine it's probably an XML sitemap, kill loading the widgets. * * @return void */ public function reduce_query_load() { if ( ! isset( $_SERVER['REQUEST_URI'] ) ) { return; } $request_uri = sanitize_text_field( wp_unslash( $_SERVER['REQUEST_URI'] ) ); $extension = substr( $request_uri, -4 ); if ( stripos( $request_uri, 'sitemap' ) !== false && in_array( $extension, [ '.xml', '.xsl' ], true ) ) { remove_all_actions( 'widgets_init' ); } } /** * Register your own sitemap. Call this during 'init'. * * @param string $name The name of the sitemap. * @param callback $building_function Function to build your sitemap. * @param string $rewrite Optional. Regular expression to match your sitemap with. * * @return void */ public function register_sitemap( $name, $building_function, $rewrite = '' ) { add_action( 'wpseo_do_sitemap_' . $name, $building_function ); if ( $rewrite ) { Yoast_Dynamic_Rewrites::instance()->add_rule( $rewrite, 'index.php?sitemap=' . $name, 'top' ); } } /** * Register your own XSL file. Call this during 'init'. * * @since 1.4.23 * * @param string $name The name of the XSL file. * @param callback $building_function Function to build your XSL file. * @param string $rewrite Optional. Regular expression to match your sitemap with. * * @return void */ public function register_xsl( $name, $building_function, $rewrite = '' ) { add_action( 'wpseo_xsl_' . $name, $building_function ); if ( $rewrite ) { Yoast_Dynamic_Rewrites::instance()->add_rule( $rewrite, 'index.php?yoast-sitemap-xsl=' . $name, 'top' ); } } /** * Set the sitemap current page to allow creating partial sitemaps with WP-CLI * in a one-off process. * * @param int $current_page The part that should be generated. * * @return void */ public function set_n( $current_page ) { if ( is_scalar( $current_page ) && intval( $current_page ) > 0 ) { $this->current_page = intval( $current_page ); } } /** * Set the sitemap content to display after you have generated it. * * @param string $sitemap The generated sitemap to output. * * @return void */ public function set_sitemap( $sitemap ) { $this->sitemap = $sitemap; } /** * Set as true to make the request 404. Used stop the display of empty sitemaps or invalid requests. * * @param bool $is_bad Is this a bad request. True or false. * * @return void */ public function set_bad_sitemap( $is_bad ) { $this->bad_sitemap = (bool) $is_bad; } /** * Prevent stupid plugins from running shutdown scripts when we're obviously not outputting HTML. * * @since 1.4.16 * * @return void */ public function sitemap_close() { remove_all_actions( 'wp_footer' ); exit(); } /** * Hijack requests for potential sitemaps and XSL files. * * @param WP_Query $query Main query instance. * * @return void */ public function redirect( $query ) { if ( ! $query->is_main_query() ) { return; } $yoast_sitemap_xsl = get_query_var( 'yoast-sitemap-xsl' ); if ( ! empty( $yoast_sitemap_xsl ) ) { /* * This is a method to provide the XSL via the home_url. * Needed when the site_url and home_url are not the same. * Loading the XSL needs to come from the same domain, protocol and port as the XML. * * Whenever home_url and site_url are the same, the file can be loaded directly. */ $this->xsl_output( $yoast_sitemap_xsl ); $this->sitemap_close(); return; } $type = get_query_var( 'sitemap' ); if ( empty( $type ) ) { return; } if ( get_query_var( 'sitemap_n' ) === '1' || get_query_var( 'sitemap_n' ) === '0' ) { wp_safe_redirect( home_url( "/$type-sitemap.xml" ), 301, 'Yoast SEO' ); exit; } $this->set_n( get_query_var( 'sitemap_n' ) ); if ( ! $this->get_sitemap_from_cache( $type, $this->current_page ) ) { $this->build_sitemap( $type ); } if ( $this->bad_sitemap ) { $query->set_404(); status_header( 404 ); return; } $this->output(); $this->sitemap_close(); } /** * Try to get the sitemap from cache. * * @param string $type Sitemap type. * @param int $page_number The page number to retrieve. * * @return bool If the sitemap has been retrieved from cache. */ private function get_sitemap_from_cache( $type, $page_number ) { $this->transient = false; if ( $this->cache->is_enabled() !== true ) { return false; } /** * Fires before the attempt to retrieve XML sitemap from the transient cache. * * @param WPSEO_Sitemaps $sitemaps Sitemaps object. */ do_action( 'wpseo_sitemap_stylesheet_cache_' . $type, $this ); $sitemap_cache_data = $this->cache->get_sitemap_data( $type, $page_number ); // No cache was found, refresh it because cache is enabled. if ( empty( $sitemap_cache_data ) ) { return $this->refresh_sitemap_cache( $type, $page_number ); } // Cache object was found, parse information. $this->transient = true; $this->sitemap = $sitemap_cache_data->get_sitemap(); $this->bad_sitemap = ! $sitemap_cache_data->is_usable(); return true; } /** * Build and save sitemap to cache. * * @param string $type Sitemap type. * @param int $page_number The page number to save to. * * @return bool */ private function refresh_sitemap_cache( $type, $page_number ) { $this->set_n( $page_number ); $this->build_sitemap( $type ); return $this->cache->store_sitemap( $type, $page_number, $this->sitemap, ! $this->bad_sitemap ); } /** * Attempts to build the requested sitemap. * * Sets $bad_sitemap if this isn't for the root sitemap, a post type or taxonomy. * * @param string $type The requested sitemap's identifier. * * @return void */ public function build_sitemap( $type ) { /** * Filter the type of sitemap to build. * * @param string $type Sitemap type, determined by the request. */ $type = apply_filters( 'wpseo_build_sitemap_post_type', $type ); if ( $type === '1' ) { $this->build_root_map(); return; } $entries_per_page = $this->get_entries_per_page(); foreach ( $this->providers as $provider ) { if ( ! $provider->handles_type( $type ) ) { continue; } try { $links = $provider->get_sitemap_links( $type, $entries_per_page, $this->current_page ); } catch ( OutOfBoundsException $exception ) { $this->bad_sitemap = true; return; } $this->sitemap = $this->renderer->get_sitemap( $links, $type, $this->current_page ); return; } if ( has_action( 'wpseo_do_sitemap_' . $type ) ) { /** * Fires custom handler, if hooked to generate sitemap for the type. */ do_action( 'wpseo_do_sitemap_' . $type ); return; } $this->bad_sitemap = true; } /** * Build the root sitemap (example.com/sitemap_index.xml) which lists sub-sitemaps for other content types. * * @return void */ public function build_root_map() { $links = []; $entries_per_page = $this->get_entries_per_page(); foreach ( $this->providers as $provider ) { $links = array_merge( $links, $provider->get_index_links( $entries_per_page ) ); } /** * Filter the sitemap links array before the index sitemap is built. * * @param array $links Array of sitemap links */ $links = apply_filters( 'wpseo_sitemap_index_links', $links ); if ( empty( $links ) ) { $this->bad_sitemap = true; $this->sitemap = ''; return; } $this->sitemap = $this->renderer->get_index( $links ); } /** * Spits out the XSL for the XML sitemap. * * @since 1.4.13 * * @param string $type Type to output. * * @return void */ public function xsl_output( $type ) { if ( $type !== 'main' ) { /** * Fires for the output of XSL for XML sitemaps, other than type "main". */ do_action( 'wpseo_xsl_' . $type ); return; } header( $this->http_protocol . ' 200 OK', true, 200 ); // Prevent the search engines from indexing the XML Sitemap. header( 'X-Robots-Tag: noindex, follow', true ); header( 'Content-Type: text/xml' ); // Make the browser cache this file properly. $expires = YEAR_IN_SECONDS; header( 'Pragma: public' ); header( 'Cache-Control: max-age=' . $expires ); header( 'Expires: ' . YoastSEO()->helpers->date->format_timestamp( ( time() + $expires ), 'D, d M Y H:i:s' ) . ' GMT' ); // Don't use WP_Filesystem() here because that's not initialized yet. See https://yoast.atlassian.net/browse/QAK-2043. readfile( WPSEO_PATH . 'css/main-sitemap.xsl' ); } /** * Spit out the generated sitemap. * * @return void */ public function output() { $this->send_headers(); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Escaping sitemap as either xml or html results in empty document. echo $this->renderer->get_output( $this->sitemap ); } /** * Makes a request to the sitemap index to cache it before the arrival of the search engines. * * @return void */ public function hit_sitemap_index() { if ( ! $this->cache->is_enabled() ) { return; } wp_remote_get( WPSEO_Sitemaps_Router::get_base_url( 'sitemap_index.xml' ) ); } /** * Get the GMT modification date for the last modified post in the post type. * * @since 3.2 * * @param string|array $post_types Post type or array of types. * @param bool $return_all Flag to return array of values. * * @return string|array|false */ public static function get_last_modified_gmt( $post_types, $return_all = false ) { global $wpdb; static $post_type_dates = null; if ( ! is_array( $post_types ) ) { $post_types = [ $post_types ]; } foreach ( $post_types as $post_type ) { if ( ! isset( $post_type_dates[ $post_type ] ) ) { // If we hadn't seen post type before. R. $post_type_dates = null; break; } } if ( $post_type_dates === null ) { $post_type_dates = []; $post_type_names = WPSEO_Post_Type::get_accessible_post_types(); if ( ! empty( $post_type_names ) ) { $post_statuses = array_map( 'esc_sql', self::get_post_statuses() ); $replacements = array_merge( [ 'post_type', 'post_modified_gmt', 'date', $wpdb->posts, 'post_status', ], $post_statuses, [ 'post_type' ], array_keys( $post_type_names ), [ 'post_type', 'date', ] ); //phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- We need to use a direct query here. //phpcs:disable WordPress.DB.DirectDatabaseQuery.NoCaching -- Reason: No relevant caches. $dates = $wpdb->get_results( //phpcs:disable WordPress.DB.PreparedSQLPlaceholders -- %i placeholder is still not recognized. $wpdb->prepare( ' SELECT %i, MAX(%i) AS %i FROM %i WHERE %i IN (' . implode( ', ', array_fill( 0, count( $post_statuses ), '%s' ) ) . ') AND %i IN (' . implode( ', ', array_fill( 0, count( $post_type_names ), '%s' ) ) . ') GROUP BY %i ORDER BY %i DESC ', $replacements ) ); foreach ( $dates as $obj ) { $post_type_dates[ $obj->post_type ] = $obj->date; } } } $dates = array_intersect_key( $post_type_dates, array_flip( $post_types ) ); if ( count( $dates ) > 0 ) { if ( $return_all ) { return $dates; } return max( $dates ); } return false; } /** * Get the modification date for the last modified post in the post type. * * @param array $post_types Post types to get the last modification date for. * * @return string */ public function get_last_modified( $post_types ) { return YoastSEO()->helpers->date->format( self::get_last_modified_gmt( $post_types ) ); } /** * Get the maximum number of entries per XML sitemap. * * @return int The maximum number of entries. */ protected function get_entries_per_page() { /** * Filter the maximum number of entries per XML sitemap. * * After changing the output of the filter, make sure that you disable and enable the * sitemaps to make sure the value is picked up for the sitemap cache. * * @param int $entries The maximum number of entries per XML sitemap. */ $entries = (int) apply_filters( 'wpseo_sitemap_entries_per_page', 1000 ); return $entries; } /** * Get post statuses for post_type or the root sitemap. * * @since 10.2 * * @param string $type Provide a type for a post_type sitemap, SITEMAP_INDEX_TYPE for the root sitemap. * * @return array List of post statuses. */ public static function get_post_statuses( $type = self::SITEMAP_INDEX_TYPE ) { /** * Filter post status list for sitemap query for the post type. * * @param array $post_statuses Post status list, defaults to array( 'publish' ). * @param string $type Post type or SITEMAP_INDEX_TYPE. */ $post_statuses = apply_filters( 'wpseo_sitemap_post_statuses', [ 'publish' ], $type ); if ( ! is_array( $post_statuses ) || empty( $post_statuses ) ) { $post_statuses = [ 'publish' ]; } if ( ( $type === self::SITEMAP_INDEX_TYPE || $type === 'attachment' ) && ! in_array( 'inherit', $post_statuses, true ) ) { $post_statuses[] = 'inherit'; } return $post_statuses; } /** * Sends all the required HTTP Headers. * * @return void */ private function send_headers() { if ( headers_sent() ) { return; } $headers = [ $this->http_protocol . ' 200 OK' => 200, // Prevent the search engines from indexing the XML Sitemap. 'X-Robots-Tag: noindex, follow' => '', 'Content-Type: text/xml; charset=' . esc_attr( $this->renderer->get_output_charset() ) => '', ]; /** * Filter the HTTP headers we send before an XML sitemap. * * @param array $headers The HTTP headers we're going to send out. */ $headers = apply_filters( 'wpseo_sitemap_http_headers', $headers ); foreach ( $headers as $header => $status ) { if ( is_numeric( $status ) ) { header( $header, true, $status ); continue; } header( $header, true ); } } } interface-sitemap-provider.php000066600000001443151733213030012511 0ustar00status_transition_bulk( $new_status, $old_status, $post ); return; } $post_type = get_post_type( $post ); wp_cache_delete( 'lastpostmodified:gmt:' . $post_type, 'timeinfo' ); // #17455. } /** * Notify Google of the updated sitemap. * * @deprecated 22.0 * @codeCoverageIgnore * * @return void */ public function ping_search_engines() { _deprecated_function( __METHOD__, 'Yoast SEO 22.0' ); } /** * While bulk importing, just save unique post_types. * * When importing is done, if we have a post_type that is saved in the sitemap * try to ping the search engines. * * @param string $new_status New post status. * @param string $old_status Old post status. * @param WP_Post $post Post object. * * @return void */ private function status_transition_bulk( $new_status, $old_status, $post ) { $this->importing_post_types[] = get_post_type( $post ); $this->importing_post_types = array_unique( $this->importing_post_types ); } /** * After import finished, walk through imported post_types and update info. * * @return void */ public function status_transition_bulk_finished() { if ( ! defined( 'WP_IMPORTING' ) ) { return; } if ( empty( $this->importing_post_types ) ) { return; } $ping_search_engines = false; foreach ( $this->importing_post_types as $post_type ) { wp_cache_delete( 'lastpostmodified:gmt:' . $post_type, 'timeinfo' ); // #17455. // Just have the cache deleted for nav_menu_item. if ( $post_type === 'nav_menu_item' ) { continue; } if ( WPSEO_Options::get( 'noindex-' . $post_type, false ) === false ) { $ping_search_engines = true; } } // Nothing to do. if ( $ping_search_engines === false ) { return; } if ( WP_CACHE ) { do_action( 'wpseo_hit_sitemap_index' ); } } } class-sitemaps-router.php000066600000010707151733213030011532 0ustar00classes->get( Deactivating_Yoast_Seo_Conditional::class )->is_met() ) { return; } add_action( 'yoast_add_dynamic_rewrite_rules', [ $this, 'add_rewrite_rules' ] ); add_filter( 'query_vars', [ $this, 'add_query_vars' ] ); add_filter( 'redirect_canonical', [ $this, 'redirect_canonical' ] ); add_action( 'template_redirect', [ $this, 'template_redirect' ], 0 ); } /** * Adds rewrite routes for sitemaps. * * @param Yoast_Dynamic_Rewrites $dynamic_rewrites Dynamic rewrites handler instance. * * @return void */ public function add_rewrite_rules( $dynamic_rewrites ) { $dynamic_rewrites->add_rule( 'sitemap_index\.xml$', 'index.php?sitemap=1', 'top' ); $dynamic_rewrites->add_rule( '([^/]+?)-sitemap([0-9]+)?\.xml$', 'index.php?sitemap=$matches[1]&sitemap_n=$matches[2]', 'top' ); $dynamic_rewrites->add_rule( '([a-z]+)?-?sitemap\.xsl$', 'index.php?yoast-sitemap-xsl=$matches[1]', 'top' ); } /** * Adds query variables for sitemaps. * * @param array $query_vars List of query variables to filter. * * @return array Filtered query variables. */ public function add_query_vars( $query_vars ) { $query_vars[] = 'sitemap'; $query_vars[] = 'sitemap_n'; $query_vars[] = 'yoast-sitemap-xsl'; return $query_vars; } /** * Sets up rewrite rules. * * @deprecated 21.8 * @codeCoverageIgnore * * @return void */ public function init() { _deprecated_function( __METHOD__, 'Yoast SEO 21.8' ); } /** * Stop trailing slashes on sitemap.xml URLs. * * @param string $redirect The redirect URL currently determined. * * @return bool|string */ public function redirect_canonical( $redirect ) { if ( get_query_var( 'sitemap' ) || get_query_var( 'yoast-sitemap-xsl' ) ) { return false; } return $redirect; } /** * Redirects sitemap.xml to sitemap_index.xml. * * @return void */ public function template_redirect() { if ( ! $this->needs_sitemap_index_redirect() ) { return; } YoastSEO()->helpers->redirect->do_safe_redirect( home_url( '/sitemap_index.xml' ), 301, 'Yoast SEO' ); } /** * Checks whether the current request needs to be redirected to sitemap_index.xml. * * @global WP_Query $wp_query Current query. * * @return bool True if redirect is needed, false otherwise. */ public function needs_sitemap_index_redirect() { global $wp_query; $protocol = 'http://'; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.MissingUnslash,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized if ( ! empty( $_SERVER['HTTPS'] ) && strtolower( $_SERVER['HTTPS'] ) === 'on' ) { $protocol = 'https://'; } $domain = ''; if ( isset( $_SERVER['SERVER_NAME'] ) ) { $domain = sanitize_text_field( wp_unslash( $_SERVER['SERVER_NAME'] ) ); } $path = ''; if ( isset( $_SERVER['REQUEST_URI'] ) ) { $path = sanitize_text_field( wp_unslash( $_SERVER['REQUEST_URI'] ) ); } // Due to different environment configurations, we need to check both SERVER_NAME and HTTP_HOST. $check_urls = [ $protocol . $domain . $path ]; if ( ! empty( $_SERVER['HTTP_HOST'] ) ) { $check_urls[] = $protocol . sanitize_text_field( wp_unslash( $_SERVER['HTTP_HOST'] ) ) . $path; } return $wp_query->is_404 && in_array( home_url( '/sitemap.xml' ), $check_urls, true ); } /** * Create base URL for the sitemap. * * @param string $page Page to append to the base URL. * * @return string base URL (incl page) */ public static function get_base_url( $page ) { global $wp_rewrite; $base = $wp_rewrite->using_index_permalinks() ? 'index.php/' : '/'; /** * Filter the base URL of the sitemaps. * * @param string $base The string that should be added to home_url() to make the full base URL. */ $base = apply_filters( 'wpseo_sitemaps_base_url', $base ); /* * Get the scheme from the configured home URL instead of letting WordPress * determine the scheme based on the requested URI. */ return home_url( $base . $page, wp_parse_url( get_option( 'home' ), PHP_URL_SCHEME ) ); } } class-taxonomy-sitemap-provider.php000066600000022263151733213030013535 0ustar00include_images = apply_filters( 'wpseo_xml_sitemap_include_images', true ); } /** * Check if provider supports given item type. * * @param string $type Type string to check for. * * @return bool */ public function handles_type( $type ) { $taxonomy = get_taxonomy( $type ); if ( $taxonomy === false || ! $this->is_valid_taxonomy( $taxonomy->name ) || ! $taxonomy->public ) { return false; } return true; } /** * Retrieves the links for the sitemap. * * @param int $max_entries Entries per sitemap. * * @return array */ public function get_index_links( $max_entries ) { $taxonomies = get_taxonomies( [ 'public' => true ], 'objects' ); if ( empty( $taxonomies ) ) { return []; } $taxonomy_names = array_filter( array_keys( $taxonomies ), [ $this, 'is_valid_taxonomy' ] ); $taxonomies = array_intersect_key( $taxonomies, array_flip( $taxonomy_names ) ); // Retrieve all the taxonomies and their terms so we can do a proper count on them. /** * Filter the setting of excluding empty terms from the XML sitemap. * * @param bool $exclude Defaults to true. * @param array $taxonomy_names Array of names for the taxonomies being processed. */ $hide_empty = apply_filters( 'wpseo_sitemap_exclude_empty_terms', true, $taxonomy_names ); $all_taxonomies = []; foreach ( $taxonomy_names as $taxonomy_name ) { /** * Filter the setting of excluding empty terms from the XML sitemap for a specific taxonomy. * * @param bool $exclude Defaults to the sitewide setting. * @param string $taxonomy_name The name of the taxonomy being processed. */ $hide_empty_tax = apply_filters( 'wpseo_sitemap_exclude_empty_terms_taxonomy', $hide_empty, $taxonomy_name ); $term_args = [ 'taxonomy' => $taxonomy_name, 'hide_empty' => $hide_empty_tax, 'fields' => 'ids', ]; $taxonomy_terms = get_terms( $term_args ); if ( count( $taxonomy_terms ) > 0 ) { $all_taxonomies[ $taxonomy_name ] = $taxonomy_terms; } } $index = []; foreach ( $taxonomies as $tax_name => $tax ) { if ( ! isset( $all_taxonomies[ $tax_name ] ) ) { // No eligible terms found. continue; } $total_count = ( isset( $all_taxonomies[ $tax_name ] ) ) ? count( $all_taxonomies[ $tax_name ] ) : 1; $max_pages = 1; if ( $total_count > $max_entries ) { $max_pages = (int) ceil( $total_count / $max_entries ); } $last_modified_gmt = WPSEO_Sitemaps::get_last_modified_gmt( $tax->object_type ); for ( $page_counter = 0; $page_counter < $max_pages; $page_counter++ ) { $current_page = ( $page_counter === 0 ) ? '' : ( $page_counter + 1 ); if ( ! is_array( $tax->object_type ) || count( $tax->object_type ) === 0 ) { continue; } $terms = array_splice( $all_taxonomies[ $tax_name ], 0, $max_entries ); if ( ! $terms ) { continue; } $args = [ 'post_type' => $tax->object_type, 'tax_query' => [ [ 'taxonomy' => $tax_name, 'terms' => $terms, ], ], 'orderby' => 'modified', 'order' => 'DESC', 'posts_per_page' => 1, ]; $query = new WP_Query( $args ); if ( $query->have_posts() ) { $date = $query->posts[0]->post_modified_gmt; } else { $date = $last_modified_gmt; } $index[] = [ 'loc' => WPSEO_Sitemaps_Router::get_base_url( $tax_name . '-sitemap' . $current_page . '.xml' ), 'lastmod' => $date, ]; } } return $index; } /** * Get set of sitemap link data. * * @param string $type Sitemap type. * @param int $max_entries Entries per sitemap. * @param int $current_page Current page of the sitemap. * * @return array * * @throws OutOfBoundsException When an invalid page is requested. */ public function get_sitemap_links( $type, $max_entries, $current_page ) { global $wpdb; $links = []; if ( ! $this->handles_type( $type ) ) { return $links; } $taxonomy = get_taxonomy( $type ); $steps = $max_entries; $offset = ( $current_page > 1 ) ? ( ( $current_page - 1 ) * $max_entries ) : 0; /** This filter is documented in inc/sitemaps/class-taxonomy-sitemap-provider.php */ $hide_empty = apply_filters( 'wpseo_sitemap_exclude_empty_terms', true, [ $taxonomy->name ] ); /** This filter is documented in inc/sitemaps/class-taxonomy-sitemap-provider.php */ $hide_empty_tax = apply_filters( 'wpseo_sitemap_exclude_empty_terms_taxonomy', $hide_empty, $taxonomy->name ); $terms = get_terms( [ 'taxonomy' => $taxonomy->name, 'hide_empty' => $hide_empty_tax, 'update_term_meta_cache' => false, 'offset' => $offset, 'number' => $steps, ] ); // If there are no terms fetched for this range, we are on an invalid page. if ( empty( $terms ) ) { throw new OutOfBoundsException( 'Invalid sitemap page requested' ); } $post_statuses = array_map( 'esc_sql', WPSEO_Sitemaps::get_post_statuses() ); $replacements = array_merge( [ 'post_modified_gmt', $wpdb->posts, $wpdb->term_relationships, 'object_id', 'ID', $wpdb->term_taxonomy, 'term_taxonomy_id', 'term_taxonomy_id', 'taxonomy', 'term_id', 'post_status', ], $post_statuses, [ 'post_password' ] ); /** * Filter: 'wpseo_exclude_from_sitemap_by_term_ids' - Allow excluding terms by ID. * * @param array $terms_to_exclude The terms to exclude. */ $terms_to_exclude = apply_filters( 'wpseo_exclude_from_sitemap_by_term_ids', [] ); foreach ( $terms as $term ) { if ( in_array( $term->term_id, $terms_to_exclude, true ) ) { continue; } $url = []; $tax_noindex = WPSEO_Taxonomy_Meta::get_term_meta( $term, $term->taxonomy, 'noindex' ); if ( $tax_noindex === 'noindex' ) { continue; } $canonical = WPSEO_Taxonomy_Meta::get_term_meta( $term, $term->taxonomy, 'canonical' ); $url['loc'] = get_term_link( $term, $term->taxonomy ); if ( is_string( $canonical ) && $canonical !== '' && $canonical !== $url['loc'] ) { continue; } $current_replacements = $replacements; array_splice( $current_replacements, 9, 0, $term->taxonomy ); array_splice( $current_replacements, 11, 0, $term->term_id ); //phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- We need to use a direct query here. //phpcs:disable WordPress.DB.DirectDatabaseQuery.NoCaching -- Reason: No relevant caches. $url['mod'] = $wpdb->get_var( //phpcs:disable WordPress.DB.PreparedSQLPlaceholders -- %i placeholder is still not recognized. $wpdb->prepare( ' SELECT MAX(p.%i) AS lastmod FROM %i AS p INNER JOIN %i AS term_rel ON term_rel.%i = p.%i INNER JOIN %i AS term_tax ON term_tax.%i = term_rel.%i AND term_tax.%i = %s AND term_tax.%i = %d WHERE p.%i IN (' . implode( ', ', array_fill( 0, count( $post_statuses ), '%s' ) ) . ") AND p.%i = '' ", $current_replacements ) ); if ( $this->include_images ) { $url['images'] = $this->get_image_parser()->get_term_images( $term ); } // Deprecated, kept for backwards data compat. R. $url['chf'] = 'daily'; $url['pri'] = 1; /** This filter is documented at inc/sitemaps/class-post-type-sitemap-provider.php */ $url = apply_filters( 'wpseo_sitemap_entry', $url, 'term', $term ); if ( ! empty( $url ) ) { $links[] = $url; } } return $links; } /** * Check if taxonomy by name is valid to appear in sitemaps. * * @param string $taxonomy_name Taxonomy name to check. * * @return bool */ public function is_valid_taxonomy( $taxonomy_name ) { if ( WPSEO_Options::get( "noindex-tax-{$taxonomy_name}" ) === true ) { return false; } if ( in_array( $taxonomy_name, [ 'link_category', 'nav_menu', 'wp_pattern_category' ], true ) ) { return false; } if ( $taxonomy_name === 'post_format' && WPSEO_Options::get( 'disable-post_format', false ) ) { return false; } /** * Filter to exclude the taxonomy from the XML sitemap. * * @param bool $exclude Defaults to false. * @param string $taxonomy_name Name of the taxonomy to exclude.. */ if ( apply_filters( 'wpseo_sitemap_exclude_taxonomy', false, $taxonomy_name ) ) { return false; } return true; } /** * Get the Image Parser. * * @return WPSEO_Sitemap_Image_Parser */ protected function get_image_parser() { if ( ! isset( self::$image_parser ) ) { self::$image_parser = new WPSEO_Sitemap_Image_Parser(); } return self::$image_parser; } } class-sitemap-cache-data.php000066600000011376151733213030012004 0ustar00sitemap = $sitemap; /* * Empty sitemap is not usable. */ if ( ! empty( $sitemap ) ) { $this->set_status( self::OK ); } else { $this->set_status( self::ERROR ); } } /** * Set the status of the sitemap, is it usable. * * @param bool|string $usable Is the sitemap usable or not. * * @return void */ public function set_status( $usable ) { if ( $usable === self::OK ) { $this->status = self::OK; return; } if ( $usable === self::ERROR ) { $this->status = self::ERROR; $this->sitemap = ''; return; } $this->status = self::UNKNOWN; } /** * Is the sitemap usable. * * @return bool True if usable, False if bad or unknown. */ public function is_usable() { return $this->status === self::OK; } /** * Get the XML content of the sitemap. * * @return string The content of the sitemap. */ public function get_sitemap() { return $this->sitemap; } /** * Get the status of the sitemap. * * @return string Status of the sitemap, 'ok'/'error'/'unknown'. */ public function get_status() { return $this->status; } /** * String representation of object. * * {@internal This magic method is only "magic" as of PHP 7.4 in which the magic method was introduced.} * * @link https://www.php.net/language.oop5.magic#object.serialize * @link https://wiki.php.net/rfc/custom_object_serialization * * @since 17.8.0 * * @return array The data to be serialized. */ public function __serialize() { // phpcs:ignore PHPCompatibility.FunctionNameRestrictions.NewMagicMethods.__serializeFound $data = [ 'status' => $this->status, 'xml' => $this->sitemap, ]; return $data; } /** * Constructs the object. * * {@internal This magic method is only "magic" as of PHP 7.4 in which the magic method was introduced.} * * @link https://www.php.net/language.oop5.magic#object.serialize * @link https://wiki.php.net/rfc/custom_object_serialization * * @since 17.8.0 * * @param array $data The unserialized data to use to (re)construct the object. * * @return void */ public function __unserialize( $data ) { // phpcs:ignore PHPCompatibility.FunctionNameRestrictions.NewMagicMethods.__unserializeFound $this->set_sitemap( $data['xml'] ); $this->set_status( $data['status'] ); } /** * String representation of object. * * {@internal The magic methods take precedence over the Serializable interface. * This means that in practice, this method will now only be called on PHP < 7.4. * For PHP 7.4 and higher, the magic methods will be used instead.} * * {@internal The Serializable interface is being phased out, in favour of the magic methods. * This method should be deprecated and removed and the class should no longer * implement the `Serializable` interface. * This change, however, can't be made until the minimum PHP version goes up to PHP 7.4 or higher.} * * @link http://php.net/manual/en/serializable.serialize.php * @link https://wiki.php.net/rfc/phase_out_serializable * * @since 5.1.0 * * @return string The string representation of the object or null in C-format. */ public function serialize() { return serialize( $this->__serialize() ); } /** * Constructs the object. * * {@internal The magic methods take precedence over the Serializable interface. * This means that in practice, this method will now only be called on PHP < 7.4. * For PHP 7.4 and higher, the magic methods will be used instead.} * * {@internal The Serializable interface is being phased out, in favour of the magic methods. * This method should be deprecated and removed and the class should no longer * implement the `Serializable` interface. * This change, however, can't be made until the minimum PHP version goes up to PHP 7.4 or higher.} * * @link http://php.net/manual/en/serializable.unserialize.php * @link https://wiki.php.net/rfc/phase_out_serializable * * @since 5.1.0 * * @param string $data The string representation of the object in C or O-format. * * @return void */ public function unserialize( $data ) { $data = unserialize( $data ); $this->__unserialize( $data ); } } class-sitemaps-cache-validator.php000066600000022651151733213030013241 0ustar00 $max_length ) { if ( $max_length < 15 ) { /* * If this happens the most likely cause is a page number that is too high. * * So this would not happen unintentionally. * Either by trying to cause a high server load, finding backdoors or misconfiguration. */ throw new OutOfRangeException( __( 'Trying to build the sitemap cache key, but the postfix and prefix combination leaves too little room to do this. You are probably requesting a page that is way out of the expected range.', 'wordpress-seo' ) ); } $half = ( $max_length / 2 ); $first_part = substr( $type, 0, ( ceil( $half ) - 1 ) ); $last_part = substr( $type, ( 1 - floor( $half ) ) ); $type = $first_part . '..' . $last_part; } return $type; } /** * Invalidate sitemap cache. * * @since 3.2 * * @param string|null $type The type to get the key for. Null for all caches. * * @return void */ public static function invalidate_storage( $type = null ) { // Global validator gets cleared when no type is provided. $old_validator = null; // Get the current type validator. if ( $type !== null ) { $old_validator = self::get_validator( $type ); } // Refresh validator. self::create_validator( $type ); if ( ! wp_using_ext_object_cache() ) { // Clean up current cache from the database. self::cleanup_database( $type, $old_validator ); } // External object cache pushes old and unretrieved items out by itself so we don't have to do anything for that. } /** * Cleanup invalidated database cache. * * @since 3.2 * * @param string|null $type The type of sitemap to clear cache for. * @param string|null $validator The validator to clear cache of. * * @return void */ public static function cleanup_database( $type = null, $validator = null ) { global $wpdb; if ( $type === null ) { // Clear all cache if no type is provided. $like = sprintf( '%s%%', self::STORAGE_KEY_PREFIX ); } else { // Clear type cache for all type keys. $like = sprintf( '%1$s%2$s_%%', self::STORAGE_KEY_PREFIX, $type ); } /* * Add slashes to the LIKE "_" single character wildcard. * * We can't use `esc_like` here because we need the % in the query. */ $where = []; $where[] = sprintf( "option_name LIKE '%s'", addcslashes( '_transient_' . $like, '_' ) ); $where[] = sprintf( "option_name LIKE '%s'", addcslashes( '_transient_timeout_' . $like, '_' ) ); // Delete transients. //phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- We need to use a direct query here. //phpcs:disable WordPress.DB.DirectDatabaseQuery.NoCaching -- Reason: No relevant caches. $wpdb->query( $wpdb->prepare( //phpcs:disable WordPress.DB.PreparedSQLPlaceholders -- %i placeholder is still not recognized. 'DELETE FROM %i WHERE ' . implode( ' OR ', array_fill( 0, count( $where ), '%s' ) ), array_merge( [ $wpdb->options ], $where ) ) ); wp_cache_delete( 'alloptions', 'options' ); } /** * Get the current cache validator. * * Without the type the global validator is returned. * This can invalidate -all- keys in cache at once. * * With the type parameter the validator for that specific type can be invalidated. * * @since 3.2 * * @param string $type Provide a type for a specific type validator, empty for global validator. * * @return string|null The validator for the supplied type. */ public static function get_validator( $type = '' ) { $key = self::get_validator_key( $type ); $current = get_option( $key, null ); if ( $current !== null ) { return $current; } if ( self::create_validator( $type ) ) { return self::get_validator( $type ); } return null; } /** * Get the cache validator option key for the specified type. * * @since 3.2 * * @param string $type Provide a type for a specific type validator, empty for global validator. * * @return string Validator to be used to generate the cache key. */ public static function get_validator_key( $type = '' ) { if ( empty( $type ) ) { return self::VALIDATION_GLOBAL_KEY; } return sprintf( self::VALIDATION_TYPE_KEY_FORMAT, $type ); } /** * Refresh the cache validator value. * * @since 3.2 * * @param string $type Provide a type for a specific type validator, empty for global validator. * * @return bool True if validator key has been saved as option. */ public static function create_validator( $type = '' ) { $key = self::get_validator_key( $type ); // Generate new validator. $microtime = microtime(); // Remove space. list( $milliseconds, $seconds ) = explode( ' ', $microtime ); // Transients are purged every 24h. $seconds = ( $seconds % DAY_IN_SECONDS ); $milliseconds = intval( substr( $milliseconds, 2, 3 ), 10 ); // Combine seconds and milliseconds and convert to integer. $validator = intval( $seconds . '' . $milliseconds, 10 ); // Apply base 61 encoding. $compressed = self::convert_base10_to_base61( $validator ); return update_option( $key, $compressed, false ); } /** * Encode to base61 format. * * This is base64 (numeric + alpha + alpha upper case) without the 0. * * @since 3.2 * * @param int $base10 The number that has to be converted to base 61. * * @return string Base 61 converted string. * * @throws InvalidArgumentException When the input is not an integer. */ public static function convert_base10_to_base61( $base10 ) { if ( ! is_int( $base10 ) ) { throw new InvalidArgumentException( __( 'Expected an integer as input.', 'wordpress-seo' ) ); } // Characters that will be used in the conversion. $characters = '123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'; $length = strlen( $characters ); $remainder = $base10; $output = ''; do { // Building from right to left in the result. $index = ( $remainder % $length ); // Prepend the character to the output. $output = $characters[ $index ] . $output; // Determine the remainder after removing the applied number. $remainder = floor( $remainder / $length ); // Keep doing it until we have no remainder left. } while ( $remainder ); return $output; } } class-sitemaps-renderer.php000066600000022551151733213030012020 0ustar00get_xsl_url() ); $this->stylesheet = ''; $this->charset = get_bloginfo( 'charset' ); $this->output_charset = $this->charset; if ( $this->charset !== 'UTF-8' && function_exists( 'mb_list_encodings' ) && in_array( $this->charset, mb_list_encodings(), true ) ) { $this->output_charset = 'UTF-8'; } $this->needs_conversion = $this->output_charset !== $this->charset; } /** * Builds the sitemap index. * * @param array $links Set of sitemaps index links. * * @return string */ public function get_index( $links ) { $xml = '' . "\n"; foreach ( $links as $link ) { $xml .= $this->sitemap_index_url( $link ); } /** * Filter to append sitemaps to the index. * * @param string $index String to append to sitemaps index, defaults to empty. */ $xml .= apply_filters( 'wpseo_sitemap_index', '' ); $xml .= ''; return $xml; } /** * Builds the sitemap. * * @param array $links Set of sitemap links. * @param string $type Sitemap type. * @param int $current_page Current sitemap page number. * * @return string */ public function get_sitemap( $links, $type, $current_page ) { $urlset = '' . "\n"; /** * Filters the `urlset` for all sitemaps. * * @param string $urlset The output for the sitemap's `urlset`. */ $urlset = apply_filters( 'wpseo_sitemap_urlset', $urlset ); /** * Filters the `urlset` for a sitemap by type. * * @param string $urlset The output for the sitemap's `urlset`. */ $xml = apply_filters( "wpseo_sitemap_{$type}_urlset", $urlset ); foreach ( $links as $url ) { $xml .= $this->sitemap_url( $url ); } /** * Filter to add extra URLs to the XML sitemap by type. * * Only runs for the first page, not on all. * * @param string $content String content to add, defaults to empty. */ if ( $current_page === 1 ) { $xml .= apply_filters( "wpseo_sitemap_{$type}_content", '' ); } $xml .= ''; return $xml; } /** * Produce final XML output with debug information. * * @param string $sitemap Sitemap XML. * * @return string */ public function get_output( $sitemap ) { $output = 'output_charset ) . '"?>'; if ( $this->stylesheet ) { /** * Filter the stylesheet URL for the XML sitemap. * * @param string $stylesheet Stylesheet URL. */ $output .= apply_filters( 'wpseo_stylesheet_url', $this->stylesheet ) . "\n"; } $output .= $sitemap; $output .= "\n"; return $output; } /** * Get charset for the output. * * @return string */ public function get_output_charset() { return $this->output_charset; } /** * Set a custom stylesheet for this sitemap. Set to empty to just remove the default stylesheet. * * @param string $stylesheet Full XML-stylesheet declaration. * * @return void */ public function set_stylesheet( $stylesheet ) { $this->stylesheet = $stylesheet; } /** * Build the `` tag for a given URL. * * @param array $url Array of parts that make up this entry. * * @return string */ protected function sitemap_index_url( $url ) { $date = null; if ( ! empty( $url['lastmod'] ) ) { $date = YoastSEO()->helpers->date->format( $url['lastmod'] ); } $url['loc'] = htmlspecialchars( $url['loc'], ENT_COMPAT, $this->output_charset, false ); $output = "\t\n"; $output .= "\t\t" . $url['loc'] . "\n"; $output .= empty( $date ) ? '' : "\t\t" . htmlspecialchars( $date, ENT_COMPAT, $this->output_charset, false ) . "\n"; $output .= "\t\n"; return $output; } /** * Build the `` tag for a given URL. * * Public access for backwards compatibility reasons. * * @param array $url Array of parts that make up this entry. * * @return string */ public function sitemap_url( $url ) { $date = null; if ( ! empty( $url['mod'] ) ) { // Create a DateTime object date in the correct timezone. $date = YoastSEO()->helpers->date->format( $url['mod'] ); } $output = "\t\n"; $output .= "\t\t" . $this->encode_and_escape( $url['loc'] ) . "\n"; $output .= empty( $date ) ? '' : "\t\t" . htmlspecialchars( $date, ENT_COMPAT, $this->output_charset, false ) . "\n"; if ( empty( $url['images'] ) ) { $url['images'] = []; } foreach ( $url['images'] as $img ) { if ( empty( $img['src'] ) ) { continue; } $output .= "\t\t\n"; $output .= "\t\t\t" . $this->encode_and_escape( $img['src'] ) . "\n"; $output .= "\t\t\n"; } unset( $img ); $output .= "\t\n"; /** * Filters the output for the sitemap URL tag. * * @param string $output The output for the sitemap url tag. * @param array $url The sitemap URL array on which the output is based. */ return apply_filters( 'wpseo_sitemap_url', $output, $url ); } /** * Ensure the URL is encoded per RFC3986 and correctly escaped for use in an XML sitemap. * * This method works around a two quirks in esc_url(): * 1. `esc_url()` leaves schema-relative URLs alone, while according to the sitemap specs, * the URL must always begin with a protocol. * 2. `esc_url()` escapes ampersands as `&` instead of the more common `&`. * According to the specs, `&` should be used, and even though this shouldn't * really make a difference in practice, to quote Jono: "I'd be nervous about & * given how many weird and wonderful things eat sitemaps", so better safe than sorry. * * @link https://www.sitemaps.org/protocol.html#xmlTagDefinitions * @link https://www.sitemaps.org/protocol.html#escaping * @link https://developer.wordpress.org/reference/functions/esc_url/ * * @param string $url URL to encode and escape. * * @return string */ protected function encode_and_escape( $url ) { $url = $this->encode_url_rfc3986( $url ); $url = esc_url( $url ); $url = str_replace( '&', '&', $url ); $url = str_replace( ''', ''', $url ); if ( strpos( $url, '//' ) === 0 ) { // Schema-relative URL for which esc_url() does not add a scheme. $url = 'http:' . $url; } return $url; } /** * Apply some best effort conversion to comply with RFC3986. * * @param string $url URL to encode. * * @return string */ protected function encode_url_rfc3986( $url ) { if ( filter_var( $url, FILTER_VALIDATE_URL ) ) { return $url; } $path = wp_parse_url( $url, PHP_URL_PATH ); if ( ! empty( $path ) && $path !== '/' ) { $encoded_path = explode( '/', $path ); // First decode the path, to prevent double encoding. $encoded_path = array_map( 'rawurldecode', $encoded_path ); $encoded_path = array_map( 'rawurlencode', $encoded_path ); $encoded_path = implode( '/', $encoded_path ); $url = str_replace( $path, $encoded_path, $url ); } $query = wp_parse_url( $url, PHP_URL_QUERY ); if ( ! empty( $query ) ) { parse_str( $query, $parsed_query ); $parsed_query = http_build_query( $parsed_query, '', '&', PHP_QUERY_RFC3986 ); $url = str_replace( $query, $parsed_query, $url ); } return $url; } /** * Retrieves the XSL URL that should be used in the current environment * * When home_url and site_url are not the same, the home_url should be used. * This is because the XSL needs to be served from the same domain, protocol and port * as the XML file that is loading it. * * @return string The XSL URL that needs to be used. */ protected function get_xsl_url() { if ( home_url() !== site_url() ) { return apply_filters( 'wpseo_sitemap_public_url', home_url( 'main-sitemap.xsl' ) ); } /* * Fallback to circumvent a cross-domain security problem when the XLS file is * loaded from a different (sub)domain. */ if ( strpos( plugins_url(), home_url() ) !== 0 ) { return home_url( 'main-sitemap.xsl' ); } return plugin_dir_url( WPSEO_FILE ) . 'css/main-sitemap.xsl'; } } class-post-type-sitemap-provider.php000066600000046607151733213030013633 0ustar00include_images = apply_filters( 'wpseo_xml_sitemap_include_images', true ); } /** * Get the Image Parser. * * @return WPSEO_Sitemap_Image_Parser */ protected function get_image_parser() { if ( ! isset( self::$image_parser ) ) { self::$image_parser = new WPSEO_Sitemap_Image_Parser(); } return self::$image_parser; } /** * Gets the parsed home url. * * @return array The home url, as parsed by wp_parse_url. */ protected function get_parsed_home_url() { if ( ! isset( self::$parsed_home_url ) ) { self::$parsed_home_url = wp_parse_url( home_url() ); } return self::$parsed_home_url; } /** * Check if provider supports given item type. * * @param string $type Type string to check for. * * @return bool */ public function handles_type( $type ) { return post_type_exists( $type ); } /** * Retrieves the sitemap links. * * @param int $max_entries Entries per sitemap. * * @return array */ public function get_index_links( $max_entries ) { global $wpdb; $post_types = WPSEO_Post_Type::get_accessible_post_types(); $post_types = array_filter( $post_types, [ $this, 'is_valid_post_type' ] ); $last_modified_times = WPSEO_Sitemaps::get_last_modified_gmt( $post_types, true ); $index = []; foreach ( $post_types as $post_type ) { $total_count = $this->get_post_type_count( $post_type ); if ( $total_count === 0 ) { continue; } $max_pages = 1; if ( $total_count > $max_entries ) { $max_pages = (int) ceil( $total_count / $max_entries ); } $all_dates = []; if ( $max_pages > 1 ) { $all_dates = version_compare( $wpdb->db_version(), '8.0', '>=' ) ? $this->get_all_dates_using_with_clause( $post_type, $max_entries ) : $this->get_all_dates( $post_type, $max_entries ); } for ( $page_counter = 0; $page_counter < $max_pages; $page_counter++ ) { $current_page = ( $page_counter === 0 ) ? '' : ( $page_counter + 1 ); $date = false; if ( empty( $current_page ) || $current_page === $max_pages ) { if ( ! empty( $last_modified_times[ $post_type ] ) ) { $date = $last_modified_times[ $post_type ]; } } else { $date = $all_dates[ $page_counter ]; } $index[] = [ 'loc' => WPSEO_Sitemaps_Router::get_base_url( $post_type . '-sitemap' . $current_page . '.xml' ), 'lastmod' => $date, ]; } } return $index; } /** * Get set of sitemap link data. * * @param string $type Sitemap type. * @param int $max_entries Entries per sitemap. * @param int $current_page Current page of the sitemap. * * @return array * * @throws OutOfBoundsException When an invalid page is requested. */ public function get_sitemap_links( $type, $max_entries, $current_page ) { $links = []; $post_type = $type; if ( ! $this->is_valid_post_type( $post_type ) ) { throw new OutOfBoundsException( 'Invalid sitemap page requested' ); } $steps = min( 100, $max_entries ); $offset = ( $current_page > 1 ) ? ( ( $current_page - 1 ) * $max_entries ) : 0; $total = ( $offset + $max_entries ); $post_type_entries = $this->get_post_type_count( $post_type ); if ( $total > $post_type_entries ) { $total = $post_type_entries; } if ( $current_page === 1 ) { $links = array_merge( $links, $this->get_first_links( $post_type ) ); } // If total post type count is lower than the offset, an invalid page is requested. if ( $post_type_entries < $offset ) { throw new OutOfBoundsException( 'Invalid sitemap page requested' ); } if ( $post_type_entries === 0 ) { return $links; } $posts_to_exclude = $this->get_excluded_posts( $type ); while ( $total > $offset ) { $posts = $this->get_posts( $post_type, $steps, $offset ); $offset += $steps; if ( empty( $posts ) ) { continue; } foreach ( $posts as $post ) { if ( in_array( $post->ID, $posts_to_exclude, true ) ) { continue; } if ( WPSEO_Meta::get_value( 'meta-robots-noindex', $post->ID ) === '1' ) { continue; } $url = $this->get_url( $post ); if ( ! isset( $url['loc'] ) ) { continue; } /** * Filter URL entry before it gets added to the sitemap. * * @param array $url Array of URL parts. * @param string $type URL type. * @param object $post Data object for the URL. */ $url = apply_filters( 'wpseo_sitemap_entry', $url, 'post', $post ); if ( ! empty( $url ) ) { $links[] = $url; } } unset( $post, $url ); } return $links; } /** * Check for relevant post type before invalidation. * * @param int $post_id Post ID to possibly invalidate for. * * @return void */ public function save_post( $post_id ) { if ( $this->is_valid_post_type( get_post_type( $post_id ) ) ) { WPSEO_Sitemaps_Cache::invalidate_post( $post_id ); } } /** * Check if post type should be present in sitemaps. * * @param string $post_type Post type string to check for. * * @return bool */ public function is_valid_post_type( $post_type ) { if ( ! WPSEO_Post_Type::is_post_type_accessible( $post_type ) || ! WPSEO_Post_Type::is_post_type_indexable( $post_type ) ) { return false; } /** * Filter decision if post type is excluded from the XML sitemap. * * @param bool $exclude Default false. * @param string $post_type Post type name. */ if ( apply_filters( 'wpseo_sitemap_exclude_post_type', false, $post_type ) ) { return false; } return true; } /** * Retrieves a list with the excluded post ids. * * @param string $post_type Post type. * * @return array Array with post ids to exclude. */ protected function get_excluded_posts( $post_type ) { $excluded_posts_ids = []; $page_on_front_id = ( $post_type === 'page' ) ? (int) get_option( 'page_on_front' ) : 0; if ( $page_on_front_id > 0 ) { $excluded_posts_ids[] = $page_on_front_id; } /** * Filter: 'wpseo_exclude_from_sitemap_by_post_ids' - Allow extending and modifying the posts to exclude. * * @param array $posts_to_exclude The posts to exclude. */ $excluded_posts_ids = apply_filters( 'wpseo_exclude_from_sitemap_by_post_ids', $excluded_posts_ids ); if ( ! is_array( $excluded_posts_ids ) ) { $excluded_posts_ids = []; } $excluded_posts_ids = array_map( 'intval', $excluded_posts_ids ); $page_for_posts_id = ( $post_type === 'page' ) ? (int) get_option( 'page_for_posts' ) : 0; if ( $page_for_posts_id > 0 ) { $excluded_posts_ids[] = $page_for_posts_id; } return array_unique( $excluded_posts_ids ); } /** * Get count of posts for post type. * * @param string $post_type Post type to retrieve count for. * * @return int */ protected function get_post_type_count( $post_type ) { global $wpdb; /** * Filter JOIN query part for type count of post type. * * @param string $join SQL part, defaults to empty string. * @param string $post_type Post type name. */ $join_filter = apply_filters( 'wpseo_typecount_join', '', $post_type ); /** * Filter WHERE query part for type count of post type. * * @param string $where SQL part, defaults to empty string. * @param string $post_type Post type name. */ $where_filter = apply_filters( 'wpseo_typecount_where', '', $post_type ); $where = $this->get_sql_where_clause( $post_type ); $sql = " SELECT COUNT({$wpdb->posts}.ID) FROM {$wpdb->posts} {$join_filter} {$where} {$where_filter} "; return (int) $wpdb->get_var( $sql ); } /** * Produces set of links to prepend at start of first sitemap page. * * @param string $post_type Post type to produce links for. * * @return array */ protected function get_first_links( $post_type ) { $links = []; $archive_url = false; if ( $post_type === 'page' ) { $page_on_front_id = (int) get_option( 'page_on_front' ); if ( $page_on_front_id > 0 ) { $front_page = $this->get_url( get_post( $page_on_front_id ) ); } if ( empty( $front_page ) ) { $front_page = [ 'loc' => YoastSEO()->helpers->url->home(), ]; } // Deprecated, kept for backwards data compat. R. $front_page['chf'] = 'daily'; $front_page['pri'] = 1; $images = ( $front_page['images'] ?? [] ); /** * Filter images to be included for the term in XML sitemap. * * @param array $images Array of image items. * @return array $image_list Array of image items. */ $image_list = apply_filters( 'wpseo_sitemap_urlimages_front_page', $images ); if ( is_array( $image_list ) ) { $front_page['images'] = $image_list; } $links[] = $front_page; } elseif ( $post_type !== 'page' ) { /** * Filter the URL Yoast SEO uses in the XML sitemap for this post type archive. * * @param string $archive_url The URL of this archive * @param string $post_type The post type this archive is for. */ $archive_url = apply_filters( 'wpseo_sitemap_post_type_archive_link', $this->get_post_type_archive_link( $post_type ), $post_type ); } if ( $archive_url ) { $links[] = [ 'loc' => $archive_url, 'mod' => WPSEO_Sitemaps::get_last_modified_gmt( $post_type ), // Deprecated, kept for backwards data compat. R. 'chf' => 'daily', 'pri' => 1, ]; } /** * Filters the first post type links. * * @param array $links The first post type links. * @param string $post_type The post type this archive is for. */ return apply_filters( 'wpseo_sitemap_post_type_first_links', $links, $post_type ); } /** * Get URL for a post type archive. * * @since 5.3 * * @param string $post_type Post type. * * @return string|bool URL or false if it should be excluded. */ protected function get_post_type_archive_link( $post_type ) { $pt_archive_page_id = -1; if ( $post_type === 'post' ) { if ( get_option( 'show_on_front' ) === 'posts' ) { return YoastSEO()->helpers->url->home(); } $pt_archive_page_id = (int) get_option( 'page_for_posts' ); // Post archive should be excluded if posts page isn't set. if ( $pt_archive_page_id <= 0 ) { return false; } } if ( ! $this->is_post_type_archive_indexable( $post_type, $pt_archive_page_id ) ) { return false; } return get_post_type_archive_link( $post_type ); } /** * Determines whether a post type archive is indexable. * * @since 11.5 * * @param string $post_type Post type. * @param int $archive_page_id The page id. * * @return bool True when post type archive is indexable. */ protected function is_post_type_archive_indexable( $post_type, $archive_page_id = -1 ) { if ( WPSEO_Options::get( 'noindex-ptarchive-' . $post_type, false ) ) { return false; } /** * Filter the page which is dedicated to this post type archive. * * @since 9.3 * * @param string $archive_page_id The post_id of the page. * @param string $post_type The post type this archive is for. */ $archive_page_id = (int) apply_filters( 'wpseo_sitemap_page_for_post_type_archive', $archive_page_id, $post_type ); if ( $archive_page_id > 0 && WPSEO_Meta::get_value( 'meta-robots-noindex', $archive_page_id ) === '1' ) { return false; } return true; } /** * Retrieve set of posts with optimized query routine. * * @param string $post_type Post type to retrieve. * @param int $count Count of posts to retrieve. * @param int $offset Starting offset. * * @return object[] */ protected function get_posts( $post_type, $count, $offset ) { global $wpdb; static $filters = []; if ( ! isset( $filters[ $post_type ] ) ) { // Make sure you're wpdb->preparing everything you throw into this!! $filters[ $post_type ] = [ /** * Filter JOIN query part for the post type. * * @param string $join SQL part, defaults to false. * @param string $post_type Post type name. */ 'join' => apply_filters( 'wpseo_posts_join', false, $post_type ), /** * Filter WHERE query part for the post type. * * @param string $where SQL part, defaults to false. * @param string $post_type Post type name. */ 'where' => apply_filters( 'wpseo_posts_where', false, $post_type ), ]; } $join_filter = $filters[ $post_type ]['join']; $where_filter = $filters[ $post_type ]['where']; $where = $this->get_sql_where_clause( $post_type ); /* * Optimized query per this thread: * {@link http://wordpress.org/support/topic/plugin-wordpress-seo-by-yoast-performance-suggestion}. * Also see {@link http://explainextended.com/2009/10/23/mysql-order-by-limit-performance-late-row-lookups/}. */ $sql = " SELECT l.ID, post_title, post_content, post_name, post_parent, post_author, post_status, post_modified_gmt, post_date, post_date_gmt FROM ( SELECT {$wpdb->posts}.ID FROM {$wpdb->posts} {$join_filter} {$where} {$where_filter} ORDER BY {$wpdb->posts}.post_modified ASC LIMIT %d OFFSET %d ) o JOIN {$wpdb->posts} l ON l.ID = o.ID "; $posts = $wpdb->get_results( $wpdb->prepare( $sql, $count, $offset ) ); $post_ids = []; foreach ( $posts as $post_index => $post ) { $post->post_type = $post_type; $sanitized_post = sanitize_post( $post, 'raw' ); $posts[ $post_index ] = new WP_Post( $sanitized_post ); $post_ids[] = $sanitized_post->ID; } update_meta_cache( 'post', $post_ids ); return $posts; } /** * Constructs an SQL where clause for a given post type. * * @param string $post_type Post type slug. * * @return string */ protected function get_sql_where_clause( $post_type ) { global $wpdb; $join = ''; $post_statuses = array_map( 'esc_sql', WPSEO_Sitemaps::get_post_statuses( $post_type ) ); $status_where = "{$wpdb->posts}.post_status IN ('" . implode( "','", $post_statuses ) . "')"; // Based on WP_Query->get_posts(). R. if ( $post_type === 'attachment' ) { $join = " LEFT JOIN {$wpdb->posts} AS p2 ON ({$wpdb->posts}.post_parent = p2.ID) "; $parent_statuses = array_diff( $post_statuses, [ 'inherit' ] ); $status_where = "p2.post_status IN ('" . implode( "','", $parent_statuses ) . "') AND p2.post_password = ''"; } $where_clause = " {$join} WHERE {$status_where} AND {$wpdb->posts}.post_type = %s AND {$wpdb->posts}.post_password = '' AND {$wpdb->posts}.post_date != '0000-00-00 00:00:00' "; return $wpdb->prepare( $where_clause, $post_type ); } /** * Produce array of URL parts for given post object. * * @param object $post Post object to get URL parts for. * * @return array|bool */ protected function get_url( $post ) { $url = []; /** * Filter the URL Yoast SEO uses in the XML sitemap. * * Note that only absolute local URLs are allowed as the check after this removes external URLs. * * @param string $url URL to use in the XML sitemap * @param object $post Post object for the URL. */ $url['loc'] = apply_filters( 'wpseo_xml_sitemap_post_url', get_permalink( $post ), $post ); $link_type = YoastSEO()->helpers->url->get_link_type( wp_parse_url( $url['loc'] ), $this->get_parsed_home_url() ); /* * Do not include external URLs. * * {@link https://wordpress.org/plugins/page-links-to/} can rewrite permalinks to external URLs. */ if ( $link_type === SEO_Links::TYPE_EXTERNAL ) { return false; } $modified = max( $post->post_modified_gmt, $post->post_date_gmt ); if ( $modified !== '0000-00-00 00:00:00' ) { $url['mod'] = $modified; } $url['chf'] = 'daily'; // Deprecated, kept for backwards data compat. R. $canonical = WPSEO_Meta::get_value( 'canonical', $post->ID ); if ( $canonical !== '' && $canonical !== $url['loc'] ) { /* * Let's assume that if a canonical is set for this page and it's different from * the URL of this post, that page is either already in the XML sitemap OR is on * an external site, either way, we shouldn't include it here. */ return false; } unset( $canonical ); $url['pri'] = 1; // Deprecated, kept for backwards data compat. R. if ( $this->include_images ) { $url['images'] = $this->get_image_parser()->get_images( $post ); } return $url; } /** * Get all dates for a post type by using the WITH clause for performance. * * @param string $post_type Post type to retrieve dates for. * @param int $max_entries Maximum number of entries to retrieve. * * @return array Array of dates. */ private function get_all_dates_using_with_clause( $post_type, $max_entries ) { global $wpdb; $post_statuses = array_map( 'esc_sql', WPSEO_Sitemaps::get_post_statuses( $post_type ) ); $replacements = array_merge( [ 'ordering', 'post_modified_gmt', $wpdb->posts, 'type_status_date', 'post_status', ], $post_statuses, [ 'post_type', $post_type, 'post_modified_gmt', 'post_modified_gmt', 'ordering', $max_entries, ] ); //phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- We need to use a direct query here. //phpcs:disable WordPress.DB.DirectDatabaseQuery.NoCaching -- Reason: No relevant caches. return $wpdb->get_col( //phpcs:disable WordPress.DB.PreparedSQLPlaceholders -- %i placeholder is still not recognized. $wpdb->prepare( ' WITH %i AS (SELECT ROW_NUMBER() OVER (ORDER BY %i) AS n, post_modified_gmt FROM %i USE INDEX ( %i ) WHERE %i IN (' . implode( ', ', array_fill( 0, count( $post_statuses ), '%s' ) ) . ') AND %i = %s ORDER BY %i) SELECT %i FROM %i WHERE MOD(n, %d) = 0; ', $replacements ) ); } /** * Get all dates for a post type. * * @param string $post_type Post type to retrieve dates for. * @param int $max_entries Maximum number of entries to retrieve. * * @return array Array of dates. */ private function get_all_dates( $post_type, $max_entries ) { global $wpdb; $post_statuses = array_map( 'esc_sql', WPSEO_Sitemaps::get_post_statuses( $post_type ) ); $replacements = array_merge( [ 'post_modified_gmt', $wpdb->posts, 'type_status_date', 'post_status', ], $post_statuses, [ 'post_type', $post_type, $max_entries, 'post_modified_gmt', ] ); return $wpdb->get_col( //phpcs:disable WordPress.DB.PreparedSQLPlaceholders -- %i placeholder is still not recognized. $wpdb->prepare( ' SELECT %i FROM ( SELECT @rownum:=0 ) init JOIN %i USE INDEX( %i ) WHERE %i IN (' . implode( ', ', array_fill( 0, count( $post_statuses ), '%s' ) ) . ') AND %i = %s AND ( @rownum:=@rownum+1 ) %% %d = 0 ORDER BY %i ASC ', $replacements ) ); } } class-author-sitemap-provider.php000066600000013122151733213030013153 0ustar00handles_type( 'author' ) ) { return []; } // @todo Consider doing this less often / when necessary. R. $this->update_user_meta(); $has_exclude_filter = has_filter( 'wpseo_sitemap_exclude_author' ); $query_arguments = []; if ( ! $has_exclude_filter ) { // We only need full users if legacy filter(s) hooked to exclusion logic. R. $query_arguments['fields'] = 'ID'; } $users = $this->get_users( $query_arguments ); if ( $has_exclude_filter ) { $users = $this->exclude_users( $users ); $users = wp_list_pluck( $users, 'ID' ); } if ( empty( $users ) ) { return []; } $index = []; $user_pages = array_chunk( $users, $max_entries ); foreach ( $user_pages as $page_counter => $users_page ) { $current_page = ( $page_counter === 0 ) ? '' : ( $page_counter + 1 ); $user_id = array_shift( $users_page ); // Time descending, first user on page is most recently updated. $user = get_user_by( 'id', $user_id ); $index[] = [ 'loc' => WPSEO_Sitemaps_Router::get_base_url( 'author-sitemap' . $current_page . '.xml' ), 'lastmod' => ( $user->_yoast_wpseo_profile_updated ) ? YoastSEO()->helpers->date->format_timestamp( $user->_yoast_wpseo_profile_updated ) : null, ]; } return $index; } /** * Retrieve users, taking account of all necessary exclusions. * * @param array $arguments Arguments to add. * * @return array */ protected function get_users( $arguments = [] ) { global $wpdb; $defaults = [ 'capability' => [ 'edit_posts' ], 'meta_key' => '_yoast_wpseo_profile_updated', 'orderby' => 'meta_value_num', 'order' => 'DESC', 'meta_query' => [ 'relation' => 'AND', [ 'key' => $wpdb->get_blog_prefix() . 'user_level', 'value' => '0', 'compare' => '!=', ], [ 'relation' => 'OR', [ 'key' => 'wpseo_noindex_author', 'value' => 'on', 'compare' => '!=', ], [ 'key' => 'wpseo_noindex_author', 'compare' => 'NOT EXISTS', ], ], ], ]; if ( WPSEO_Options::get( 'noindex-author-noposts-wpseo', true ) ) { unset( $defaults['capability'] ); // Otherwise it cancels out next argument. $defaults['has_published_posts'] = YoastSEO()->helpers->author_archive->get_author_archive_post_types(); } return get_users( array_merge( $defaults, $arguments ) ); } /** * Get set of sitemap link data. * * @param string $type Sitemap type. * @param int $max_entries Entries per sitemap. * @param int $current_page Current page of the sitemap. * * @return array * * @throws OutOfBoundsException When an invalid page is requested. */ public function get_sitemap_links( $type, $max_entries, $current_page ) { $links = []; if ( ! $this->handles_type( 'author' ) ) { return $links; } $user_criteria = [ 'offset' => ( ( $current_page - 1 ) * $max_entries ), 'number' => $max_entries, ]; $users = $this->get_users( $user_criteria ); // Throw an exception when there are no users in the sitemap. if ( count( $users ) === 0 ) { throw new OutOfBoundsException( 'Invalid sitemap page requested' ); } $users = $this->exclude_users( $users ); if ( empty( $users ) ) { $users = []; } $time = time(); foreach ( $users as $user ) { $author_link = get_author_posts_url( $user->ID ); if ( empty( $author_link ) ) { continue; } $mod = $time; if ( isset( $user->_yoast_wpseo_profile_updated ) ) { $mod = $user->_yoast_wpseo_profile_updated; } $url = [ 'loc' => $author_link, 'mod' => date( DATE_W3C, $mod ), // Deprecated, kept for backwards data compat. R. 'chf' => 'daily', 'pri' => 1, ]; /** This filter is documented at inc/sitemaps/class-post-type-sitemap-provider.php */ $url = apply_filters( 'wpseo_sitemap_entry', $url, 'user', $user ); if ( ! empty( $url ) ) { $links[] = $url; } } return $links; } /** * Update any users that don't have last profile update timestamp. * * @return int Count of users updated. */ protected function update_user_meta() { $user_criteria = [ 'capability' => [ 'edit_posts' ], 'meta_query' => [ [ 'key' => '_yoast_wpseo_profile_updated', 'compare' => 'NOT EXISTS', ], ], ]; $users = get_users( $user_criteria ); $time = time(); foreach ( $users as $user ) { update_user_meta( $user->ID, '_yoast_wpseo_profile_updated', $time ); } return count( $users ); } /** * Wrap legacy filter to deduplicate calls. * * @param array $users Array of user objects to filter. * * @return array */ protected function exclude_users( $users ) { /** * Filter the authors, included in XML sitemap. * * @param array $users Array of user objects to filter. */ return apply_filters( 'wpseo_sitemap_exclude_author', $users ); } } class-sitemaps-cache.php000066600000020423151733213030011251 0ustar00is_enabled(); } /** * If cache is enabled. * * @since 3.2 * * @return bool */ public function is_enabled() { /** * Filter if XML sitemap transient cache is enabled. * * @param bool $unsigned Enable cache or not, defaults to true. */ return apply_filters( 'wpseo_enable_xml_sitemap_transient_caching', false ); } /** * Retrieve the sitemap page from cache. * * @since 3.2 * * @param string $type Sitemap type. * @param int $page Page number to retrieve. * * @return string|bool */ public function get_sitemap( $type, $page ) { $transient_key = WPSEO_Sitemaps_Cache_Validator::get_storage_key( $type, $page ); if ( $transient_key === false ) { return false; } return get_transient( $transient_key ); } /** * Get the sitemap that is cached. * * @param string $type Sitemap type. * @param int $page Page number to retrieve. * * @return WPSEO_Sitemap_Cache_Data|null Null on no cache found otherwise object containing sitemap and meta data. */ public function get_sitemap_data( $type, $page ) { $sitemap = $this->get_sitemap( $type, $page ); if ( empty( $sitemap ) ) { return null; } /* * Unserialize Cache Data object as is_serialized() doesn't recognize classes in C format. * This work-around should no longer be needed once the minimum PHP version has gone up to PHP 7.4, * as the `WPSEO_Sitemap_Cache_Data` class uses O format serialization in PHP 7.4 and higher. * * @link https://wiki.php.net/rfc/custom_object_serialization */ if ( is_string( $sitemap ) && strpos( $sitemap, 'C:24:"WPSEO_Sitemap_Cache_Data"' ) === 0 ) { // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.serialize_unserialize -- Can't be avoided due to how WP stores options. $sitemap = unserialize( $sitemap ); } // What we expect it to be if it is set. if ( $sitemap instanceof WPSEO_Sitemap_Cache_Data_Interface ) { return $sitemap; } return null; } /** * Store the sitemap page from cache. * * @since 3.2 * * @param string $type Sitemap type. * @param int $page Page number to store. * @param string $sitemap Sitemap body to store. * @param bool $usable Is this a valid sitemap or a cache of an invalid sitemap. * * @return bool */ public function store_sitemap( $type, $page, $sitemap, $usable = true ) { $transient_key = WPSEO_Sitemaps_Cache_Validator::get_storage_key( $type, $page ); if ( $transient_key === false ) { return false; } $status = ( $usable ) ? WPSEO_Sitemap_Cache_Data::OK : WPSEO_Sitemap_Cache_Data::ERROR; $sitemap_data = new WPSEO_Sitemap_Cache_Data(); $sitemap_data->set_sitemap( $sitemap ); $sitemap_data->set_status( $status ); return set_transient( $transient_key, $sitemap_data, DAY_IN_SECONDS ); } /** * Delete cache transients for index and specific type. * * Always deletes the main index sitemaps cache, as that's always invalidated by any other change. * * @since 1.5.4 * @since 3.2 Changed from function wpseo_invalidate_sitemap_cache() to method in this class. * * @param string $type Sitemap type to invalidate. * * @return void */ public static function invalidate( $type ) { self::clear( [ $type ] ); } /** * Helper to invalidate in hooks where type is passed as second argument. * * @since 3.2 * * @param int $unused Unused term ID value. * @param string $type Taxonomy to invalidate. * * @return void */ public static function invalidate_helper( $unused, $type ) { if ( WPSEO_Options::get( 'noindex-' . $type ) === false || WPSEO_Options::get( 'noindex-tax-' . $type ) === false ) { self::invalidate( $type ); } } /** * Invalidate sitemap cache for authors. * * @param int $user_id User ID. * * @return bool True if the sitemap was properly invalidated. False otherwise. */ public static function invalidate_author( $user_id ) { $user = get_user_by( 'id', $user_id ); if ( $user === false ) { return false; } if ( current_action() === 'user_register' ) { update_user_meta( $user_id, '_yoast_wpseo_profile_updated', time() ); } if ( empty( $user->roles ) || in_array( 'subscriber', $user->roles, true ) ) { return false; } self::invalidate( 'author' ); return true; } /** * Invalidate sitemap cache for the post type of a post. * * Don't invalidate for revisions. * * @since 1.5.4 * @since 3.2 Changed from function wpseo_invalidate_sitemap_cache_on_save_post() to method in this class. * * @param int $post_id Post ID to invalidate type for. * * @return void */ public static function invalidate_post( $post_id ) { if ( wp_is_post_revision( $post_id ) ) { return; } self::invalidate( get_post_type( $post_id ) ); } /** * Delete cache transients for given sitemaps types or all by default. * * @since 1.8.0 * @since 3.2 Moved from WPSEO_Utils to this class. * * @param array $types Set of sitemap types to delete cache transients for. * * @return void */ public static function clear( $types = [] ) { if ( ! self::$is_enabled ) { return; } // No types provided, clear all. if ( empty( $types ) ) { self::$clear_all = true; return; } // Always invalidate the index sitemap as well. if ( ! in_array( WPSEO_Sitemaps::SITEMAP_INDEX_TYPE, $types, true ) ) { array_unshift( $types, WPSEO_Sitemaps::SITEMAP_INDEX_TYPE ); } foreach ( $types as $type ) { if ( ! in_array( $type, self::$clear_types, true ) ) { self::$clear_types[] = $type; } } } /** * Invalidate storage for cache types queued to clear. * * @return void */ public static function clear_queued() { if ( self::$clear_all ) { WPSEO_Sitemaps_Cache_Validator::invalidate_storage(); self::$clear_all = false; self::$clear_types = []; return; } foreach ( self::$clear_types as $type ) { WPSEO_Sitemaps_Cache_Validator::invalidate_storage( $type ); } self::$clear_types = []; } /** * Adds a hook that when given option is updated, the cache is cleared. * * @since 3.2 * * @param string $option Option name. * @param string $type Sitemap type. * * @return void */ public static function register_clear_on_option_update( $option, $type = '' ) { self::$cache_clear[ $option ] = $type; } /** * Clears the transient cache when a given option is updated, if that option has been registered before. * * @since 3.2 * * @param string $option The option name that's being updated. * * @return void */ public static function clear_on_option_update( $option ) { if ( array_key_exists( $option, self::$cache_clear ) ) { if ( empty( self::$cache_clear[ $option ] ) ) { // Clear all caches. self::clear(); } else { // Clear specific provided type(s). $types = (array) self::$cache_clear[ $option ]; self::clear( $types ); } } } } interface-sitemap-cache-data.php000066600000002315151733213030012630 0ustar00