sitemap-buffer.php 8.4 KB

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