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