PageRenderTime 55ms CodeModel.GetById 28ms RepoModel.GetById 0ms app.codeStats 0ms

/src/Utility/Xml.php

https://github.com/ceeram/cakephp
PHP | 401 lines | 220 code | 24 blank | 157 comment | 55 complexity | be7d7a1d271b2e4e865fcf886df7f0ec MD5 | raw file
  1. <?php
  2. /**
  3. * CakePHP(tm) : Rapid Development Framework (http://cakephp.org)
  4. * Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
  5. *
  6. * Licensed under The MIT License
  7. * For full copyright and license information, please see the LICENSE.txt
  8. * Redistributions of files must retain the above copyright notice.
  9. *
  10. * @copyright Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
  11. * @link http://cakephp.org CakePHP(tm) Project
  12. * @since 0.10.3
  13. * @license http://www.opensource.org/licenses/mit-license.php MIT License
  14. */
  15. namespace Cake\Utility;
  16. use Cake\Utility\Exception\XmlException;
  17. use DOMDocument;
  18. /**
  19. * XML handling for CakePHP.
  20. *
  21. * The methods in these classes enable the datasources that use XML to work.
  22. *
  23. */
  24. class Xml
  25. {
  26. /**
  27. * Initialize SimpleXMLElement or DOMDocument from a given XML string, file path, URL or array.
  28. *
  29. * ### Usage:
  30. *
  31. * Building XML from a string:
  32. *
  33. * `$xml = Xml::build('<example>text</example>');`
  34. *
  35. * Building XML from string (output DOMDocument):
  36. *
  37. * `$xml = Xml::build('<example>text</example>', ['return' => 'domdocument']);`
  38. *
  39. * Building XML from a file path:
  40. *
  41. * `$xml = Xml::build('/path/to/an/xml/file.xml');`
  42. *
  43. * Building from a remote URL:
  44. *
  45. * `$xml = Xml::build('http://example.com/example.xml');`
  46. *
  47. * Building from an array:
  48. *
  49. * ```
  50. * $value = [
  51. * 'tags' => [
  52. * 'tag' => [
  53. * [
  54. * 'id' => '1',
  55. * 'name' => 'defect'
  56. * ],
  57. * [
  58. * 'id' => '2',
  59. * 'name' => 'enhancement'
  60. * ]
  61. * ]
  62. * ]
  63. * ];
  64. * $xml = Xml::build($value);
  65. * ```
  66. *
  67. * When building XML from an array ensure that there is only one top level element.
  68. *
  69. * ### Options
  70. *
  71. * - `return` Can be 'simplexml' to return object of SimpleXMLElement or 'domdocument' to return DOMDocument.
  72. * - `loadEntities` Defaults to false. Set to true to enable loading of `<!ENTITY` definitions. This
  73. * is disabled by default for security reasons.
  74. * - If using array as input, you can pass `options` from Xml::fromArray.
  75. *
  76. * @param string|array $input XML string, a path to a file, a URL or an array
  77. * @param string|array $options The options to use
  78. * @return \SimpleXMLElement|\DOMDocument SimpleXMLElement or DOMDocument
  79. * @throws \Cake\Utility\Exception\XmlException
  80. */
  81. public static function build($input, array $options = [])
  82. {
  83. $defaults = [
  84. 'return' => 'simplexml',
  85. 'loadEntities' => false,
  86. ];
  87. $options += $defaults;
  88. if (is_array($input) || is_object($input)) {
  89. return static::fromArray($input, $options);
  90. }
  91. if (strpos($input, '<') !== false) {
  92. return static::_loadXml($input, $options);
  93. }
  94. if (file_exists($input)) {
  95. return static::_loadXml(file_get_contents($input), $options);
  96. }
  97. if (!is_string($input)) {
  98. throw new XmlException('Invalid input.');
  99. }
  100. throw new XmlException('XML cannot be read.');
  101. }
  102. /**
  103. * Parse the input data and create either a SimpleXmlElement object or a DOMDocument.
  104. *
  105. * @param string $input The input to load.
  106. * @param array $options The options to use. See Xml::build()
  107. * @return \SimpleXmlElement|\DOMDocument
  108. * @throws \Cake\Utility\Exception\XmlException
  109. */
  110. protected static function _loadXml($input, $options)
  111. {
  112. $hasDisable = function_exists('libxml_disable_entity_loader');
  113. $internalErrors = libxml_use_internal_errors(true);
  114. if ($hasDisable && !$options['loadEntities']) {
  115. libxml_disable_entity_loader(true);
  116. }
  117. try {
  118. if ($options['return'] === 'simplexml' || $options['return'] === 'simplexmlelement') {
  119. $xml = new \SimpleXMLElement($input, LIBXML_NOCDATA);
  120. } else {
  121. $xml = new \DOMDocument();
  122. $xml->loadXML($input);
  123. }
  124. } catch (\Exception $e) {
  125. $xml = null;
  126. }
  127. if ($hasDisable && !$options['loadEntities']) {
  128. libxml_disable_entity_loader(false);
  129. }
  130. libxml_use_internal_errors($internalErrors);
  131. if ($xml === null) {
  132. throw new XmlException('Xml cannot be read.');
  133. }
  134. return $xml;
  135. }
  136. /**
  137. * Transform an array into a SimpleXMLElement
  138. *
  139. * ### Options
  140. *
  141. * - `format` If create childs ('tags') or attributes ('attribute').
  142. * - `pretty` Returns formatted Xml when set to `true`. Defaults to `false`
  143. * - `version` Version of XML document. Default is 1.0.
  144. * - `encoding` Encoding of XML document. If null remove from XML header. Default is the some of application.
  145. * - `return` If return object of SimpleXMLElement ('simplexml') or DOMDocument ('domdocument'). Default is SimpleXMLElement.
  146. *
  147. * Using the following data:
  148. *
  149. * ```
  150. * $value = [
  151. * 'root' => [
  152. * 'tag' => [
  153. * 'id' => 1,
  154. * 'value' => 'defect',
  155. * '@' => 'description'
  156. * ]
  157. * ]
  158. * ];
  159. * ```
  160. *
  161. * Calling `Xml::fromArray($value, 'tags');` Will generate:
  162. *
  163. * `<root><tag><id>1</id><value>defect</value>description</tag></root>`
  164. *
  165. * And calling `Xml::fromArray($value, 'attribute');` Will generate:
  166. *
  167. * `<root><tag id="1" value="defect">description</tag></root>`
  168. *
  169. * @param array|\Cake\Collection\Collection $input Array with data or a collection instance.
  170. * @param string|array $options The options to use or a string to use as format.
  171. * @return \SimpleXMLElement|\DOMDocument SimpleXMLElement or DOMDocument
  172. * @throws \Cake\Utility\Exception\XmlException
  173. */
  174. public static function fromArray($input, $options = [])
  175. {
  176. if (method_exists($input, 'toArray')) {
  177. $input = $input->toArray();
  178. }
  179. if (!is_array($input) || count($input) !== 1) {
  180. throw new XmlException('Invalid input.');
  181. }
  182. $key = key($input);
  183. if (is_int($key)) {
  184. throw new XmlException('The key of input must be alphanumeric');
  185. }
  186. if (!is_array($options)) {
  187. $options = ['format' => (string)$options];
  188. }
  189. $defaults = [
  190. 'format' => 'tags',
  191. 'version' => '1.0',
  192. 'encoding' => mb_internal_encoding(),
  193. 'return' => 'simplexml',
  194. 'pretty' => false
  195. ];
  196. $options += $defaults;
  197. $dom = new DOMDocument($options['version'], $options['encoding']);
  198. if ($options['pretty']) {
  199. $dom->formatOutput = true;
  200. }
  201. self::_fromArray($dom, $dom, $input, $options['format']);
  202. $options['return'] = strtolower($options['return']);
  203. if ($options['return'] === 'simplexml' || $options['return'] === 'simplexmlelement') {
  204. return new \SimpleXMLElement($dom->saveXML());
  205. }
  206. return $dom;
  207. }
  208. /**
  209. * Recursive method to create childs from array
  210. *
  211. * @param \DOMDocument $dom Handler to DOMDocument
  212. * @param \DOMElement $node Handler to DOMElement (child)
  213. * @param array &$data Array of data to append to the $node.
  214. * @param string $format Either 'attribute' or 'tags'. This determines where nested keys go.
  215. * @return void
  216. * @throws \Cake\Utility\Exception\XmlException
  217. */
  218. protected static function _fromArray($dom, $node, &$data, $format)
  219. {
  220. if (empty($data) || !is_array($data)) {
  221. return;
  222. }
  223. foreach ($data as $key => $value) {
  224. if (is_string($key)) {
  225. if (method_exists($value, 'toArray')) {
  226. $value = $value->toArray();
  227. }
  228. if (!is_array($value)) {
  229. if (is_bool($value)) {
  230. $value = (int)$value;
  231. } elseif ($value === null) {
  232. $value = '';
  233. }
  234. $isNamespace = strpos($key, 'xmlns:');
  235. if ($isNamespace !== false) {
  236. $node->setAttributeNS('http://www.w3.org/2000/xmlns/', $key, $value);
  237. continue;
  238. }
  239. if ($key[0] !== '@' && $format === 'tags') {
  240. $child = null;
  241. if (!is_numeric($value)) {
  242. // Escape special characters
  243. // http://www.w3.org/TR/REC-xml/#syntax
  244. // https://bugs.php.net/bug.php?id=36795
  245. $child = $dom->createElement($key, '');
  246. $child->appendChild(new \DOMText($value));
  247. } else {
  248. $child = $dom->createElement($key, $value);
  249. }
  250. $node->appendChild($child);
  251. } else {
  252. if ($key[0] === '@') {
  253. $key = substr($key, 1);
  254. }
  255. $attribute = $dom->createAttribute($key);
  256. $attribute->appendChild($dom->createTextNode($value));
  257. $node->appendChild($attribute);
  258. }
  259. } else {
  260. if ($key[0] === '@') {
  261. throw new XmlException('Invalid array');
  262. }
  263. if (is_numeric(implode('', array_keys($value)))) {
  264. // List
  265. foreach ($value as $item) {
  266. $itemData = compact('dom', 'node', 'key', 'format');
  267. $itemData['value'] = $item;
  268. static::_createChild($itemData);
  269. }
  270. } else {
  271. // Struct
  272. static::_createChild(compact('dom', 'node', 'key', 'value', 'format'));
  273. }
  274. }
  275. } else {
  276. throw new XmlException('Invalid array');
  277. }
  278. }
  279. }
  280. /**
  281. * Helper to _fromArray(). It will create childs of arrays
  282. *
  283. * @param array $data Array with informations to create childs
  284. * @return void
  285. */
  286. protected static function _createChild($data)
  287. {
  288. extract($data);
  289. $childNS = $childValue = null;
  290. if (method_exists($value, 'toArray')) {
  291. $value = $value->toArray();
  292. }
  293. if (is_array($value)) {
  294. if (isset($value['@'])) {
  295. $childValue = (string)$value['@'];
  296. unset($value['@']);
  297. }
  298. if (isset($value['xmlns:'])) {
  299. $childNS = $value['xmlns:'];
  300. unset($value['xmlns:']);
  301. }
  302. } elseif (!empty($value) || $value === 0) {
  303. $childValue = (string)$value;
  304. }
  305. $child = $dom->createElement($key);
  306. if ($childValue !== null) {
  307. $child->appendChild($dom->createTextNode($childValue));
  308. }
  309. if ($childNS) {
  310. $child->setAttribute('xmlns', $childNS);
  311. }
  312. static::_fromArray($dom, $child, $value, $format);
  313. $node->appendChild($child);
  314. }
  315. /**
  316. * Returns this XML structure as an array.
  317. *
  318. * @param \SimpleXMLElement|\DOMDocument|\DOMNode $obj SimpleXMLElement, DOMDocument or DOMNode instance
  319. * @return array Array representation of the XML structure.
  320. * @throws \Cake\Utility\Exception\XmlException
  321. */
  322. public static function toArray($obj)
  323. {
  324. if ($obj instanceof \DOMNode) {
  325. $obj = simplexml_import_dom($obj);
  326. }
  327. if (!($obj instanceof \SimpleXMLElement)) {
  328. throw new XmlException('The input is not instance of SimpleXMLElement, DOMDocument or DOMNode.');
  329. }
  330. $result = [];
  331. $namespaces = array_merge(['' => ''], $obj->getNamespaces(true));
  332. static::_toArray($obj, $result, '', array_keys($namespaces));
  333. return $result;
  334. }
  335. /**
  336. * Recursive method to toArray
  337. *
  338. * @param \SimpleXMLElement $xml SimpleXMLElement object
  339. * @param array &$parentData Parent array with data
  340. * @param string $ns Namespace of current child
  341. * @param array $namespaces List of namespaces in XML
  342. * @return void
  343. */
  344. protected static function _toArray($xml, &$parentData, $ns, $namespaces)
  345. {
  346. $data = [];
  347. foreach ($namespaces as $namespace) {
  348. foreach ($xml->attributes($namespace, true) as $key => $value) {
  349. if (!empty($namespace)) {
  350. $key = $namespace . ':' . $key;
  351. }
  352. $data['@' . $key] = (string)$value;
  353. }
  354. foreach ($xml->children($namespace, true) as $child) {
  355. static::_toArray($child, $data, $namespace, $namespaces);
  356. }
  357. }
  358. $asString = trim((string)$xml);
  359. if (empty($data)) {
  360. $data = $asString;
  361. } elseif (strlen($asString) > 0) {
  362. $data['@'] = $asString;
  363. }
  364. if (!empty($ns)) {
  365. $ns .= ':';
  366. }
  367. $name = $ns . $xml->getName();
  368. if (isset($parentData[$name])) {
  369. if (!is_array($parentData[$name]) || !isset($parentData[$name][0])) {
  370. $parentData[$name] = [$parentData[$name]];
  371. }
  372. $parentData[$name][] = $data;
  373. } else {
  374. $parentData[$name] = $data;
  375. }
  376. }
  377. }