PluginProbe
Yoast SEO – Advanced SEO with real-time guidance and built-in AI / 18.5
Yoast SEO – Advanced SEO with real-time guidance and built-in AI v18.5
28.5 28.4 28.3 28.2 28.1 28.0 27.9 27.8 27.7 27.6 27.5 trunk 18.0 18.1 18.2 18.3 18.4 18.4.1 18.5 18.5.1 18.6 18.7 18.8 18.9 19.0 All 129 releases
wordpress-seo / inc / sitemaps / class-sitemaps-renderer.php

class-sitemaps-renderer.php in Yoast SEO – Advanced SEO with real-time guidance and built-in AI 18.5, at inc/sitemaps/class-sitemaps-renderer.php

372 lines 9.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * WPSEO plugin file.
4 *
5 * @package WPSEO\XML_Sitemaps
6 */
7
8 /**
9 * Renders XML output for sitemaps.
10 */
11 class WPSEO_Sitemaps_Renderer {
12
13 /**
14 * XSL stylesheet for styling a sitemap for web browsers.
15 *
16 * @var string
17 */
18 protected $stylesheet = '';
19
20 /**
21 * Holds the get_bloginfo( 'charset' ) value to reuse for performance.
22 *
23 * @var string
24 */
25 protected $charset = 'UTF-8';
26
27 /**
28 * Holds charset of output, might be converted.
29 *
30 * @var string
31 */
32 protected $output_charset = 'UTF-8';
33
34 /**
35 * If data encoding needs to be converted for output.
36 *
37 * @var bool
38 */
39 protected $needs_conversion = false;
40
41 /**
42 * Set up object properties.
43 */
44 public function __construct() {
45 $stylesheet_url = preg_replace( '/(^http[s]?:)/', '', $this->get_xsl_url() );
46 $this->stylesheet = '<?xml-stylesheet type="text/xsl" href="' . esc_url( $stylesheet_url ) . '"?>';
47 $this->charset = get_bloginfo( 'charset' );
48 $this->output_charset = $this->charset;
49
50 if (
51 $this->charset !== 'UTF-8'
52 && function_exists( 'mb_list_encodings' )
53 && in_array( $this->charset, mb_list_encodings(), true )
54 ) {
55 $this->output_charset = 'UTF-8';
56 }
57
58 $this->needs_conversion = $this->output_charset !== $this->charset;
59 }
60
61 /**
62 * Builds the sitemap index.
63 *
64 * @param array $links Set of sitemaps index links.
65 *
66 * @return string
67 */
68 public function get_index( $links ) {
69
70 $xml = '<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">' . "\n";
71
72 foreach ( $links as $link ) {
73 $xml .= $this->sitemap_index_url( $link );
74 }
75
76 /**
77 * Filter to append sitemaps to the index.
78 *
79 * @param string $index String to append to sitemaps index, defaults to empty.
80 */
81 $xml .= apply_filters( 'wpseo_sitemap_index', '' );
82 $xml .= '</sitemapindex>';
83
84 return $xml;
85 }
86
87 /**
88 * Builds the sitemap.
89 *
90 * @param array $links Set of sitemap links.
91 * @param string $type Sitemap type.
92 * @param int $current_page Current sitemap page number.
93 *
94 * @return string
95 */
96 public function get_sitemap( $links, $type, $current_page ) {
97
98 $urlset = '<urlset xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:image="http://www.google.com/schemas/sitemap-image/1.1" '
99 . 'xsi:schemaLocation="http://www.sitemaps.org/schemas/sitemap/0.9 http://www.sitemaps.org/schemas/sitemap/0.9/sitemap.xsd '
100 . 'http://www.google.com/schemas/sitemap-image/1.1 http://www.google.com/schemas/sitemap-image/1.1/sitemap-image.xsd" '
101 . 'xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">' . "\n";
102
103 /**
104 * Filters the `urlset` for a sitemap by type.
105 *
106 * @api string $urlset The output for the sitemap's `urlset`.
107 */
108 $xml = apply_filters( "wpseo_sitemap_{$type}_urlset", $urlset );
109
110 foreach ( $links as $url ) {
111 $xml .= $this->sitemap_url( $url );
112 }
113
114 /**
115 * Filter to add extra URLs to the XML sitemap by type.
116 *
117 * Only runs for the first page, not on all.
118 *
119 * @param string $content String content to add, defaults to empty.
120 */
121 if ( $current_page === 1 ) {
122 $xml .= apply_filters( "wpseo_sitemap_{$type}_content", '' );
123 }
124
125 $xml .= '</urlset>';
126
127 return $xml;
128 }
129
130 /**
131 * Produce final XML output with debug information.
132 *
133 * @param string $sitemap Sitemap XML.
134 *
135 * @return string
136 */
137 public function get_output( $sitemap ) {
138
139 $output = '<?xml version="1.0" encoding="' . esc_attr( $this->output_charset ) . '"?>';
140
141 if ( $this->stylesheet ) {
142 /**
143 * Filter the stylesheet URL for the XML sitemap.
144 *
145 * @param string $stylesheet Stylesheet URL.
146 */
147 $output .= apply_filters( 'wpseo_stylesheet_url', $this->stylesheet ) . "\n";
148 }
149
150 $output .= $sitemap;
151 $output .= "\n<!-- XML Sitemap generated by Yoast SEO -->";
152
153 return $output;
154 }
155
156 /**
157 * Get charset for the output.
158 *
159 * @return string
160 */
161 public function get_output_charset() {
162 return $this->output_charset;
163 }
164
165 /**
166 * Set a custom stylesheet for this sitemap. Set to empty to just remove the default stylesheet.
167 *
168 * @param string $stylesheet Full XML-stylesheet declaration.
169 */
170 public function set_stylesheet( $stylesheet ) {
171 $this->stylesheet = $stylesheet;
172 }
173
174 /**
175 * Build the `<sitemap>` tag for a given URL.
176 *
177 * @param array $url Array of parts that make up this entry.
178 *
179 * @return string
180 */
181 protected function sitemap_index_url( $url ) {
182
183 $date = null;
184
185 if ( ! empty( $url['lastmod'] ) ) {
186 $date = YoastSEO()->helpers->date->format( $url['lastmod'] );
187 }
188
189 $url['loc'] = htmlspecialchars( $url['loc'], ENT_COMPAT, $this->output_charset, false );
190
191 $output = "\t<sitemap>\n";
192 $output .= "\t\t<loc>" . $url['loc'] . "</loc>\n";
193 $output .= empty( $date ) ? '' : "\t\t<lastmod>" . htmlspecialchars( $date, ENT_COMPAT, $this->output_charset, false ) . "</lastmod>\n";
194 $output .= "\t</sitemap>\n";
195
196 return $output;
197 }
198
199 /**
200 * Build the `<url>` tag for a given URL.
201 *
202 * Public access for backwards compatibility reasons.
203 *
204 * @param array $url Array of parts that make up this entry.
205 *
206 * @return string
207 */
208 public function sitemap_url( $url ) {
209
210 $date = null;
211
212 if ( ! empty( $url['mod'] ) ) {
213 // Create a DateTime object date in the correct timezone.
214 $date = YoastSEO()->helpers->date->format( $url['mod'] );
215 }
216
217 $output = "\t<url>\n";
218 $output .= "\t\t<loc>" . $this->encode_and_escape( $url['loc'] ) . "</loc>\n";
219 $output .= empty( $date ) ? '' : "\t\t<lastmod>" . htmlspecialchars( $date, ENT_COMPAT, $this->output_charset, false ) . "</lastmod>\n";
220
221 if ( empty( $url['images'] ) ) {
222 $url['images'] = [];
223 }
224
225 foreach ( $url['images'] as $img ) {
226
227 if ( empty( $img['src'] ) ) {
228 continue;
229 }
230
231 $output .= "\t\t<image:image>\n";
232 $output .= "\t\t\t<image:loc>" . $this->encode_and_escape( $img['src'] ) . "</image:loc>\n";
233
234 if ( ! empty( $img['title'] ) ) {
235
236 $title = $img['title'];
237
238 if ( $this->needs_conversion ) {
239 $title = mb_convert_encoding( $title, $this->output_charset, $this->charset );
240 }
241
242 $title = _wp_specialchars( html_entity_decode( $title, ENT_QUOTES, $this->output_charset ) );
243 $output .= "\t\t\t<image:title><![CDATA[{$title}]]></image:title>\n";
244 }
245
246 if ( ! empty( $img['alt'] ) ) {
247
248 $alt = $img['alt'];
249
250 if ( $this->needs_conversion ) {
251 $alt = mb_convert_encoding( $alt, $this->output_charset, $this->charset );
252 }
253
254 $alt = _wp_specialchars( html_entity_decode( $alt, ENT_QUOTES, $this->output_charset ) );
255 $output .= "\t\t\t<image:caption><![CDATA[{$alt}]]></image:caption>\n";
256 }
257
258 $output .= "\t\t</image:image>\n";
259 }
260 unset( $img, $title, $alt );
261
262 $output .= "\t</url>\n";
263
264 /**
265 * Filters the output for the sitemap URL tag.
266 *
267 * @api string $output The output for the sitemap url tag.
268 *
269 * @param array $url The sitemap URL array on which the output is based.
270 */
271 return apply_filters( 'wpseo_sitemap_url', $output, $url );
272 }
273
274 /**
275 * Ensure the URL is encoded per RFC3986 and correctly escaped for use in an XML sitemap.
276 *
277 * This method works around a two quirks in esc_url():
278 * 1. `esc_url()` leaves schema-relative URLs alone, while according to the sitemap specs,
279 * the URL must always begin with a protocol.
280 * 2. `esc_url()` escapes ampersands as `&#038;` instead of the more common `&amp;`.
281 * According to the specs, `&amp;` should be used, and even though this shouldn't
282 * really make a difference in practice, to quote Jono: "I'd be nervous about &#038;
283 * given how many weird and wonderful things eat sitemaps", so better safe than sorry.
284 *
285 * @link https://www.sitemaps.org/protocol.html#xmlTagDefinitions
286 * @link https://www.sitemaps.org/protocol.html#escaping
287 * @link https://developer.wordpress.org/reference/functions/esc_url/
288 *
289 * @param string $url URL to encode and escape.
290 *
291 * @return string
292 */
293 protected function encode_and_escape( $url ) {
294 $url = $this->encode_url_rfc3986( $url );
295 $url = esc_url( $url );
296 $url = str_replace( '&#038;', '&amp;', $url );
297
298 if ( strpos( $url, '//' ) === 0 ) {
299 // Schema-relative URL for which esc_url() does not add a scheme.
300 $url = 'http:' . $url;
301 }
302
303 return $url;
304 }
305
306 /**
307 * Apply some best effort conversion to comply with RFC3986.
308 *
309 * @param string $url URL to encode.
310 *
311 * @return string
312 */
313 protected function encode_url_rfc3986( $url ) {
314
315 if ( filter_var( $url, FILTER_VALIDATE_URL ) ) {
316 return $url;
317 }
318
319 $path = wp_parse_url( $url, PHP_URL_PATH );
320
321 if ( ! empty( $path ) && $path !== '/' ) {
322 $encoded_path = explode( '/', $path );
323
324 // First decode the path, to prevent double encoding.
325 $encoded_path = array_map( 'rawurldecode', $encoded_path );
326
327 $encoded_path = array_map( 'rawurlencode', $encoded_path );
328 $encoded_path = implode( '/', $encoded_path );
329
330 $url = str_replace( $path, $encoded_path, $url );
331 }
332
333 $query = wp_parse_url( $url, PHP_URL_QUERY );
334
335 if ( ! empty( $query ) ) {
336
337 parse_str( $query, $parsed_query );
338
339 $parsed_query = http_build_query( $parsed_query, '', '&amp;', PHP_QUERY_RFC3986 );
340
341 $url = str_replace( $query, $parsed_query, $url );
342 }
343
344 return $url;
345 }
346
347 /**
348 * Retrieves the XSL URL that should be used in the current environment
349 *
350 * When home_url and site_url are not the same, the home_url should be used.
351 * This is because the XSL needs to be served from the same domain, protocol and port
352 * as the XML file that is loading it.
353 *
354 * @return string The XSL URL that needs to be used.
355 */
356 protected function get_xsl_url() {
357 if ( home_url() !== site_url() ) {
358 return home_url( 'main-sitemap.xsl' );
359 }
360
361 /*
362 * Fallback to circumvent a cross-domain security problem when the XLS file is
363 * loaded from a different (sub)domain.
364 */
365 if ( strpos( plugins_url(), home_url() ) !== 0 ) {
366 return home_url( 'main-sitemap.xsl' );
367 }
368
369 return plugin_dir_url( WPSEO_FILE ) . 'css/main-sitemap.xsl';
370 }
371 }
372