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