1<?php
2/**
3 * Sitemaps: WP_Sitemaps_Stylesheet class
4 *
5 * This class provides the XSL stylesheets to style all sitemaps.
6 *
7 * @package WordPress
8 * @subpackage Sitemaps
9 * @since 5.5.0
10 */
11
12/**
13 * Stylesheet provider class.
14 *
15 * @since 5.5.0
16 */
17#[AllowDynamicProperties]
18class WP_Sitemaps_Stylesheet {
19 /**
20 * Renders the XSL stylesheet depending on whether it's the sitemap index or not.
21 *
22 * @param string $type Stylesheet type. Either 'sitemap' or 'index'.
23 */
24 public function render_stylesheet( $type ) {
25 header( 'Content-Type: application/xml; charset=UTF-8' );
26
27 if ( 'sitemap' === $type ) {
28 // All content is escaped below.
29 echo $this->get_sitemap_stylesheet();
30 }
31
32 if ( 'index' === $type ) {
33 // All content is escaped below.
34 echo $this->get_sitemap_index_stylesheet();
35 }
36
37 exit;
38 }
39
40 /**
41 * Returns the escaped XSL for all sitemaps, except index.
42 *
43 * @since 5.5.0
44 */
45 public function get_sitemap_stylesheet() {
46 $css = $this->get_stylesheet_css();
47 $title = esc_xml( __( 'XML Sitemap' ) );
48 $description = esc_xml( __( 'This XML Sitemap is generated by WordPress to make your content more visible for search engines.' ) );
49 $learn_more = sprintf(
50 '<a href="%s">%s</a>',
51 esc_url( __( 'https://www.sitemaps.org/' ) ),
52 esc_xml( __( 'Learn more about XML sitemaps.' ) )
53 );
54
55 $text = sprintf(
56 /* translators: %s: Number of URLs. */
57 esc_xml( __( 'Number of URLs in this XML Sitemap: %s.' ) ),
58 '<xsl:value-of select="count( sitemap:urlset/sitemap:url )" />'
59 );
60
61 $lang = get_language_attributes( 'html' );
62 $url = esc_xml( __( 'URL' ) );
63 $lastmod = esc_xml( __( 'Last Modified' ) );
64 $changefreq = esc_xml( __( 'Change Frequency' ) );
65 $priority = esc_xml( __( 'Priority' ) );
66
67 $xsl_content = <<<XSL
68<?xml version="1.0" encoding="UTF-8"?>
69<xsl:stylesheet
70 version="1.0"
71 xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
72 xmlns:sitemap="http://www.sitemaps.org/schemas/sitemap/0.9"
73 exclude-result-prefixes="sitemap"
74 >
75
76 <xsl:output method="html" encoding="UTF-8" indent="yes" />
77
78 <!--
79 Set variables for whether lastmod, changefreq or priority occur for any url in the sitemap.
80 We do this up front because it can be expensive in a large sitemap.
81 -->
82 <xsl:variable name="has-lastmod" select="count( /sitemap:urlset/sitemap:url/sitemap:lastmod )" />
83 <xsl:variable name="has-changefreq" select="count( /sitemap:urlset/sitemap:url/sitemap:changefreq )" />
84 <xsl:variable name="has-priority" select="count( /sitemap:urlset/sitemap:url/sitemap:priority )" />
85
86 <xsl:template match="/">
87 <html {$lang}>
88 <head>
89 <title>{$title}</title>
90 <style>
91 {$css}
92 </style>
93 </head>
94 <body>
95 <div id="sitemap">
96 <div id="sitemap__header">
97 <h1>{$title}</h1>
98 <p>{$description}</p>
99 <p>{$learn_more}</p>
100 </div>
101 <div id="sitemap__content">
102 <p class="text">{$text}</p>
103 <table id="sitemap__table">
104 <thead>
105 <tr>
106 <th class="loc">{$url}</th>
107 <xsl:if test="\$has-lastmod">
108 <th class="lastmod">{$lastmod}</th>
109 </xsl:if>
110 <xsl:if test="\$has-changefreq">
111 <th class="changefreq">{$changefreq}</th>
112 </xsl:if>
113 <xsl:if test="\$has-priority">
114 <th class="priority">{$priority}</th>
115 </xsl:if>
116 </tr>
117 </thead>
118 <tbody>
119 <xsl:for-each select="sitemap:urlset/sitemap:url">
120 <tr>
121 <td class="loc"><a href="{sitemap:loc}"><xsl:value-of select="sitemap:loc" /></a></td>
122 <xsl:if test="\$has-lastmod">
123 <td class="lastmod"><xsl:value-of select="sitemap:lastmod" /></td>
124 </xsl:if>
125 <xsl:if test="\$has-changefreq">
126 <td class="changefreq"><xsl:value-of select="sitemap:changefreq" /></td>
127 </xsl:if>
128 <xsl:if test="\$has-priority">
129 <td class="priority"><xsl:value-of select="sitemap:priority" /></td>
130 </xsl:if>
131 </tr>
132 </xsl:for-each>
133 </tbody>
134 </table>
135 </div>
136 </div>
137 </body>
138 </html>
139 </xsl:template>
140</xsl:stylesheet>
141
142XSL;
143
144 /**
145 * Filters the content of the sitemap stylesheet.
146 *
147 * @since 5.5.0
148 *
149 * @param string $xsl_content Full content for the XML stylesheet.
150 */
151 return apply_filters( 'wp_sitemaps_stylesheet_content', $xsl_content );
152 }
153
154 /**
155 * Returns the escaped XSL for the index sitemaps.
156 *
157 * @since 5.5.0
158 */
159 public function get_sitemap_index_stylesheet() {
160 $css = $this->get_stylesheet_css();
161 $title = esc_xml( __( 'XML Sitemap' ) );
162 $description = esc_xml( __( 'This XML Sitemap is generated by WordPress to make your content more visible for search engines.' ) );
163 $learn_more = sprintf(
164 '<a href="%s">%s</a>',
165 esc_url( __( 'https://www.sitemaps.org/' ) ),
166 esc_xml( __( 'Learn more about XML sitemaps.' ) )
167 );
168
169 $text = sprintf(
170 /* translators: %s: Number of URLs. */
171 esc_xml( __( 'Number of URLs in this XML Sitemap: %s.' ) ),
172 '<xsl:value-of select="count( sitemap:sitemapindex/sitemap:sitemap )" />'
173 );
174
175 $lang = get_language_attributes( 'html' );
176 $url = esc_xml( __( 'URL' ) );
177 $lastmod = esc_xml( __( 'Last Modified' ) );
178
179 $xsl_content = <<<XSL
180<?xml version="1.0" encoding="UTF-8"?>
181<xsl:stylesheet
182 version="1.0"
183 xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
184 xmlns:sitemap="http://www.sitemaps.org/schemas/sitemap/0.9"
185 exclude-result-prefixes="sitemap"
186 >
187
188 <xsl:output method="html" encoding="UTF-8" indent="yes" />
189
190 <!--
191 Set variables for whether lastmod occurs for any sitemap in the index.
192 We do this up front because it can be expensive in a large sitemap.
193 -->
194 <xsl:variable name="has-lastmod" select="count( /sitemap:sitemapindex/sitemap:sitemap/sitemap:lastmod )" />
195
196 <xsl:template match="/">
197 <html {$lang}>
198 <head>
199 <title>{$title}</title>
200 <style>
201 {$css}
202 </style>
203 </head>
204 <body>
205 <div id="sitemap">
206 <div id="sitemap__header">
207 <h1>{$title}</h1>
208 <p>{$description}</p>
209 <p>{$learn_more}</p>
210 </div>
211 <div id="sitemap__content">
212 <p class="text">{$text}</p>
213 <table id="sitemap__table">
214 <thead>
215 <tr>
216 <th class="loc">{$url}</th>
217 <xsl:if test="\$has-lastmod">
218 <th class="lastmod">{$lastmod}</th>
219 </xsl:if>
220 </tr>
221 </thead>
222 <tbody>
223 <xsl:for-each select="sitemap:sitemapindex/sitemap:sitemap">
224 <tr>
225 <td class="loc"><a href="{sitemap:loc}"><xsl:value-of select="sitemap:loc" /></a></td>
226 <xsl:if test="\$has-lastmod">
227 <td class="lastmod"><xsl:value-of select="sitemap:lastmod" /></td>
228 </xsl:if>
229 </tr>
230 </xsl:for-each>
231 </tbody>
232 </table>
233 </div>
234 </div>
235 </body>
236 </html>
237 </xsl:template>
238</xsl:stylesheet>
239
240XSL;
241
242 /**
243 * Filters the content of the sitemap index stylesheet.
244 *
245 * @since 5.5.0
246 *
247 * @param string $xsl_content Full content for the XML stylesheet.
248 */
249 return apply_filters( 'wp_sitemaps_stylesheet_index_content', $xsl_content );
250 }
251
252 /**
253 * Gets the CSS to be included in sitemap XSL stylesheets.
254 *
255 * @since 5.5.0
256 *
257 * @return string The CSS.
258 */
259 public function get_stylesheet_css() {
260 $text_align = is_rtl() ? 'right' : 'left';
261
262 $css = <<<EOF
263
264 body {
265 font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif;
266 color: #444;
267 }
268
269 #sitemap {
270 max-width: 980px;
271 margin: 0 auto;
272 }
273
274 #sitemap__table {
275 width: 100%;
276 border: solid 1px #ccc;
277 border-collapse: collapse;
278 }
279
280 #sitemap__table tr td.loc {
281 /*
282 * URLs should always be LTR.
283 * See https://core.trac.wordpress.org/ticket/16834
284 * and https://core.trac.wordpress.org/ticket/49949
285 */
286 direction: ltr;
287 }
288
289 #sitemap__table tr th {
290 text-align: {$text_align};
291 }
292
293 #sitemap__table tr td,
294 #sitemap__table tr th {
295 padding: 10px;
296 }
297
298 #sitemap__table tr:nth-child(odd) td {
299 background-color: #eee;
300 }
301
302 a:hover {
303 text-decoration: none;
304 }
305
306EOF;
307
308 /**
309 * Filters the CSS only for the sitemap stylesheet.
310 *
311 * @since 5.5.0
312 *
313 * @param string $css CSS to be applied to default XSL file.
314 */
315 return apply_filters( 'wp_sitemaps_stylesheet_css', $css );
316 }
317}
318