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: /**
24: * XML handling for Cake.
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: * - If using array as input, you can pass `options` from Xml::fromArray.
81: *
82: * @param string|array $input XML string, a path to a file, an URL or an array
83: * @param array $options The options to use
84: * @return SimpleXMLElement|DOMDocument SimpleXMLElement or DOMDocument
85: * @throws XmlException
86: */
87: public static function build($input, $options = array()) {
88: if (!is_array($options)) {
89: $options = array('return' => (string)$options);
90: }
91: $defaults = array(
92: 'return' => 'simplexml',
93: 'loadEntities' => false,
94: );
95: $options = array_merge($defaults, $options);
96:
97: if (is_array($input) || is_object($input)) {
98: return self::fromArray((array)$input, $options);
99: } elseif (strpos($input, '<') !== false) {
100: return self::_loadXml($input, $options);
101: } elseif (file_exists($input)) {
102: return self::_loadXml(file_get_contents($input), $options);
103: } elseif (strpos($input, 'http://') === 0 || strpos($input, 'https://') === 0) {
104: $socket = new HttpSocket(array('request' => array('redirect' => 10)));
105: $response = $socket->get($input);
106: if (!$response->isOk()) {
107: throw new XmlException(__d('cake_dev', 'XML cannot be read.'));
108: }
109: return self::_loadXml($response->body, $options);
110: } elseif (!is_string($input)) {
111: throw new XmlException(__d('cake_dev', 'Invalid input.'));
112: }
113: throw new XmlException(__d('cake_dev', 'XML cannot be read.'));
114: }
115:
116: /**
117: * Parse the input data and create either a SimpleXmlElement object or a DOMDocument.
118: *
119: * @param string $input The input to load.
120: * @param array $options The options to use. See Xml::build()
121: * @return SimpleXmlElement|DOMDocument.
122: */
123: protected static function _loadXml($input, $options) {
124: $hasDisable = function_exists('libxml_disable_entity_loader');
125: $internalErrors = libxml_use_internal_errors(true);
126: if ($hasDisable && !$options['loadEntities']) {
127: libxml_disable_entity_loader(true);
128: }
129: if ($options['return'] === 'simplexml' || $options['return'] === 'simplexmlelement') {
130: $xml = new SimpleXMLElement($input, LIBXML_NOCDATA);
131: } else {
132: $xml = new DOMDocument();
133: $xml->loadXML($input);
134: }
135: if ($hasDisable && !$options['loadEntities']) {
136: libxml_disable_entity_loader(false);
137: }
138: libxml_use_internal_errors($internalErrors);
139: return $xml;
140: }
141:
142: /**
143: * Transform an array into a SimpleXMLElement
144: *
145: * ### Options
146: *
147: * - `format` If create childs ('tags') or attributes ('attribute').
148: * - `version` Version of XML document. Default is 1.0.
149: * - `encoding` Encoding of XML document. If null remove from XML header. Default is the some of application.
150: * - `return` If return object of SimpleXMLElement ('simplexml') or DOMDocument ('domdocument'). Default is SimpleXMLElement.
151: *
152: * Using the following data:
153: *
154: * {{{
155: * $value = array(
156: * 'root' => array(
157: * 'tag' => array(
158: * 'id' => 1,
159: * 'value' => 'defect',
160: * '@' => 'description'
161: * )
162: * )
163: * );
164: * }}}
165: *
166: * Calling `Xml::fromArray($value, 'tags');` Will generate:
167: *
168: * `<root><tag><id>1</id><value>defect</value>description</tag></root>`
169: *
170: * And calling `Xml::fromArray($value, 'attribute');` Will generate:
171: *
172: * `<root><tag id="1" value="defect">description</tag></root>`
173: *
174: * @param array $input Array with data
175: * @param array $options The options to use
176: * @return SimpleXMLElement|DOMDocument SimpleXMLElement or DOMDocument
177: * @throws XmlException
178: */
179: public static function fromArray($input, $options = array()) {
180: if (!is_array($input) || count($input) !== 1) {
181: throw new XmlException(__d('cake_dev', 'Invalid input.'));
182: }
183: $key = key($input);
184: if (is_integer($key)) {
185: throw new XmlException(__d('cake_dev', 'The key of input must be alphanumeric'));
186: }
187:
188: if (!is_array($options)) {
189: $options = array('format' => (string)$options);
190: }
191: $defaults = array(
192: 'format' => 'tags',
193: 'version' => '1.0',
194: 'encoding' => Configure::read('App.encoding'),
195: 'return' => 'simplexml'
196: );
197: $options = array_merge($defaults, $options);
198:
199: $dom = new DOMDocument($options['version'], $options['encoding']);
200: self::_fromArray($dom, $dom, $input, $options['format']);
201:
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: /**
210: * Recursive method to create childs from array
211: *
212: * @param DOMDocument $dom Handler to DOMDocument
213: * @param DOMElement $node Handler to DOMElement (child)
214: * @param array $data Array of data to append to the $node.
215: * @param string $format Either 'attribute' or 'tags'. This determines where nested keys go.
216: * @return void
217: * @throws XmlException
218: */
219: protected static function _fromArray($dom, $node, &$data, $format) {
220: if (empty($data) || !is_array($data)) {
221: return;
222: }
223: foreach ($data as $key => $value) {
224: if (is_string($key)) {
225: if (!is_array($value)) {
226: if (is_bool($value)) {
227: $value = (int)$value;
228: } elseif ($value === null) {
229: $value = '';
230: }
231: $isNamespace = strpos($key, 'xmlns:');
232: if ($isNamespace !== false) {
233: $node->setAttributeNS('http://www.w3.org/2000/xmlns/', $key, $value);
234: continue;
235: }
236: if ($key[0] !== '@' && $format === 'tags') {
237: $child = null;
238: if (!is_numeric($value)) {
239: // Escape special characters
240: // http://www.w3.org/TR/REC-xml/#syntax
241: // https://bugs.php.net/bug.php?id=36795
242: $child = $dom->createElement($key, '');
243: $child->appendChild(new DOMText($value));
244: } else {
245: $child = $dom->createElement($key, $value);
246: }
247: $node->appendChild($child);
248: } else {
249: if ($key[0] === '@') {
250: $key = substr($key, 1);
251: }
252: $attribute = $dom->createAttribute($key);
253: $attribute->appendChild($dom->createTextNode($value));
254: $node->appendChild($attribute);
255: }
256: } else {
257: if ($key[0] === '@') {
258: throw new XmlException(__d('cake_dev', 'Invalid array'));
259: }
260: if (is_numeric(implode('', array_keys($value)))) { // List
261: foreach ($value as $item) {
262: $itemData = compact('dom', 'node', 'key', 'format');
263: $itemData['value'] = $item;
264: self::_createChild($itemData);
265: }
266: } else { // Struct
267: self::_createChild(compact('dom', 'node', 'key', 'value', 'format'));
268: }
269: }
270: } else {
271: throw new XmlException(__d('cake_dev', 'Invalid array'));
272: }
273: }
274: }
275:
276: /**
277: * Helper to _fromArray(). It will create childs of arrays
278: *
279: * @param array $data Array with informations to create childs
280: * @return void
281: */
282: protected static function _createChild($data) {
283: extract($data);
284: $childNS = $childValue = null;
285: if (is_array($value)) {
286: if (isset($value['@'])) {
287: $childValue = (string)$value['@'];
288: unset($value['@']);
289: }
290: if (isset($value['xmlns:'])) {
291: $childNS = $value['xmlns:'];
292: unset($value['xmlns:']);
293: }
294: } elseif (!empty($value) || $value === 0) {
295: $childValue = (string)$value;
296: }
297:
298: if ($childValue) {
299: $child = $dom->createElement($key, $childValue);
300: } else {
301: $child = $dom->createElement($key);
302: }
303: if ($childNS) {
304: $child->setAttribute('xmlns', $childNS);
305: }
306:
307: self::_fromArray($dom, $child, $value, $format);
308: $node->appendChild($child);
309: }
310:
311: /**
312: * Returns this XML structure as a array.
313: *
314: * @param SimpleXMLElement|DOMDocument|DOMNode $obj SimpleXMLElement, DOMDocument or DOMNode instance
315: * @return array Array representation of the XML structure.
316: * @throws XmlException
317: */
318: public static function toArray($obj) {
319: if ($obj instanceof DOMNode) {
320: $obj = simplexml_import_dom($obj);
321: }
322: if (!($obj instanceof SimpleXMLElement)) {
323: throw new XmlException(__d('cake_dev', 'The input is not instance of SimpleXMLElement, DOMDocument or DOMNode.'));
324: }
325: $result = array();
326: $namespaces = array_merge(array('' => ''), $obj->getNamespaces(true));
327: self::_toArray($obj, $result, '', array_keys($namespaces));
328: return $result;
329: }
330:
331: /**
332: * Recursive method to toArray
333: *
334: * @param SimpleXMLElement $xml SimpleXMLElement object
335: * @param array $parentData Parent array with data
336: * @param string $ns Namespace of current child
337: * @param array $namespaces List of namespaces in XML
338: * @return void
339: */
340: protected static function _toArray($xml, &$parentData, $ns, $namespaces) {
341: $data = array();
342:
343: foreach ($namespaces as $namespace) {
344: foreach ($xml->attributes($namespace, true) as $key => $value) {
345: if (!empty($namespace)) {
346: $key = $namespace . ':' . $key;
347: }
348: $data['@' . $key] = (string)$value;
349: }
350:
351: foreach ($xml->children($namespace, true) as $child) {
352: self::_toArray($child, $data, $namespace, $namespaces);
353: }
354: }
355:
356: $asString = trim((string)$xml);
357: if (empty($data)) {
358: $data = $asString;
359: } elseif (!empty($asString)) {
360: $data['@'] = $asString;
361: }
362:
363: if (!empty($ns)) {
364: $ns .= ':';
365: }
366: $name = $ns . $xml->getName();
367: if (isset($parentData[$name])) {
368: if (!is_array($parentData[$name]) || !isset($parentData[$name][0])) {
369: $parentData[$name] = array($parentData[$name]);
370: }
371: $parentData[$name][] = $data;
372: } else {
373: $parentData[$name] = $data;
374: }
375: }
376:
377: }
378: