| @@ -60,9 +60,9 @@ | ||
| 60 | 60 | |
| 61 | 61 | /** |
| 62 | 62 | * Builds the sitemap index. |
| 63 | 63 | * |
| 64 | - * @param array $links Set of sitemaps index links. | |
| 64 | + * @param array<string> $links Set of sitemaps index links. | |
| 65 | 65 | * |
| 66 | 66 | * @return string |
| 67 | 67 | */ |
| 68 | 68 | public function get_index( $links ) { |
| @@ -86,11 +86,11 @@ | ||
| 86 | 86 | |
| 87 | 87 | /** |
| 88 | 88 | * Builds the sitemap. |
| 89 | 89 | * |
| 90 | - * @param array $links Set of sitemap links. | |
| 91 | - * @param string $type Sitemap type. | |
| 92 | - * @param int $current_page Current sitemap page number. | |
| 90 | + * @param array<string> $links Set of sitemap links. | |
| 91 | + * @param string $type Sitemap type. | |
| 92 | + * @param int $current_page Current sitemap page number. | |
| 93 | 93 | * |
| 94 | 94 | * @return string |
| 95 | 95 | */ |
| 96 | 96 | public function get_sitemap( $links, $type, $current_page ) { |
| @@ -95,16 +95,23 @@ | ||
| 95 | 95 | */ |
| 96 | 96 | public function get_sitemap( $links, $type, $current_page ) { |
| 97 | 97 | |
| 98 | 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"; | |
| 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 | 102 | |
| 103 | 103 | /** |
| 104 | + * Filters the `urlset` for all sitemaps. | |
| 105 | + * | |
| 106 | + * @param string $urlset The output for the sitemap's `urlset`. | |
| 107 | + */ | |
| 108 | + $urlset = apply_filters( 'wpseo_sitemap_urlset', $urlset ); | |
| 109 | + | |
| 110 | + /** | |
| 104 | 111 | * Filters the `urlset` for a sitemap by type. |
| 105 | 112 | * |
| 106 | - * @api string $urlset The output for the sitemap's `urlset`. | |
| 113 | + * @param string $urlset The output for the sitemap's `urlset`. | |
| 107 | 114 | */ |
| 108 | 115 | $xml = apply_filters( "wpseo_sitemap_{$type}_urlset", $urlset ); |
| 109 | 116 | |
| 110 | 117 | foreach ( $links as $url ) { |
| @@ -129,9 +136,9 @@ | ||
| 129 | 136 | |
| 130 | 137 | /** |
| 131 | 138 | * Produce final XML output with debug information. |
| 132 | 139 | * |
| 133 | - * @param string $sitemap Sitemap XML. | |
| 140 | + * @param string $sitemap Sitemap XML. | |
| 134 | 141 | * |
| 135 | 142 | * @return string |
| 136 | 143 | */ |
| 137 | 144 | public function get_output( $sitemap ) { |
| @@ -165,8 +172,10 @@ | ||
| 165 | 172 | /** |
| 166 | 173 | * Set a custom stylesheet for this sitemap. Set to empty to just remove the default stylesheet. |
| 167 | 174 | * |
| 168 | 175 | * @param string $stylesheet Full XML-stylesheet declaration. |
| 176 | + * | |
| 177 | + * @return void | |
| 169 | 178 | */ |
| 170 | 179 | public function set_stylesheet( $stylesheet ) { |
| 171 | 180 | $this->stylesheet = $stylesheet; |
| 172 | 181 | } |
| @@ -173,9 +182,9 @@ | ||
| 173 | 182 | |
| 174 | 183 | /** |
| 175 | 184 | * Build the `<sitemap>` tag for a given URL. |
| 176 | 185 | * |
| 177 | - * @param array $url Array of parts that make up this entry. | |
| 186 | + * @param array<string> $url Array of parts that make up this entry. | |
| 178 | 187 | * |
| 179 | 188 | * @return string |
| 180 | 189 | */ |
| 181 | 190 | protected function sitemap_index_url( $url ) { |
| @@ -200,9 +209,9 @@ | ||
| 200 | 209 | * Build the `<url>` tag for a given URL. |
| 201 | 210 | * |
| 202 | 211 | * Public access for backwards compatibility reasons. |
| 203 | 212 | * |
| 204 | - * @param array $url Array of parts that make up this entry. | |
| 213 | + * @param array<string> $url Array of parts that make up this entry. | |
| 205 | 214 | * |
| 206 | 215 | * @return string |
| 207 | 216 | */ |
| 208 | 217 | public function sitemap_url( $url ) { |
| @@ -208,18 +217,15 @@ | ||
| 208 | 217 | public function sitemap_url( $url ) { |
| 209 | 218 | |
| 210 | 219 | $date = null; |
| 211 | 220 | |
| 212 | - | |
| 213 | 221 | if ( ! empty( $url['mod'] ) ) { |
| 214 | 222 | // Create a DateTime object date in the correct timezone. |
| 215 | 223 | $date = YoastSEO()->helpers->date->format( $url['mod'] ); |
| 216 | 224 | } |
| 217 | 225 | |
| 218 | - $url['loc'] = htmlspecialchars( $url['loc'], ENT_COMPAT, $this->output_charset, false ); | |
| 219 | - | |
| 220 | 226 | $output = "\t<url>\n"; |
| 221 | - $output .= "\t\t<loc>" . $this->encode_url_rfc3986( $url['loc'] ) . "</loc>\n"; | |
| 227 | + $output .= "\t\t<loc>" . $this->encode_and_escape( $url['loc'] ) . "</loc>\n"; | |
| 222 | 228 | $output .= empty( $date ) ? '' : "\t\t<lastmod>" . htmlspecialchars( $date, ENT_COMPAT, $this->output_charset, false ) . "</lastmod>\n"; |
| 223 | 229 | |
| 224 | 230 | if ( empty( $url['images'] ) ) { |
| 225 | 231 | $url['images'] = []; |
| @@ -231,37 +237,12 @@ | ||
| 231 | 237 | continue; |
| 232 | 238 | } |
| 233 | 239 | |
| 234 | 240 | $output .= "\t\t<image:image>\n"; |
| 235 | - $output .= "\t\t\t<image:loc>" . esc_html( $this->encode_url_rfc3986( $img['src'] ) ) . "</image:loc>\n"; | |
| 236 | - | |
| 237 | - if ( ! empty( $img['title'] ) ) { | |
| 238 | - | |
| 239 | - $title = $img['title']; | |
| 240 | - | |
| 241 | - if ( $this->needs_conversion ) { | |
| 242 | - $title = mb_convert_encoding( $title, $this->output_charset, $this->charset ); | |
| 243 | - } | |
| 244 | - | |
| 245 | - $title = _wp_specialchars( html_entity_decode( $title, ENT_QUOTES, $this->output_charset ) ); | |
| 246 | - $output .= "\t\t\t<image:title><![CDATA[{$title}]]></image:title>\n"; | |
| 247 | - } | |
| 248 | - | |
| 249 | - if ( ! empty( $img['alt'] ) ) { | |
| 250 | - | |
| 251 | - $alt = $img['alt']; | |
| 252 | - | |
| 253 | - if ( $this->needs_conversion ) { | |
| 254 | - $alt = mb_convert_encoding( $alt, $this->output_charset, $this->charset ); | |
| 255 | - } | |
| 256 | - | |
| 257 | - $alt = _wp_specialchars( html_entity_decode( $alt, ENT_QUOTES, $this->output_charset ) ); | |
| 258 | - $output .= "\t\t\t<image:caption><![CDATA[{$alt}]]></image:caption>\n"; | |
| 259 | - } | |
| 260 | - | |
| 241 | + $output .= "\t\t\t<image:loc>" . $this->encode_and_escape( $img['src'] ) . "</image:loc>\n"; | |
| 261 | 242 | $output .= "\t\t</image:image>\n"; |
| 262 | 243 | } |
| 263 | - unset( $img, $title, $alt ); | |
| 244 | + unset( $img ); | |
| 264 | 245 | |
| 265 | 246 | $output .= "\t</url>\n"; |
| 266 | 247 | |
| 267 | 248 | /** |
| @@ -266,16 +247,48 @@ | ||
| 266 | 247 | |
| 267 | 248 | /** |
| 268 | 249 | * Filters the output for the sitemap URL tag. |
| 269 | 250 | * |
| 270 | - * @api string $output The output for the sitemap url tag. | |
| 271 | - * | |
| 272 | - * @param array $url The sitemap URL array on which the output is based. | |
| 251 | + * @param string $output The output for the sitemap url tag. | |
| 252 | + * @param array $url The sitemap URL array on which the output is based. | |
| 273 | 253 | */ |
| 274 | 254 | return apply_filters( 'wpseo_sitemap_url', $output, $url ); |
| 275 | 255 | } |
| 276 | 256 | |
| 277 | 257 | /** |
| 258 | + * Ensure the URL is encoded per RFC3986 and correctly escaped for use in an XML sitemap. | |
| 259 | + * | |
| 260 | + * This method works around a two quirks in esc_url(): | |
| 261 | + * 1. `esc_url()` leaves schema-relative URLs alone, while according to the sitemap specs, | |
| 262 | + * the URL must always begin with a protocol. | |
| 263 | + * 2. `esc_url()` escapes ampersands as `&` instead of the more common `&`. | |
| 264 | + * According to the specs, `&` should be used, and even though this shouldn't | |
| 265 | + * really make a difference in practice, to quote Jono: "I'd be nervous about & | |
| 266 | + * given how many weird and wonderful things eat sitemaps", so better safe than sorry. | |
| 267 | + * | |
| 268 | + * @link https://www.sitemaps.org/protocol.html#xmlTagDefinitions | |
| 269 | + * @link https://www.sitemaps.org/protocol.html#escaping | |
| 270 | + * @link https://developer.wordpress.org/reference/functions/esc_url/ | |
| 271 | + * | |
| 272 | + * @param string $url URL to encode and escape. | |
| 273 | + * | |
| 274 | + * @return string | |
| 275 | + */ | |
| 276 | + protected function encode_and_escape( $url ) { | |
| 277 | + $url = $this->encode_url_rfc3986( $url ); | |
| 278 | + $url = esc_url( $url ); | |
| 279 | + $url = str_replace( '&', '&', $url ); | |
| 280 | + $url = str_replace( ''', ''', $url ); | |
| 281 | + | |
| 282 | + if ( strpos( $url, '//' ) === 0 ) { | |
| 283 | + // Schema-relative URL for which esc_url() does not add a scheme. | |
| 284 | + $url = 'http:' . $url; | |
| 285 | + } | |
| 286 | + | |
| 287 | + return $url; | |
| 288 | + } | |
| 289 | + | |
| 290 | + /** | |
| 278 | 291 | * Apply some best effort conversion to comply with RFC3986. |
| 279 | 292 | * |
| 280 | 293 | * @param string $url URL to encode. |
| 281 | 294 | * |
| @@ -325,9 +338,9 @@ | ||
| 325 | 338 | * @return string The XSL URL that needs to be used. |
| 326 | 339 | */ |
| 327 | 340 | protected function get_xsl_url() { |
| 328 | 341 | if ( home_url() !== site_url() ) { |
| 329 | - return home_url( 'main-sitemap.xsl' ); | |
| 342 | + return apply_filters( 'wpseo_sitemap_public_url', home_url( 'main-sitemap.xsl' ) ); | |
| 330 | 343 | } |
| 331 | 344 | |
| 332 | 345 | /* |
| 333 | 346 | * Fallback to circumvent a cross-domain security problem when the XLS file is |