Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
83.64% covered (warning)
83.64%
46 / 55
66.67% covered (warning)
66.67%
6 / 9
CRAP
0.00% covered (danger)
0.00%
0 / 1
Jetpack_Sitemap_Buffer
86.79% covered (warning)
86.79%
46 / 53
66.67% covered (warning)
66.67%
6 / 9
22.02
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
1
 get_root_element
n/a
0 / 0
n/a
0 / 0
0
 append
81.82% covered (warning)
81.82%
9 / 11
0.00% covered (danger)
0.00%
0 / 1
5.15
 contents
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 get_document
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 is_full
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 is_empty
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 view_time
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 last_modified
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 array_to_xml_string
82.61% covered (warning)
82.61%
19 / 23
0.00% covered (danger)
0.00%
0 / 1
7.26
1<?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName
2/**
3 * Sitemaps (per the protocol) are essentially lists of XML fragments;
4 * lists which are subject to size constraints. The Jetpack_Sitemap_Buffer
5 * class abstracts the details of constructing these lists while
6 * maintaining the constraints.
7 *
8 * @since 4.8.0
9 * @package automattic/jetpack
10 */
11
12if ( ! defined( 'ABSPATH' ) ) {
13    exit( 0 );
14}
15
16/**
17 * A buffer for constructing sitemap xml files.
18 *
19 * Models a list of strings such that
20 *
21 * 1. the list must have a bounded number of entries,
22 * 2. the concatenation of the strings must have bounded
23 *      length (including some header and footer strings), and
24 * 3. each item has a timestamp, and we need to keep track
25 *      of the most recent timestamp of the items in the list.
26 *
27 * @since 4.8.0
28 */
29abstract class Jetpack_Sitemap_Buffer {
30
31    /**
32     * Largest number of items the buffer can hold.
33     *
34     * @access protected
35     * @since 4.8.0
36     * @var int $item_capacity The item capacity.
37     */
38    protected $item_capacity;
39
40    /**
41     * Largest number of bytes the buffer can hold.
42     *
43     * @access protected
44     * @since 4.8.0
45     * @var int $byte_capacity The byte capacity.
46     */
47    protected $byte_capacity;
48
49    /**
50     * Flag which detects when the buffer is full.
51     *
52     * @access protected
53     * @since 4.8.0
54     * @var bool $is_full_flag The flag value. This flag is set to false on construction and only flipped to true if we've tried to add something and failed.
55     */
56    protected $is_full_flag;
57
58    /**
59     * Flag which detects when the buffer is empty.
60     *
61     * @access protected
62     * @since 4.8.0
63     * @var bool $is_empty_flag The flag value. This flag is set to true on construction and only flipped to false if we've tried to add something and succeeded.
64     */
65    protected $is_empty_flag;
66
67    /**
68     * The most recent timestamp seen by the buffer.
69     *
70     * @access protected
71     * @since 4.8.0
72     * @var string $timestamp Must be in 'YYYY-MM-DD hh:mm:ss' format.
73     */
74    protected $timestamp;
75
76    /**
77     * The DOM document object that is currently being used to construct the XML doc.
78     *
79     * @access protected
80     * @since 5.3.0
81     * @var DOMDocument $doc
82     */
83    protected $doc = null;
84
85    /**
86     * The root DOM element object that holds everything inside. Do not use directly, call
87     * the get_root_element getter method instead.
88     *
89     * @access protected
90     * @since 5.3.0
91     * @var DOMElement $doc
92     */
93    protected $root = null;
94
95    /**
96     * Helper class to construct sitemap paths.
97     *
98     * @since 5.3.0
99     * @protected
100     * @var Jetpack_Sitemap_Finder
101     */
102    protected $finder;
103
104    /**
105     * Construct a new Jetpack_Sitemap_Buffer.
106     *
107     * @since 4.8.0
108     *
109     * @param int    $item_limit The maximum size of the buffer in items.
110     * @param int    $byte_limit The maximum size of the buffer in bytes.
111     * @param string $time The initial datetime of the buffer. Must be in 'YYYY-MM-DD hh:mm:ss' format.
112     */
113    public function __construct( $item_limit, $byte_limit, $time ) {
114        $this->is_full_flag = false;
115        $this->timestamp    = $time;
116
117        $this->finder                  = new Jetpack_Sitemap_Finder();
118        $this->doc                     = new DOMDocument( '1.0', 'UTF-8' );
119        $this->doc->formatOutput       = true;
120        $this->doc->preserveWhiteSpace = false;
121
122        $this->item_capacity = max( 1, (int) $item_limit );
123        $this->byte_capacity = max( 1, (int) $byte_limit ) - strlen( $this->doc->saveXML() );
124    }
125
126    /**
127     * Returns a DOM element that contains all sitemap elements.
128     *
129     * @access protected
130     * @since 5.3.0
131     * @return DOMElement $root
132     */
133    abstract protected function get_root_element();
134
135    /**
136     * Append an item to the buffer, if there is room for it,
137     * and set is_empty_flag to false. If there is no room,
138     * we set is_full_flag to true. If $item is null,
139     * don't do anything and report success.
140     *
141     * @since 5.3.0
142     *
143     * @param array $array The item to be added.
144     *
145     * @return bool True if the append succeeded, False if not.
146     */
147    public function append( $array ) {
148        if ( $array === null ) {
149            return true;
150        }
151
152        if ( $this->is_full_flag ) {
153            return false;
154        }
155
156        if ( 0 >= $this->item_capacity || 0 >= $this->byte_capacity ) {
157            $this->is_full_flag = true;
158            return false;
159        } else {
160            $this->item_capacity -= 1;
161            $added_element        = $this->array_to_xml_string( $array, $this->get_root_element(), $this->doc );
162
163            $this->byte_capacity -= strlen( $this->doc->saveXML( $added_element ) );
164
165            return true;
166        }
167    }
168
169    /**
170     * Retrieve the contents of the buffer.
171     *
172     * @since 4.8.0
173     *
174     * @return string The contents of the buffer (with the footer included).
175     */
176    public function contents() {
177        if ( $this->is_empty() ) {
178            // The sitemap should have at least the root element added to the DOM.
179            $this->get_root_element();
180        }
181        return $this->doc->saveXML();
182    }
183
184    /**
185     * Retrieve the document object.
186     *
187     * @since 5.3.0
188     * @return DOMDocument $doc
189     */
190    public function get_document() {
191        return $this->doc;
192    }
193
194    /**
195     * Detect whether the buffer is full.
196     *
197     * @since 4.8.0
198     *
199     * @return bool True if the buffer is full, false otherwise.
200     */
201    public function is_full() {
202        return $this->is_full_flag;
203    }
204
205    /**
206     * Detect whether the buffer is empty.
207     *
208     * @since 4.8.0
209     *
210     * @return bool True if the buffer is empty, false otherwise.
211     */
212    public function is_empty() {
213        return (
214            ! isset( $this->root )
215            || ! $this->root->hasChildNodes()
216        );
217    }
218
219    /**
220     * Update the timestamp of the buffer.
221     *
222     * @since 4.8.0
223     *
224     * @param string $new_time A datetime string in 'YYYY-MM-DD hh:mm:ss' format.
225     */
226    public function view_time( $new_time ) {
227        $this->timestamp = max( $this->timestamp, $new_time );
228    }
229
230    /**
231     * Retrieve the timestamp of the buffer.
232     *
233     * @since 4.8.0
234     *
235     * @return string A datetime string in 'YYYY-MM-DD hh:mm:ss' format.
236     */
237    public function last_modified() {
238        return $this->timestamp;
239    }
240
241    /**
242     * Render an associative array as an XML string. This is needed because
243     * SimpleXMLElement only handles valid XML, but we sometimes want to
244     * pass around (possibly invalid) fragments. Note that 'null' values make
245     * a tag self-closing; this is only sometimes correct (depending on the
246     * version of HTML/XML); see the list of 'void tags'.
247     *
248     * Example:
249     *
250     * array(
251     *   'html' => array(                    |<html xmlns="foo">
252     *     'head' => array(                  |  <head>
253     *       'title' => 'Woo!',              |    <title>Woo!</title>
254     *     ),                                |  </head>
255     *     'body' => array(             ==>  |  <body>
256     *       'h2' => 'Some thing',           |    <h2>Some thing</h2>
257     *       'p'  => 'it's all up ons',      |    <p>it's all up ons</p>
258     *       'br' => null,                   |    <br />
259     *     ),                                |  </body>
260     *   ),                                  |</html>
261     * )
262     *
263     * @access protected
264     * @since 3.9.0
265     * @since 4.8.0 Rename, add $depth parameter, and change return type.
266     * @since 5.3.0 Refactor, remove $depth parameter, add $parent and $root, make access protected.
267     *
268     * @param array       $array A recursive associative array of tag/child relationships.
269     * @param DOMElement  $parent (optional) an element to which new children should be added.
270     * @param DOMDocument $root (optional) the parent document.
271     *
272     * @return string|DOMDocument The rendered XML string or an object if root element is specified.
273     */
274    protected function array_to_xml_string( $array, $parent = null, $root = null ) {
275        $element       = null;
276        $return_string = false;
277
278        if ( null === $parent ) {
279            $return_string = true;
280            $root          = new DOMDocument();
281            $parent        = $root;
282        }
283
284        if ( is_array( $array ) ) {
285
286            foreach ( $array as $key => $value ) {
287                $element = $root->createElement( $key );
288                $parent->appendChild( $element );
289
290                if ( is_array( $value ) ) {
291                    foreach ( $value as $child_key => $child_value ) {
292                        $child = $root->createElement( $child_key );
293                        $element->appendChild( $child );
294                        $child->appendChild( self::array_to_xml_string( $child_value, $child, $root ) );
295                    }
296                } else {
297                    $element->appendChild(
298                        $root->createTextNode( $value )
299                    );
300                }
301            }
302        } else {
303            $element = $root->createTextNode( $array );
304            $parent->appendChild( $element );
305        }
306
307        if ( $return_string ) {
308            return $root->saveHTML();
309        } else {
310            return $element;
311        }
312    }
313}