Version 1
[yaffs-website] / vendor / phpunit / phpunit / src / Util / XML.php
diff --git a/vendor/phpunit/phpunit/src/Util/XML.php b/vendor/phpunit/phpunit/src/Util/XML.php
new file mode 100644 (file)
index 0000000..c73438f
--- /dev/null
@@ -0,0 +1,946 @@
+<?php
+/*
+ * This file is part of PHPUnit.
+ *
+ * (c) Sebastian Bergmann <sebastian@phpunit.de>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+/**
+ * XML helpers.
+ *
+ * @since Class available since Release 3.2.0
+ */
+class PHPUnit_Util_XML
+{
+    /**
+     * Escapes a string for the use in XML documents
+     * Any Unicode character is allowed, excluding the surrogate blocks, FFFE,
+     * and FFFF (not even as character reference).
+     * See http://www.w3.org/TR/xml/#charsets
+     *
+     * @param string $string
+     *
+     * @return string
+     *
+     * @since  Method available since Release 3.4.6
+     */
+    public static function prepareString($string)
+    {
+        return preg_replace(
+            '/[\\x00-\\x08\\x0b\\x0c\\x0e-\\x1f\\x7f]/',
+            '',
+            htmlspecialchars(
+                PHPUnit_Util_String::convertToUtf8($string),
+                ENT_QUOTES,
+                'UTF-8'
+            )
+        );
+    }
+
+    /**
+     * Loads an XML (or HTML) file into a DOMDocument object.
+     *
+     * @param string $filename
+     * @param bool   $isHtml
+     * @param bool   $xinclude
+     * @param bool   $strict
+     *
+     * @return DOMDocument
+     *
+     * @since  Method available since Release 3.3.0
+     */
+    public static function loadFile($filename, $isHtml = false, $xinclude = false, $strict = false)
+    {
+        $reporting = error_reporting(0);
+        $contents  = file_get_contents($filename);
+        error_reporting($reporting);
+
+        if ($contents === false) {
+            throw new PHPUnit_Framework_Exception(
+                sprintf(
+                    'Could not read "%s".',
+                    $filename
+                )
+            );
+        }
+
+        return self::load($contents, $isHtml, $filename, $xinclude, $strict);
+    }
+
+    /**
+     * Load an $actual document into a DOMDocument.  This is called
+     * from the selector assertions.
+     *
+     * If $actual is already a DOMDocument, it is returned with
+     * no changes.  Otherwise, $actual is loaded into a new DOMDocument
+     * as either HTML or XML, depending on the value of $isHtml. If $isHtml is
+     * false and $xinclude is true, xinclude is performed on the loaded
+     * DOMDocument.
+     *
+     * Note: prior to PHPUnit 3.3.0, this method loaded a file and
+     * not a string as it currently does.  To load a file into a
+     * DOMDocument, use loadFile() instead.
+     *
+     * @param string|DOMDocument $actual
+     * @param bool               $isHtml
+     * @param string             $filename
+     * @param bool               $xinclude
+     * @param bool               $strict
+     *
+     * @return DOMDocument
+     *
+     * @since  Method available since Release 3.3.0
+     */
+    public static function load($actual, $isHtml = false, $filename = '', $xinclude = false, $strict = false)
+    {
+        if ($actual instanceof DOMDocument) {
+            return $actual;
+        }
+
+        if (!is_string($actual)) {
+            throw new PHPUnit_Framework_Exception('Could not load XML from ' . gettype($actual));
+        }
+
+        if ($actual === '') {
+            throw new PHPUnit_Framework_Exception('Could not load XML from empty string');
+        }
+
+        // Required for XInclude on Windows.
+        if ($xinclude) {
+            $cwd = getcwd();
+            @chdir(dirname($filename));
+        }
+
+        $document                     = new DOMDocument;
+        $document->preserveWhiteSpace = false;
+
+        $internal  = libxml_use_internal_errors(true);
+        $message   = '';
+        $reporting = error_reporting(0);
+
+        if ('' !== $filename) {
+            // Necessary for xinclude
+            $document->documentURI = $filename;
+        }
+
+        if ($isHtml) {
+            $loaded = $document->loadHTML($actual);
+        } else {
+            $loaded = $document->loadXML($actual);
+        }
+
+        if (!$isHtml && $xinclude) {
+            $document->xinclude();
+        }
+
+        foreach (libxml_get_errors() as $error) {
+            $message .= "\n" . $error->message;
+        }
+
+        libxml_use_internal_errors($internal);
+        error_reporting($reporting);
+
+        if ($xinclude) {
+            @chdir($cwd);
+        }
+
+        if ($loaded === false || ($strict && $message !== '')) {
+            if ($filename !== '') {
+                throw new PHPUnit_Framework_Exception(
+                    sprintf(
+                        'Could not load "%s".%s',
+                        $filename,
+                        $message != '' ? "\n" . $message : ''
+                    )
+                );
+            } else {
+                if ($message === '') {
+                    $message = 'Could not load XML for unknown reason';
+                }
+                throw new PHPUnit_Framework_Exception($message);
+            }
+        }
+
+        return $document;
+    }
+
+    /**
+     * @param DOMNode $node
+     *
+     * @return string
+     *
+     * @since  Method available since Release 3.4.0
+     */
+    public static function nodeToText(DOMNode $node)
+    {
+        if ($node->childNodes->length == 1) {
+            return $node->textContent;
+        }
+
+        $result = '';
+
+        foreach ($node->childNodes as $childNode) {
+            $result .= $node->ownerDocument->saveXML($childNode);
+        }
+
+        return $result;
+    }
+
+    /**
+     * @param DOMNode $node
+     *
+     * @since  Method available since Release 3.3.0
+     */
+    public static function removeCharacterDataNodes(DOMNode $node)
+    {
+        if ($node->hasChildNodes()) {
+            for ($i = $node->childNodes->length - 1; $i >= 0; $i--) {
+                if (($child = $node->childNodes->item($i)) instanceof DOMCharacterData) {
+                    $node->removeChild($child);
+                }
+            }
+        }
+    }
+
+    /**
+     * "Convert" a DOMElement object into a PHP variable.
+     *
+     * @param DOMElement $element
+     *
+     * @return mixed
+     *
+     * @since  Method available since Release 3.4.0
+     */
+    public static function xmlToVariable(DOMElement $element)
+    {
+        $variable = null;
+
+        switch ($element->tagName) {
+            case 'array':
+                $variable = array();
+
+                foreach ($element->childNodes as $entry) {
+                    if (!$entry instanceof DOMElement || $entry->tagName !== 'element') {
+                        continue;
+                    }
+                    $item = $entry->childNodes->item(0);
+
+                    if ($item instanceof DOMText) {
+                        $item = $entry->childNodes->item(1);
+                    }
+
+                    $value = self::xmlToVariable($item);
+
+                    if ($entry->hasAttribute('key')) {
+                        $variable[(string) $entry->getAttribute('key')] = $value;
+                    } else {
+                        $variable[] = $value;
+                    }
+                }
+                break;
+
+            case 'object':
+                $className = $element->getAttribute('class');
+
+                if ($element->hasChildNodes()) {
+                    $arguments       = $element->childNodes->item(1)->childNodes;
+                    $constructorArgs = array();
+
+                    foreach ($arguments as $argument) {
+                        if ($argument instanceof DOMElement) {
+                            $constructorArgs[] = self::xmlToVariable($argument);
+                        }
+                    }
+
+                    $class    = new ReflectionClass($className);
+                    $variable = $class->newInstanceArgs($constructorArgs);
+                } else {
+                    $variable = new $className;
+                }
+                break;
+
+            case 'boolean':
+                $variable = $element->textContent == 'true' ? true : false;
+                break;
+
+            case 'integer':
+            case 'double':
+            case 'string':
+                $variable = $element->textContent;
+
+                settype($variable, $element->tagName);
+                break;
+        }
+
+        return $variable;
+    }
+
+    /**
+     * Validate list of keys in the associative array.
+     *
+     * @param array $hash
+     * @param array $validKeys
+     *
+     * @return array
+     *
+     * @throws PHPUnit_Framework_Exception
+     *
+     * @since  Method available since Release 3.3.0
+     */
+    public static function assertValidKeys(array $hash, array $validKeys)
+    {
+        $valids = array();
+
+        // Normalize validation keys so that we can use both indexed and
+        // associative arrays.
+        foreach ($validKeys as $key => $val) {
+            is_int($key) ? $valids[$val] = null : $valids[$key] = $val;
+        }
+
+        $validKeys = array_keys($valids);
+
+        // Check for invalid keys.
+        foreach ($hash as $key => $value) {
+            if (!in_array($key, $validKeys)) {
+                $unknown[] = $key;
+            }
+        }
+
+        if (!empty($unknown)) {
+            throw new PHPUnit_Framework_Exception(
+                'Unknown key(s): ' . implode(', ', $unknown)
+            );
+        }
+
+        // Add default values for any valid keys that are empty.
+        foreach ($valids as $key => $value) {
+            if (!isset($hash[$key])) {
+                $hash[$key] = $value;
+            }
+        }
+
+        return $hash;
+    }
+
+    /**
+     * Parse a CSS selector into an associative array suitable for
+     * use with findNodes().
+     *
+     * @param string $selector
+     * @param mixed  $content
+     *
+     * @return array
+     *
+     * @since  Method available since Release 3.3.0
+     */
+    public static function convertSelectToTag($selector, $content = true)
+    {
+        $selector = trim(preg_replace("/\s+/", ' ', $selector));
+
+        // substitute spaces within attribute value
+        while (preg_match('/\[[^\]]+"[^"]+\s[^"]+"\]/', $selector)) {
+            $selector = preg_replace(
+                '/(\[[^\]]+"[^"]+)\s([^"]+"\])/',
+                '$1__SPACE__$2',
+                $selector
+            );
+        }
+
+        if (strstr($selector, ' ')) {
+            $elements = explode(' ', $selector);
+        } else {
+            $elements = array($selector);
+        }
+
+        $previousTag = array();
+
+        foreach (array_reverse($elements) as $element) {
+            $element = str_replace('__SPACE__', ' ', $element);
+
+            // child selector
+            if ($element == '>') {
+                $previousTag = array('child' => $previousTag['descendant']);
+                continue;
+            }
+
+            // adjacent-sibling selector
+            if ($element == '+') {
+                $previousTag = array('adjacent-sibling' => $previousTag['descendant']);
+                continue;
+            }
+
+            $tag = array();
+
+            // match element tag
+            preg_match("/^([^\.#\[]*)/", $element, $eltMatches);
+
+            if (!empty($eltMatches[1])) {
+                $tag['tag'] = $eltMatches[1];
+            }
+
+            // match attributes (\[[^\]]*\]*), ids (#[^\.#\[]*),
+            // and classes (\.[^\.#\[]*))
+            preg_match_all(
+                "/(\[[^\]]*\]*|#[^\.#\[]*|\.[^\.#\[]*)/",
+                $element,
+                $matches
+            );
+
+            if (!empty($matches[1])) {
+                $classes = array();
+                $attrs   = array();
+
+                foreach ($matches[1] as $match) {
+                    // id matched
+                    if (substr($match, 0, 1) == '#') {
+                        $tag['id'] = substr($match, 1);
+                    } // class matched
+                    elseif (substr($match, 0, 1) == '.') {
+                        $classes[] = substr($match, 1);
+                    } // attribute matched
+                    elseif (substr($match, 0, 1) == '[' &&
+                             substr($match, -1, 1) == ']') {
+                        $attribute = substr($match, 1, strlen($match) - 2);
+                        $attribute = str_replace('"', '', $attribute);
+
+                        // match single word
+                        if (strstr($attribute, '~=')) {
+                            list($key, $value) = explode('~=', $attribute);
+                            $value             = "regexp:/.*\b$value\b.*/";
+                        } // match substring
+                        elseif (strstr($attribute, '*=')) {
+                            list($key, $value) = explode('*=', $attribute);
+                            $value             = "regexp:/.*$value.*/";
+                        } // exact match
+                        else {
+                            list($key, $value) = explode('=', $attribute);
+                        }
+
+                        $attrs[$key] = $value;
+                    }
+                }
+
+                if (!empty($classes)) {
+                    $tag['class'] = implode(' ', $classes);
+                }
+
+                if (!empty($attrs)) {
+                    $tag['attributes'] = $attrs;
+                }
+            }
+
+            // tag content
+            if (is_string($content)) {
+                $tag['content'] = $content;
+            }
+
+            // determine previous child/descendants
+            if (!empty($previousTag['descendant'])) {
+                $tag['descendant'] = $previousTag['descendant'];
+            } elseif (!empty($previousTag['child'])) {
+                $tag['child'] = $previousTag['child'];
+            } elseif (!empty($previousTag['adjacent-sibling'])) {
+                $tag['adjacent-sibling'] = $previousTag['adjacent-sibling'];
+                unset($tag['content']);
+            }
+
+            $previousTag = array('descendant' => $tag);
+        }
+
+        return $tag;
+    }
+
+    /**
+     * Parse an $actual document and return an array of DOMNodes
+     * matching the CSS $selector.  If an error occurs, it will
+     * return false.
+     *
+     * To only return nodes containing a certain content, give
+     * the $content to match as a string.  Otherwise, setting
+     * $content to true will return all nodes matching $selector.
+     *
+     * The $actual document may be a DOMDocument or a string
+     * containing XML or HTML, identified by $isHtml.
+     *
+     * @param array  $selector
+     * @param string $content
+     * @param mixed  $actual
+     * @param bool   $isHtml
+     *
+     * @return bool|array
+     *
+     * @since  Method available since Release 3.3.0
+     */
+    public static function cssSelect($selector, $content, $actual, $isHtml = true)
+    {
+        $matcher = self::convertSelectToTag($selector, $content);
+        $dom     = self::load($actual, $isHtml);
+        $tags    = self::findNodes($dom, $matcher, $isHtml);
+
+        return $tags;
+    }
+
+    /**
+     * Parse out the options from the tag using DOM object tree.
+     *
+     * @param DOMDocument $dom
+     * @param array       $options
+     * @param bool        $isHtml
+     *
+     * @return array
+     *
+     * @since  Method available since Release 3.3.0
+     */
+    public static function findNodes(DOMDocument $dom, array $options, $isHtml = true)
+    {
+        $valid = array(
+          'id', 'class', 'tag', 'content', 'attributes', 'parent',
+          'child', 'ancestor', 'descendant', 'children', 'adjacent-sibling'
+        );
+
+        $filtered = array();
+        $options  = self::assertValidKeys($options, $valid);
+
+        // find the element by id
+        if ($options['id']) {
+            $options['attributes']['id'] = $options['id'];
+        }
+
+        if ($options['class']) {
+            $options['attributes']['class'] = $options['class'];
+        }
+
+        $nodes = array();
+
+        // find the element by a tag type
+        if ($options['tag']) {
+            if ($isHtml) {
+                $elements = self::getElementsByCaseInsensitiveTagName(
+                    $dom,
+                    $options['tag']
+                );
+            } else {
+                $elements = $dom->getElementsByTagName($options['tag']);
+            }
+
+            foreach ($elements as $element) {
+                $nodes[] = $element;
+            }
+
+            if (empty($nodes)) {
+                return false;
+            }
+        } // no tag selected, get them all
+        else {
+            $tags = array(
+              'a', 'abbr', 'acronym', 'address', 'area', 'b', 'base', 'bdo',
+              'big', 'blockquote', 'body', 'br', 'button', 'caption', 'cite',
+              'code', 'col', 'colgroup', 'dd', 'del', 'div', 'dfn', 'dl',
+              'dt', 'em', 'fieldset', 'form', 'frame', 'frameset', 'h1', 'h2',
+              'h3', 'h4', 'h5', 'h6', 'head', 'hr', 'html', 'i', 'iframe',
+              'img', 'input', 'ins', 'kbd', 'label', 'legend', 'li', 'link',
+              'map', 'meta', 'noframes', 'noscript', 'object', 'ol', 'optgroup',
+              'option', 'p', 'param', 'pre', 'q', 'samp', 'script', 'select',
+              'small', 'span', 'strong', 'style', 'sub', 'sup', 'table',
+              'tbody', 'td', 'textarea', 'tfoot', 'th', 'thead', 'title',
+              'tr', 'tt', 'ul', 'var',
+              // HTML5
+              'article', 'aside', 'audio', 'bdi', 'canvas', 'command',
+              'datalist', 'details', 'dialog', 'embed', 'figure', 'figcaption',
+              'footer', 'header', 'hgroup', 'keygen', 'mark', 'meter', 'nav',
+              'output', 'progress', 'ruby', 'rt', 'rp', 'track', 'section',
+              'source', 'summary', 'time', 'video', 'wbr'
+            );
+
+            foreach ($tags as $tag) {
+                if ($isHtml) {
+                    $elements = self::getElementsByCaseInsensitiveTagName(
+                        $dom,
+                        $tag
+                    );
+                } else {
+                    $elements = $dom->getElementsByTagName($tag);
+                }
+
+                foreach ($elements as $element) {
+                    $nodes[] = $element;
+                }
+            }
+
+            if (empty($nodes)) {
+                return false;
+            }
+        }
+
+        // filter by attributes
+        if ($options['attributes']) {
+            foreach ($nodes as $node) {
+                $invalid = false;
+
+                foreach ($options['attributes'] as $name => $value) {
+                    // match by regexp if like "regexp:/foo/i"
+                    if (preg_match('/^regexp\s*:\s*(.*)/i', $value, $matches)) {
+                        if (!preg_match($matches[1], $node->getAttribute($name))) {
+                            $invalid = true;
+                        }
+                    } // class can match only a part
+                    elseif ($name == 'class') {
+                        // split to individual classes
+                        $findClasses = explode(
+                            ' ',
+                            preg_replace("/\s+/", ' ', $value)
+                        );
+
+                        $allClasses = explode(
+                            ' ',
+                            preg_replace("/\s+/", ' ', $node->getAttribute($name))
+                        );
+
+                        // make sure each class given is in the actual node
+                        foreach ($findClasses as $findClass) {
+                            if (!in_array($findClass, $allClasses)) {
+                                $invalid = true;
+                            }
+                        }
+                    } // match by exact string
+                    else {
+                        if ($node->getAttribute($name) != $value) {
+                            $invalid = true;
+                        }
+                    }
+                }
+
+                // if every attribute given matched
+                if (!$invalid) {
+                    $filtered[] = $node;
+                }
+            }
+
+            $nodes    = $filtered;
+            $filtered = array();
+
+            if (empty($nodes)) {
+                return false;
+            }
+        }
+
+        // filter by content
+        if ($options['content'] !== null) {
+            foreach ($nodes as $node) {
+                $invalid = false;
+
+                // match by regexp if like "regexp:/foo/i"
+                if (preg_match('/^regexp\s*:\s*(.*)/i', $options['content'], $matches)) {
+                    if (!preg_match($matches[1], self::getNodeText($node))) {
+                        $invalid = true;
+                    }
+                } // match empty string
+                elseif ($options['content'] === '') {
+                    if (self::getNodeText($node) !== '') {
+                        $invalid = true;
+                    }
+                } // match by exact string
+                elseif (strstr(self::getNodeText($node), $options['content']) === false) {
+                    $invalid = true;
+                }
+
+                if (!$invalid) {
+                    $filtered[] = $node;
+                }
+            }
+
+            $nodes    = $filtered;
+            $filtered = array();
+
+            if (empty($nodes)) {
+                return false;
+            }
+        }
+
+        // filter by parent node
+        if ($options['parent']) {
+            $parentNodes = self::findNodes($dom, $options['parent'], $isHtml);
+            $parentNode  = isset($parentNodes[0]) ? $parentNodes[0] : null;
+
+            foreach ($nodes as $node) {
+                if ($parentNode !== $node->parentNode) {
+                    continue;
+                }
+
+                $filtered[] = $node;
+            }
+
+            $nodes    = $filtered;
+            $filtered = array();
+
+            if (empty($nodes)) {
+                return false;
+            }
+        }
+
+        // filter by child node
+        if ($options['child']) {
+            $childNodes = self::findNodes($dom, $options['child'], $isHtml);
+            $childNodes = !empty($childNodes) ? $childNodes : array();
+
+            foreach ($nodes as $node) {
+                foreach ($node->childNodes as $child) {
+                    foreach ($childNodes as $childNode) {
+                        if ($childNode === $child) {
+                            $filtered[] = $node;
+                        }
+                    }
+                }
+            }
+
+            $nodes    = $filtered;
+            $filtered = array();
+
+            if (empty($nodes)) {
+                return false;
+            }
+        }
+
+        // filter by adjacent-sibling
+        if ($options['adjacent-sibling']) {
+            $adjacentSiblingNodes = self::findNodes($dom, $options['adjacent-sibling'], $isHtml);
+            $adjacentSiblingNodes = !empty($adjacentSiblingNodes) ? $adjacentSiblingNodes : array();
+
+            foreach ($nodes as $node) {
+                $sibling = $node;
+
+                while ($sibling = $sibling->nextSibling) {
+                    if ($sibling->nodeType !== XML_ELEMENT_NODE) {
+                        continue;
+                    }
+
+                    foreach ($adjacentSiblingNodes as $adjacentSiblingNode) {
+                        if ($sibling === $adjacentSiblingNode) {
+                            $filtered[] = $node;
+                            break;
+                        }
+                    }
+
+                    break;
+                }
+            }
+
+            $nodes    = $filtered;
+            $filtered = array();
+
+            if (empty($nodes)) {
+                return false;
+            }
+        }
+
+        // filter by ancestor
+        if ($options['ancestor']) {
+            $ancestorNodes = self::findNodes($dom, $options['ancestor'], $isHtml);
+            $ancestorNode  = isset($ancestorNodes[0]) ? $ancestorNodes[0] : null;
+
+            foreach ($nodes as $node) {
+                $parent = $node->parentNode;
+
+                while ($parent && $parent->nodeType != XML_HTML_DOCUMENT_NODE) {
+                    if ($parent === $ancestorNode) {
+                        $filtered[] = $node;
+                    }
+
+                    $parent = $parent->parentNode;
+                }
+            }
+
+            $nodes    = $filtered;
+            $filtered = array();
+
+            if (empty($nodes)) {
+                return false;
+            }
+        }
+
+        // filter by descendant
+        if ($options['descendant']) {
+            $descendantNodes = self::findNodes($dom, $options['descendant'], $isHtml);
+            $descendantNodes = !empty($descendantNodes) ? $descendantNodes : array();
+
+            foreach ($nodes as $node) {
+                foreach (self::getDescendants($node) as $descendant) {
+                    foreach ($descendantNodes as $descendantNode) {
+                        if ($descendantNode === $descendant) {
+                            $filtered[] = $node;
+                        }
+                    }
+                }
+            }
+
+            $nodes    = $filtered;
+            $filtered = array();
+
+            if (empty($nodes)) {
+                return false;
+            }
+        }
+
+        // filter by children
+        if ($options['children']) {
+            $validChild   = array('count', 'greater_than', 'less_than', 'only');
+            $childOptions = self::assertValidKeys(
+                $options['children'],
+                $validChild
+            );
+
+            foreach ($nodes as $node) {
+                $childNodes = $node->childNodes;
+
+                foreach ($childNodes as $childNode) {
+                    if ($childNode->nodeType !== XML_CDATA_SECTION_NODE &&
+                        $childNode->nodeType !== XML_TEXT_NODE) {
+                        $children[] = $childNode;
+                    }
+                }
+
+                // we must have children to pass this filter
+                if (!empty($children)) {
+                    // exact count of children
+                    if ($childOptions['count'] !== null) {
+                        if (count($children) !== $childOptions['count']) {
+                            break;
+                        }
+                    } // range count of children
+                    elseif ($childOptions['less_than']    !== null &&
+                            $childOptions['greater_than'] !== null) {
+                        if (count($children) >= $childOptions['less_than'] ||
+                            count($children) <= $childOptions['greater_than']) {
+                            break;
+                        }
+                    } // less than a given count
+                    elseif ($childOptions['less_than'] !== null) {
+                        if (count($children) >= $childOptions['less_than']) {
+                            break;
+                        }
+                    } // more than a given count
+                    elseif ($childOptions['greater_than'] !== null) {
+                        if (count($children) <= $childOptions['greater_than']) {
+                            break;
+                        }
+                    }
+
+                    // match each child against a specific tag
+                    if ($childOptions['only']) {
+                        $onlyNodes = self::findNodes(
+                            $dom,
+                            $childOptions['only'],
+                            $isHtml
+                        );
+
+                        // try to match each child to one of the 'only' nodes
+                        foreach ($children as $child) {
+                            $matched = false;
+
+                            foreach ($onlyNodes as $onlyNode) {
+                                if ($onlyNode === $child) {
+                                    $matched = true;
+                                }
+                            }
+
+                            if (!$matched) {
+                                break 2;
+                            }
+                        }
+                    }
+
+                    $filtered[] = $node;
+                }
+            }
+
+            $nodes = $filtered;
+
+            if (empty($nodes)) {
+                return;
+            }
+        }
+
+        // return the first node that matches all criteria
+        return !empty($nodes) ? $nodes : array();
+    }
+
+    /**
+     * Recursively get flat array of all descendants of this node.
+     *
+     * @param DOMNode $node
+     *
+     * @return array
+     *
+     * @since  Method available since Release 3.3.0
+     */
+    protected static function getDescendants(DOMNode $node)
+    {
+        $allChildren = array();
+        $childNodes  = $node->childNodes ? $node->childNodes : array();
+
+        foreach ($childNodes as $child) {
+            if ($child->nodeType === XML_CDATA_SECTION_NODE ||
+                $child->nodeType === XML_TEXT_NODE) {
+                continue;
+            }
+
+            $children    = self::getDescendants($child);
+            $allChildren = array_merge($allChildren, $children, array($child));
+        }
+
+        return isset($allChildren) ? $allChildren : array();
+    }
+
+    /**
+     * Gets elements by case insensitive tagname.
+     *
+     * @param DOMDocument $dom
+     * @param string      $tag
+     *
+     * @return DOMNodeList
+     *
+     * @since  Method available since Release 3.4.0
+     */
+    protected static function getElementsByCaseInsensitiveTagName(DOMDocument $dom, $tag)
+    {
+        $elements = $dom->getElementsByTagName(strtolower($tag));
+
+        if ($elements->length == 0) {
+            $elements = $dom->getElementsByTagName(strtoupper($tag));
+        }
+
+        return $elements;
+    }
+
+    /**
+     * Get the text value of this node's child text node.
+     *
+     * @param DOMNode $node
+     *
+     * @return string
+     *
+     * @since  Method available since Release 3.3.0
+     */
+    protected static function getNodeText(DOMNode $node)
+    {
+        if (!$node->childNodes instanceof DOMNodeList) {
+            return '';
+        }
+
+        $result = '';
+
+        foreach ($node->childNodes as $childNode) {
+            if ($childNode->nodeType === XML_TEXT_NODE ||
+                $childNode->nodeType === XML_CDATA_SECTION_NODE) {
+                $result .= trim($childNode->data) . ' ';
+            } else {
+                $result .= self::getNodeText($childNode);
+            }
+        }
+
+        return str_replace('  ', ' ', $result);
+    }
+}