PluginProbe
Jetpack – WP Security, Backup, Speed, & Growth / 16.3-a.1
Jetpack – WP Security, Backup, Speed, & Growth v16.3-a.1
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 13.7.2 13.8.3 13.9.2 14.0.1 14.1.1 All 503 releases
jetpack / modules / sitemaps / sitemap-buffer-xmlwriter.php

sitemap-buffer-xmlwriter.php in Jetpack – WP Security, Backup, Speed, & Growth 16.3-a.1, at modules/sitemaps/sitemap-buffer-xmlwriter.php

379 lines 9.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
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 }
379