PageRenderTime 45ms CodeModel.GetById 18ms RepoModel.GetById 1ms app.codeStats 0ms

/lib/Cake/Utility/Xml.php

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