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