1<?php
2/*
3 * This file is part of PHPUnit.
4 *
5 * (c) Sebastian Bergmann <sebastian@phpunit.de>
6 *
7 * For the full copyright and license information, please view the LICENSE
8 * file that was distributed with this source code.
9 */
10
11/**
12 * XML helpers.
13 */
14class PHPUnit_Util_XML
15{
16    /**
17     * Load an $actual document into a DOMDocument.  This is called
18     * from the selector assertions.
19     *
20     * If $actual is already a DOMDocument, it is returned with
21     * no changes.  Otherwise, $actual is loaded into a new DOMDocument
22     * as either HTML or XML, depending on the value of $isHtml. If $isHtml is
23     * false and $xinclude is true, xinclude is performed on the loaded
24     * DOMDocument.
25     *
26     * Note: prior to PHPUnit 3.3.0, this method loaded a file and
27     * not a string as it currently does.  To load a file into a
28     * DOMDocument, use loadFile() instead.
29     *
30     * @param string|DOMDocument $actual
31     * @param bool               $isHtml
32     * @param string             $filename
33     * @param bool               $xinclude
34     * @param bool               $strict
35     *
36     * @return DOMDocument
37     */
38    public static function load($actual, $isHtml = false, $filename = '', $xinclude = false, $strict = false)
39    {
40        if ($actual instanceof DOMDocument) {
41            return $actual;
42        }
43
44        if (!is_string($actual)) {
45            throw new PHPUnit_Framework_Exception('Could not load XML from ' . gettype($actual));
46        }
47
48        if ($actual === '') {
49            throw new PHPUnit_Framework_Exception('Could not load XML from empty string');
50        }
51
52        // Required for XInclude on Windows.
53        if ($xinclude) {
54            $cwd = getcwd();
55            @chdir(dirname($filename));
56        }
57
58        $document                     = new DOMDocument;
59        $document->preserveWhiteSpace = false;
60
61        $internal  = libxml_use_internal_errors(true);
62        $message   = '';
63        $reporting = error_reporting(0);
64
65        if ('' !== $filename) {
66            // Necessary for xinclude
67            $document->documentURI = $filename;
68        }
69
70        if ($isHtml) {
71            $loaded = $document->loadHTML($actual);
72        } else {
73            $loaded = $document->loadXML($actual);
74        }
75
76        if (!$isHtml && $xinclude) {
77            $document->xinclude();
78        }
79
80        foreach (libxml_get_errors() as $error) {
81            $message .= "\n" . $error->message;
82        }
83
84        libxml_use_internal_errors($internal);
85        error_reporting($reporting);
86
87        if ($xinclude) {
88            @chdir($cwd);
89        }
90
91        if ($loaded === false || ($strict && $message !== '')) {
92            if ($filename !== '') {
93                throw new PHPUnit_Framework_Exception(
94                    sprintf(
95                        'Could not load "%s".%s',
96                        $filename,
97                        $message != '' ? "\n" . $message : ''
98                    )
99                );
100            } else {
101                if ($message === '') {
102                    $message = 'Could not load XML for unknown reason';
103                }
104                throw new PHPUnit_Framework_Exception($message);
105            }
106        }
107
108        return $document;
109    }
110
111    /**
112     * Loads an XML (or HTML) file into a DOMDocument object.
113     *
114     * @param string $filename
115     * @param bool   $isHtml
116     * @param bool   $xinclude
117     * @param bool   $strict
118     *
119     * @return DOMDocument
120     */
121    public static function loadFile($filename, $isHtml = false, $xinclude = false, $strict = false)
122    {
123        $reporting = error_reporting(0);
124        $contents  = file_get_contents($filename);
125        error_reporting($reporting);
126
127        if ($contents === false) {
128            throw new PHPUnit_Framework_Exception(
129                sprintf(
130                    'Could not read "%s".',
131                    $filename
132                )
133            );
134        }
135
136        return self::load($contents, $isHtml, $filename, $xinclude, $strict);
137    }
138
139    /**
140     * @param DOMNode $node
141     */
142    public static function removeCharacterDataNodes(DOMNode $node)
143    {
144        if ($node->hasChildNodes()) {
145            for ($i = $node->childNodes->length - 1; $i >= 0; $i--) {
146                if (($child = $node->childNodes->item($i)) instanceof DOMCharacterData) {
147                    $node->removeChild($child);
148                }
149            }
150        }
151    }
152
153    /**
154     * Escapes a string for the use in XML documents
155     * Any Unicode character is allowed, excluding the surrogate blocks, FFFE,
156     * and FFFF (not even as character reference).
157     * See http://www.w3.org/TR/xml/#charsets
158     *
159     * @param string $string
160     *
161     * @return string
162     */
163    public static function prepareString($string)
164    {
165        return preg_replace(
166            '/[\\x00-\\x08\\x0b\\x0c\\x0e-\\x1f\\x7f]/',
167            '',
168            htmlspecialchars(
169                PHPUnit_Util_String::convertToUtf8($string),
170                ENT_QUOTES,
171                'UTF-8'
172            )
173        );
174    }
175
176    /**
177     * "Convert" a DOMElement object into a PHP variable.
178     *
179     * @param DOMElement $element
180     *
181     * @return mixed
182     */
183    public static function xmlToVariable(DOMElement $element)
184    {
185        $variable = null;
186
187        switch ($element->tagName) {
188            case 'array':
189                $variable = [];
190
191                foreach ($element->childNodes as $entry) {
192                    if (!$entry instanceof DOMElement || $entry->tagName !== 'element') {
193                        continue;
194                    }
195                    $item = $entry->childNodes->item(0);
196
197                    if ($item instanceof DOMText) {
198                        $item = $entry->childNodes->item(1);
199                    }
200
201                    $value = self::xmlToVariable($item);
202
203                    if ($entry->hasAttribute('key')) {
204                        $variable[(string) $entry->getAttribute('key')] = $value;
205                    } else {
206                        $variable[] = $value;
207                    }
208                }
209                break;
210
211            case 'object':
212                $className = $element->getAttribute('class');
213
214                if ($element->hasChildNodes()) {
215                    $arguments       = $element->childNodes->item(1)->childNodes;
216                    $constructorArgs = [];
217
218                    foreach ($arguments as $argument) {
219                        if ($argument instanceof DOMElement) {
220                            $constructorArgs[] = self::xmlToVariable($argument);
221                        }
222                    }
223
224                    $class    = new ReflectionClass($className);
225                    $variable = $class->newInstanceArgs($constructorArgs);
226                } else {
227                    $variable = new $className;
228                }
229                break;
230
231            case 'boolean':
232                $variable = $element->textContent == 'true' ? true : false;
233                break;
234
235            case 'integer':
236            case 'double':
237            case 'string':
238                $variable = $element->textContent;
239
240                settype($variable, $element->tagName);
241                break;
242        }
243
244        return $variable;
245    }
246}
247