Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
| Total | |
90.91% |
70 / 77 |
|
72.73% |
8 / 11 |
CRAP | |
0.00% |
0 / 1 |
| Jetpack_Sitemap_Buffer_XMLWriter | |
93.33% |
70 / 75 |
|
72.73% |
8 / 11 |
25.19 | |
0.00% |
0 / 1 |
| __construct | |
100.00% |
16 / 16 |
|
100.00% |
1 / 1 |
1 | |||
| initialize_buffer | n/a |
0 / 0 |
n/a |
0 / 0 |
0 | |||||
| start_root | n/a |
0 / 0 |
n/a |
0 / 0 |
0 | |||||
| ensure_root_started | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
2 | |||
| finalize_writer_output | |
100.00% |
8 / 8 |
|
100.00% |
1 / 1 |
2 | |||
| append | |
84.21% |
16 / 19 |
|
0.00% |
0 / 1 |
8.25 | |||
| append_item | n/a |
0 / 0 |
n/a |
0 / 0 |
0 | |||||
| array_to_xml | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
3 | |||
| contents | |
83.33% |
5 / 6 |
|
0.00% |
0 / 1 |
3.04 | |||
| is_full | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| is_empty | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| view_time | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| last_modified | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| get_document | |
88.89% |
8 / 9 |
|
0.00% |
0 / 1 |
2.01 | |||
| 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 | } |