PageRenderTime 36ms CodeModel.GetById 13ms RepoModel.GetById 0ms app.codeStats 0ms

/src/Utility/Xml.php

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