PluginProbe
Jetpack – WP Security, Backup, Speed, & Growth / 16.3
Jetpack – WP Security, Backup, Speed, & Growth v16.3
16.3 16.3-beta 16.3-a.5 16.3-a.7 16.3-a.3 16.3-a.1 16.2 16.2-beta 12.0.3 12.1.3 12.2.3 12.3.2 12.4.2 12.5.2 12.6.4 12.7.3 12.8.3 12.9.5 13.0.2 13.1.5 13.2.4 13.3.3 13.4.5 13.5.2 13.6.2 All 508 releases
← All changes | modules/sitemaps/sitemap-buffer-xmlwriter.php +378 -0 16.2-beta → 16.3 View file →
@@ -1,0 +1,378 @@
1 +<?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName
2 +/**
3 + * XMLWriter implementation of the sitemap buffer.
4 + *
5 + * @since 14.6
6 + * @package automattic/jetpack
7 + */
8 +
9 +if ( ! defined( 'ABSPATH' ) ) {
10 + exit( 0 );
11 +}
12 +
13 +/**
14 + * A buffer for constructing sitemap xml files using XMLWriter.
15 + *
16 + * @since 14.6
17 + */
18 +abstract class Jetpack_Sitemap_Buffer_XMLWriter {
19 +
20 + /**
21 + * Largest number of items the buffer can hold.
22 + *
23 + * @access protected
24 + * @since 14.6
25 + * @var int $item_capacity The item capacity.
26 + */
27 + protected $item_capacity;
28 +
29 + /**
30 + * Largest number of bytes the buffer can hold.
31 + *
32 + * @access protected
33 + * @since 14.6
34 + * @var int $byte_capacity The byte capacity.
35 + */
36 + protected $byte_capacity;
37 +
38 + /**
39 + * Flag which detects when the buffer is full.
40 + *
41 + * @access protected
42 + * @since 14.6
43 + * @var bool $is_full_flag The flag value.
44 + */
45 + protected $is_full_flag;
46 +
47 + /**
48 + * Flag which detects when the buffer is empty.
49 + * Set true on construction and flipped to false only after a successful append.
50 + *
51 + * @since 15.0
52 + * @var bool
53 + */
54 + protected $is_empty_flag = true;
55 +
56 + /**
57 + * The most recent timestamp seen by the buffer.
58 + *
59 + * @access protected
60 + * @since 14.6
61 + * @var string $timestamp Must be in 'YYYY-MM-DD hh:mm:ss' format.
62 + */
63 + protected $timestamp;
64 +
65 + /**
66 + * The XMLWriter instance used to construct the XML.
67 + *
68 + * @access protected
69 + * @since 14.6
70 + * @var XMLWriter $writer
71 + */
72 + protected $writer;
73 +
74 + /**
75 + * Helper class to construct sitemap paths.
76 + *
77 + * @since 14.6
78 + * @protected
79 + * @var Jetpack_Sitemap_Finder
80 + */
81 + protected $finder;
82 +
83 + /**
84 + * The XML content chunks collected from XMLWriter.
85 + *
86 + * Collect chunks and join once at the end to reduce string reallocations
87 + * and improve performance on large sitemaps.
88 + *
89 + * @access protected
90 + * @since 15.0
91 + * @var array $chunks
92 + */
93 + protected $chunks = array();
94 +
95 + /**
96 + * Tracks whether the root element has been started.
97 + *
98 + * @since 15.0
99 + * @var bool
100 + */
101 + protected $root_started = false;
102 +
103 + /**
104 + * Mirror DOMDocument built on-demand for jetpack_print_sitemap compatibility.
105 + *
106 + * @since 15.0
107 + * @var DOMDocument|null
108 + */
109 + protected $dom_document = null;
110 +
111 + /**
112 + * Tracks whether XMLWriter document has been finalized (closed and flushed).
113 + *
114 + * @since 15.0
115 + * @var bool
116 + */
117 + protected $is_finalized = false;
118 +
119 + /**
120 + * Construct a new Jetpack_Sitemap_Buffer_XMLWriter.
121 + *
122 + * @since 14.6
123 + *
124 + * @param int $item_limit The maximum size of the buffer in items.
125 + * @param int $byte_limit The maximum size of the buffer in bytes.
126 + * @param string $time The initial datetime of the buffer. Must be in 'YYYY-MM-DD hh:mm:ss' format.
127 + */
128 + public function __construct( $item_limit, $byte_limit, $time ) {
129 + $this->is_full_flag = false;
130 + $this->is_empty_flag = true;
131 + $this->timestamp = $time;
132 + $this->finder = new Jetpack_Sitemap_Finder();
133 +
134 + $this->writer = new XMLWriter();
135 + $this->writer->openMemory();
136 + $this->writer->setIndent( true );
137 + $this->writer->startDocument( '1.0', 'UTF-8' );
138 +
139 + $this->item_capacity = max( 1, (int) $item_limit );
140 + $this->byte_capacity = max( 1, (int) $byte_limit );
141 +
142 + // Capture and account the XML declaration bytes to mirror DOM behavior.
143 + $declaration = $this->writer->outputMemory( true );
144 + $this->chunks[] = $declaration;
145 + $this->byte_capacity -= strlen( $declaration );
146 +
147 + // Allow subclasses to write comments and processing instructions only.
148 + $this->initialize_buffer();
149 +
150 + // Capture pre-root bytes (comments/PI). Do not subtract from capacity.
151 + $pre_root_output = $this->writer->outputMemory( true );
152 + $this->chunks[] = $pre_root_output;
153 + }
154 +
155 + /**
156 + * Initialize the buffer with any required headers or setup.
157 + * This should be implemented by child classes.
158 + *
159 + * @access protected
160 + * @since 14.6
161 + */
162 + abstract protected function initialize_buffer();
163 +
164 + /**
165 + * Start the root element (e.g., urlset or sitemapindex) and write its attributes.
166 + * Implemented by subclasses.
167 + *
168 + * @since 15.0
169 + * @access protected
170 + * @return void
171 + */
172 + abstract protected function start_root();
173 +
174 + /**
175 + * Ensure the root element has been started and account its bytes once.
176 + *
177 + * @since 15.0
178 + * @access protected
179 + * @return void
180 + */
181 + protected function ensure_root_started() {
182 + if ( $this->root_started ) {
183 + return;
184 + }
185 + $this->start_root();
186 + $root_chunk = $this->writer->outputMemory( true );
187 + $this->chunks[] = $root_chunk;
188 + $this->byte_capacity -= strlen( $root_chunk );
189 + $this->root_started = true;
190 + }
191 +
192 + /**
193 + * Finalize writer output once by closing the root and document and flushing.
194 + *
195 + * @since 15.0
196 + * @access protected
197 + * @return void
198 + */
199 + protected function finalize_writer_output() {
200 + if ( $this->is_finalized ) {
201 + return;
202 + }
203 + $this->ensure_root_started();
204 + $this->writer->endElement(); // End root element (urlset/sitemapindex)
205 + $this->writer->endDocument();
206 + $final_content = $this->writer->outputMemory( true );
207 + $this->chunks[] = $final_content;
208 + $this->is_finalized = true;
209 + }
210 +
211 + /**
212 + * Append an item to the buffer.
213 + *
214 + * @since 14.6
215 + *
216 + * @param array $array The item to be added.
217 + * @return bool True if the append succeeded, False if not.
218 + */
219 + public function append( $array ) {
220 + if ( $array === null ) {
221 + return true;
222 + }
223 +
224 + if ( $this->is_full_flag ) {
225 + return false;
226 + }
227 +
228 + if ( 0 >= $this->item_capacity || 0 >= $this->byte_capacity ) {
229 + $this->is_full_flag = true;
230 + return false;
231 + }
232 +
233 + // Ensure root is started on first append and account its bytes.
234 + $this->ensure_root_started();
235 +
236 + // Attempt to render the item. Subclasses may decide to skip writing
237 + // if the input structure is invalid for that sitemap type.
238 + $this->append_item( $array );
239 +
240 + // Capture only the bytes produced by this item.
241 + $new_content = $this->writer->outputMemory( true );
242 +
243 + // If nothing was written, treat as a no-op: keep the buffer "empty"
244 + // and do not consume item/byte capacities.
245 + if ( '' === $new_content ) {
246 + return true;
247 + }
248 +
249 + // Persist newly written bytes and update capacities.
250 + $this->chunks[] = $new_content;
251 + $this->item_capacity -= 1;
252 + $this->byte_capacity -= strlen( $new_content );
253 + $this->is_empty_flag = false;
254 +
255 + // Check both capacity limits.
256 + if ( 0 >= $this->item_capacity || $this->byte_capacity <= 0 ) {
257 + $this->is_full_flag = true;
258 + }
259 +
260 + return true;
261 + }
262 +
263 + /**
264 + * Append a specific item to the buffer.
265 + * This should be implemented by child classes.
266 + *
267 + * @access protected
268 + * @since 14.6
269 + * @param array $array The item to be added.
270 + */
271 + abstract protected function append_item( $array );
272 +
273 + /**
274 + * Recursively writes XML elements from an associative array.
275 + *
276 + * This method iterates through an array and writes XML elements using the XMLWriter instance.
277 + * If a value in the array is itself an array, it calls itself recursively.
278 + *
279 + * @access protected
280 + * @since 15.0
281 + *
282 + * @param array $data The array to convert to XML.
283 + */
284 + protected function array_to_xml( $data ) {
285 + foreach ( (array) $data as $tag => $value ) {
286 + if ( is_array( $value ) ) {
287 + $this->writer->startElement( $tag );
288 + $this->array_to_xml( $value );
289 + $this->writer->endElement();
290 + } else {
291 + // Write raw text; XMLWriter will escape XML-reserved chars, matching DOMDocument behavior.
292 + $this->writer->writeElement( $tag, (string) $value );
293 + }
294 + }
295 + }
296 +
297 + /**
298 + * Retrieve the contents of the buffer.
299 + *
300 + * @since 14.6
301 + * @return string The contents of the buffer.
302 + */
303 + public function contents() {
304 + $this->finalize_writer_output();
305 + if ( $this->dom_document instanceof DOMDocument ) {
306 + return $this->dom_document->saveXML();
307 + }
308 + if ( empty( $this->chunks ) ) {
309 + // If buffer is empty, return a minimal valid XML structure
310 + return "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<urlset xmlns=\"http://www.sitemaps.org/schemas/sitemap/0.9\"></urlset>";
311 + }
312 + return implode( '', $this->chunks );
313 + }
314 +
315 + /**
316 + * Detect whether the buffer is full.
317 + *
318 + * @since 14.6
319 + * @return bool True if the buffer is full, false otherwise.
320 + */
321 + public function is_full() {
322 + return $this->is_full_flag;
323 + }
324 +
325 + /**
326 + * Detect whether the buffer is empty.
327 + *
328 + * @since 14.6
329 + * @return bool True if the buffer is empty, false otherwise.
330 + */
331 + public function is_empty() {
332 + return $this->is_empty_flag;
333 + }
334 +
335 + /**
336 + * Update the timestamp of the buffer.
337 + *
338 + * @since 14.6
339 + * @param string $new_time A datetime string in 'YYYY-MM-DD hh:mm:ss' format.
340 + */
341 + public function view_time( $new_time ) {
342 + $this->timestamp = max( $this->timestamp, $new_time );
343 + }
344 +
345 + /**
346 + * Retrieve the timestamp of the buffer.
347 + *
348 + * @since 14.6
349 + * @return string A datetime string in 'YYYY-MM-DD hh:mm:ss' format.
350 + */
351 + public function last_modified() {
352 + return $this->timestamp;
353 + }
354 +
355 + /**
356 + * Compatibility method for the old DOMDocument implementation.
357 + * This is only here to satisfy the jetpack_print_sitemap filter.
358 + *
359 + * @since 14.6
360 + * @return DOMDocument DOM representation of the current sitemap contents.
361 + */
362 + public function get_document() {
363 + if ( $this->dom_document instanceof DOMDocument ) {
364 + return $this->dom_document;
365 + }
366 +
367 + $this->finalize_writer_output();
368 +
369 + $dom = new DOMDocument( '1.0', 'UTF-8' );
370 + $dom->formatOutput = true; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
371 + $dom->preserveWhiteSpace = false; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
372 + // Load current XML content into DOM for compatibility with filters.
373 + @$dom->loadXML( implode( '', $this->chunks ) ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged -- Avoid fatal on unexpected content
374 +
375 + $this->dom_document = $dom;
376 + return $this->dom_document;
377 + }
378 +}