Xml.php 11 KB

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