PageRenderTime 60ms CodeModel.GetById 27ms RepoModel.GetById 0ms app.codeStats 0ms

/Vendor/phpunit/phpunit/PHPUnit/Util/XML.php

https://bitbucket.org/daveschwan/ronin-group
PHP | 920 lines | 541 code | 138 blank | 241 comment | 124 complexity | ab5873c3bdb6e03e56607ea12208abbc MD5 | raw file
Possible License(s): LGPL-2.1, MPL-2.0-no-copyleft-exception, MIT, BSD-3-Clause, Apache-2.0
  1. <?php
  2. /**
  3. * PHPUnit
  4. *
  5. * Copyright (c) 2001-2014, Sebastian Bergmann <sebastian@phpunit.de>.
  6. * All rights reserved.
  7. *
  8. * Redistribution and use in source and binary forms, with or without
  9. * modification, are permitted provided that the following conditions
  10. * are met:
  11. *
  12. * * Redistributions of source code must retain the above copyright
  13. * notice, this list of conditions and the following disclaimer.
  14. *
  15. * * Redistributions in binary form must reproduce the above copyright
  16. * notice, this list of conditions and the following disclaimer in
  17. * the documentation and/or other materials provided with the
  18. * distribution.
  19. *
  20. * * Neither the name of Sebastian Bergmann nor the names of his
  21. * contributors may be used to endorse or promote products derived
  22. * from this software without specific prior written permission.
  23. *
  24. * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
  25. * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
  26. * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
  27. * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
  28. * COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
  29. * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
  30. * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
  31. * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
  32. * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
  33. * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
  34. * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
  35. * POSSIBILITY OF SUCH DAMAGE.
  36. *
  37. * @package PHPUnit
  38. * @subpackage Util
  39. * @author Sebastian Bergmann <sebastian@phpunit.de>
  40. * @copyright 2001-2014 Sebastian Bergmann <sebastian@phpunit.de>
  41. * @license http://www.opensource.org/licenses/BSD-3-Clause The BSD 3-Clause License
  42. * @link http://www.phpunit.de/
  43. * @since File available since Release 3.2.0
  44. */
  45. /**
  46. * XML helpers.
  47. *
  48. * @package PHPUnit
  49. * @subpackage Util
  50. * @author Sebastian Bergmann <sebastian@phpunit.de>
  51. * @copyright 2001-2014 Sebastian Bergmann <sebastian@phpunit.de>
  52. * @license http://www.opensource.org/licenses/BSD-3-Clause The BSD 3-Clause License
  53. * @link http://www.phpunit.de/
  54. * @since Class available since Release 3.2.0
  55. */
  56. class PHPUnit_Util_XML
  57. {
  58. /**
  59. * @param string $string
  60. * @return string
  61. * @author Kore Nordmann <mail@kore-nordmann.de>
  62. * @since Method available since Release 3.4.6
  63. */
  64. public static function prepareString($string)
  65. {
  66. return preg_replace_callback(
  67. '/[\\x00-\\x04\\x0b\\x0c\\x0e-\\x1f\\x7f]/',
  68. function ($matches)
  69. {
  70. return sprintf('&#x%02x;', ord($matches[0]));
  71. },
  72. htmlspecialchars(
  73. PHPUnit_Util_String::convertToUtf8($string), ENT_COMPAT, 'UTF-8'
  74. )
  75. );
  76. }
  77. /**
  78. * Loads an XML (or HTML) file into a DOMDocument object.
  79. *
  80. * @param string $filename
  81. * @param boolean $isHtml
  82. * @param boolean $xinclude
  83. * @return DOMDocument
  84. * @since Method available since Release 3.3.0
  85. */
  86. public static function loadFile($filename, $isHtml = FALSE, $xinclude = FALSE)
  87. {
  88. $reporting = error_reporting(0);
  89. $contents = file_get_contents($filename);
  90. error_reporting($reporting);
  91. if ($contents === FALSE) {
  92. throw new PHPUnit_Framework_Exception(
  93. sprintf(
  94. 'Could not read "%s".',
  95. $filename
  96. )
  97. );
  98. }
  99. return self::load($contents, $isHtml, $filename, $xinclude);
  100. }
  101. /**
  102. * Load an $actual document into a DOMDocument. This is called
  103. * from the selector assertions.
  104. *
  105. * If $actual is already a DOMDocument, it is returned with
  106. * no changes. Otherwise, $actual is loaded into a new DOMDocument
  107. * as either HTML or XML, depending on the value of $isHtml. If $isHtml is
  108. * false and $xinclude is true, xinclude is performed on the loaded
  109. * DOMDocument.
  110. *
  111. * Note: prior to PHPUnit 3.3.0, this method loaded a file and
  112. * not a string as it currently does. To load a file into a
  113. * DOMDocument, use loadFile() instead.
  114. *
  115. * @param string|DOMDocument $actual
  116. * @param boolean $isHtml
  117. * @param string $filename
  118. * @param boolean $xinclude
  119. * @return DOMDocument
  120. * @since Method available since Release 3.3.0
  121. * @author Mike Naberezny <mike@maintainable.com>
  122. * @author Derek DeVries <derek@maintainable.com>
  123. * @author Tobias Schlitt <toby@php.net>
  124. */
  125. public static function load($actual, $isHtml = FALSE, $filename = '', $xinclude = FALSE)
  126. {
  127. if ($actual instanceof DOMDocument) {
  128. return $actual;
  129. }
  130. $document = new DOMDocument;
  131. $internal = libxml_use_internal_errors(TRUE);
  132. $message = '';
  133. $reporting = error_reporting(0);
  134. if ($isHtml) {
  135. $loaded = $document->loadHTML($actual);
  136. } else {
  137. $loaded = $document->loadXML($actual);
  138. }
  139. if ('' !== $filename) {
  140. // Necessary for xinclude
  141. $document->documentURI = $filename;
  142. }
  143. if (!$isHtml && $xinclude) {
  144. $document->xinclude();
  145. }
  146. foreach (libxml_get_errors() as $error) {
  147. $message .= $error->message;
  148. }
  149. libxml_use_internal_errors($internal);
  150. error_reporting($reporting);
  151. if ($loaded === FALSE) {
  152. if ($filename != '') {
  153. throw new PHPUnit_Framework_Exception(
  154. sprintf(
  155. 'Could not load "%s".%s',
  156. $filename,
  157. $message != '' ? "\n" . $message : ''
  158. )
  159. );
  160. } else {
  161. throw new PHPUnit_Framework_Exception($message);
  162. }
  163. }
  164. return $document;
  165. }
  166. /**
  167. *
  168. *
  169. * @param DOMNode $node
  170. * @return string
  171. * @since Method available since Release 3.4.0
  172. */
  173. public static function nodeToText(DOMNode $node)
  174. {
  175. if ($node->childNodes->length == 1) {
  176. return $node->nodeValue;
  177. }
  178. $result = '';
  179. foreach ($node->childNodes as $childNode) {
  180. $result .= $node->ownerDocument->saveXML($childNode);
  181. }
  182. return $result;
  183. }
  184. /**
  185. *
  186. *
  187. * @param DOMNode $node
  188. * @since Method available since Release 3.3.0
  189. * @author Mattis Stordalen Flister <mattis@xait.no>
  190. */
  191. public static function removeCharacterDataNodes(DOMNode $node)
  192. {
  193. if ($node->hasChildNodes()) {
  194. for ($i = $node->childNodes->length - 1; $i >= 0; $i--) {
  195. if (($child = $node->childNodes->item($i)) instanceof DOMCharacterData) {
  196. $node->removeChild($child);
  197. }
  198. }
  199. }
  200. }
  201. /**
  202. * "Convert" a DOMElement object into a PHP variable.
  203. *
  204. * @param DOMElement $element
  205. * @return mixed
  206. * @since Method available since Release 3.4.0
  207. */
  208. public static function xmlToVariable(DOMElement $element)
  209. {
  210. $variable = NULL;
  211. switch ($element->tagName) {
  212. case 'array': {
  213. $variable = array();
  214. foreach ($element->getElementsByTagName('element') as $element) {
  215. $value = self::xmlToVariable($element->childNodes->item(1));
  216. if ($element->hasAttribute('key')) {
  217. $variable[(string)$element->getAttribute('key')] = $value;
  218. } else {
  219. $variable[] = $value;
  220. }
  221. }
  222. }
  223. break;
  224. case 'object': {
  225. $className = $element->getAttribute('class');
  226. if ($element->hasChildNodes()) {
  227. $arguments = $element->childNodes->item(1)->childNodes;
  228. $constructorArgs = array();
  229. foreach ($arguments as $argument) {
  230. if ($argument instanceof DOMElement) {
  231. $constructorArgs[] = self::xmlToVariable($argument);
  232. }
  233. }
  234. $class = new ReflectionClass($className);
  235. $variable = $class->newInstanceArgs($constructorArgs);
  236. } else {
  237. $variable = new $className;
  238. }
  239. }
  240. break;
  241. case 'boolean': {
  242. $variable = $element->nodeValue == 'true' ? TRUE : FALSE;
  243. }
  244. break;
  245. case 'integer':
  246. case 'double':
  247. case 'string': {
  248. $variable = $element->nodeValue;
  249. settype($variable, $element->tagName);
  250. }
  251. break;
  252. }
  253. return $variable;
  254. }
  255. /**
  256. * Validate list of keys in the associative array.
  257. *
  258. * @param array $hash
  259. * @param array $validKeys
  260. * @return array
  261. * @throws PHPUnit_Framework_Exception
  262. * @since Method available since Release 3.3.0
  263. * @author Mike Naberezny <mike@maintainable.com>
  264. * @author Derek DeVries <derek@maintainable.com>
  265. */
  266. public static function assertValidKeys(array $hash, array $validKeys)
  267. {
  268. $valids = array();
  269. // Normalize validation keys so that we can use both indexed and
  270. // associative arrays.
  271. foreach ($validKeys as $key => $val) {
  272. is_int($key) ? $valids[$val] = NULL : $valids[$key] = $val;
  273. }
  274. $validKeys = array_keys($valids);
  275. // Check for invalid keys.
  276. foreach ($hash as $key => $value) {
  277. if (!in_array($key, $validKeys)) {
  278. $unknown[] = $key;
  279. }
  280. }
  281. if (!empty($unknown)) {
  282. throw new PHPUnit_Framework_Exception(
  283. 'Unknown key(s): ' . implode(', ', $unknown)
  284. );
  285. }
  286. // Add default values for any valid keys that are empty.
  287. foreach ($valids as $key => $value) {
  288. if (!isset($hash[$key])) {
  289. $hash[$key] = $value;
  290. }
  291. }
  292. return $hash;
  293. }
  294. /**
  295. * Parse a CSS selector into an associative array suitable for
  296. * use with findNodes().
  297. *
  298. * @param string $selector
  299. * @param mixed $content
  300. * @return array
  301. * @since Method available since Release 3.3.0
  302. * @author Mike Naberezny <mike@maintainable.com>
  303. * @author Derek DeVries <derek@maintainable.com>
  304. */
  305. public static function convertSelectToTag($selector, $content = TRUE)
  306. {
  307. $selector = trim(preg_replace("/\s+/", " ", $selector));
  308. // substitute spaces within attribute value
  309. while (preg_match('/\[[^\]]+"[^"]+\s[^"]+"\]/', $selector)) {
  310. $selector = preg_replace(
  311. '/(\[[^\]]+"[^"]+)\s([^"]+"\])/', "$1__SPACE__$2", $selector
  312. );
  313. }
  314. if (strstr($selector, ' ')) {
  315. $elements = explode(' ', $selector);
  316. } else {
  317. $elements = array($selector);
  318. }
  319. $previousTag = array();
  320. foreach (array_reverse($elements) as $element) {
  321. $element = str_replace('__SPACE__', ' ', $element);
  322. // child selector
  323. if ($element == '>') {
  324. $previousTag = array('child' => $previousTag['descendant']);
  325. continue;
  326. }
  327. $tag = array();
  328. // match element tag
  329. preg_match("/^([^\.#\[]*)/", $element, $eltMatches);
  330. if (!empty($eltMatches[1])) {
  331. $tag['tag'] = $eltMatches[1];
  332. }
  333. // match attributes (\[[^\]]*\]*), ids (#[^\.#\[]*),
  334. // and classes (\.[^\.#\[]*))
  335. preg_match_all(
  336. "/(\[[^\]]*\]*|#[^\.#\[]*|\.[^\.#\[]*)/", $element, $matches
  337. );
  338. if (!empty($matches[1])) {
  339. $classes = array();
  340. $attrs = array();
  341. foreach ($matches[1] as $match) {
  342. // id matched
  343. if (substr($match, 0, 1) == '#') {
  344. $tag['id'] = substr($match, 1);
  345. }
  346. // class matched
  347. else if (substr($match, 0, 1) == '.') {
  348. $classes[] = substr($match, 1);
  349. }
  350. // attribute matched
  351. else if (substr($match, 0, 1) == '[' &&
  352. substr($match, -1, 1) == ']') {
  353. $attribute = substr($match, 1, strlen($match) - 2);
  354. $attribute = str_replace('"', '', $attribute);
  355. // match single word
  356. if (strstr($attribute, '~=')) {
  357. list($key, $value) = explode('~=', $attribute);
  358. $value = "regexp:/.*\b$value\b.*/";
  359. }
  360. // match substring
  361. else if (strstr($attribute, '*=')) {
  362. list($key, $value) = explode('*=', $attribute);
  363. $value = "regexp:/.*$value.*/";
  364. }
  365. // exact match
  366. else {
  367. list($key, $value) = explode('=', $attribute);
  368. }
  369. $attrs[$key] = $value;
  370. }
  371. }
  372. if ($classes) {
  373. $tag['class'] = join(' ', $classes);
  374. }
  375. if ($attrs) {
  376. $tag['attributes'] = $attrs;
  377. }
  378. }
  379. // tag content
  380. if (is_string($content)) {
  381. $tag['content'] = $content;
  382. }
  383. // determine previous child/descendants
  384. if (!empty($previousTag['descendant'])) {
  385. $tag['descendant'] = $previousTag['descendant'];
  386. }
  387. else if (!empty($previousTag['child'])) {
  388. $tag['child'] = $previousTag['child'];
  389. }
  390. $previousTag = array('descendant' => $tag);
  391. }
  392. return $tag;
  393. }
  394. /**
  395. * Parse an $actual document and return an array of DOMNodes
  396. * matching the CSS $selector. If an error occurs, it will
  397. * return FALSE.
  398. *
  399. * To only return nodes containing a certain content, give
  400. * the $content to match as a string. Otherwise, setting
  401. * $content to TRUE will return all nodes matching $selector.
  402. *
  403. * The $actual document may be a DOMDocument or a string
  404. * containing XML or HTML, identified by $isHtml.
  405. *
  406. * @param array $selector
  407. * @param string $content
  408. * @param mixed $actual
  409. * @param boolean $isHtml
  410. * @return false|array
  411. * @since Method available since Release 3.3.0
  412. * @author Mike Naberezny <mike@maintainable.com>
  413. * @author Derek DeVries <derek@maintainable.com>
  414. * @author Tobias Schlitt <toby@php.net>
  415. */
  416. public static function cssSelect($selector, $content, $actual, $isHtml = TRUE)
  417. {
  418. $matcher = self::convertSelectToTag($selector, $content);
  419. $dom = self::load($actual, $isHtml);
  420. $tags = self::findNodes($dom, $matcher, $isHtml);
  421. return $tags;
  422. }
  423. /**
  424. * Parse out the options from the tag using DOM object tree.
  425. *
  426. * @param DOMDocument $dom
  427. * @param array $options
  428. * @param boolean $isHtml
  429. * @return array
  430. * @since Method available since Release 3.3.0
  431. * @author Mike Naberezny <mike@maintainable.com>
  432. * @author Derek DeVries <derek@maintainable.com>
  433. * @author Tobias Schlitt <toby@php.net>
  434. */
  435. public static function findNodes(DOMDocument $dom, array $options, $isHtml = TRUE)
  436. {
  437. $valid = array(
  438. 'id', 'class', 'tag', 'content', 'attributes', 'parent',
  439. 'child', 'ancestor', 'descendant', 'children'
  440. );
  441. $filtered = array();
  442. $options = self::assertValidKeys($options, $valid);
  443. // find the element by id
  444. if ($options['id']) {
  445. $options['attributes']['id'] = $options['id'];
  446. }
  447. if ($options['class']) {
  448. $options['attributes']['class'] = $options['class'];
  449. }
  450. // find the element by a tag type
  451. if ($options['tag']) {
  452. if ($isHtml) {
  453. $elements = self::getElementsByCaseInsensitiveTagName(
  454. $dom, $options['tag']
  455. );
  456. } else {
  457. $elements = $dom->getElementsByTagName($options['tag']);
  458. }
  459. foreach ($elements as $element) {
  460. $nodes[] = $element;
  461. }
  462. if (empty($nodes)) {
  463. return FALSE;
  464. }
  465. }
  466. // no tag selected, get them all
  467. else {
  468. $tags = array(
  469. 'a', 'abbr', 'acronym', 'address', 'area', 'b', 'base', 'bdo',
  470. 'big', 'blockquote', 'body', 'br', 'button', 'caption', 'cite',
  471. 'code', 'col', 'colgroup', 'dd', 'del', 'div', 'dfn', 'dl',
  472. 'dt', 'em', 'fieldset', 'form', 'frame', 'frameset', 'h1', 'h2',
  473. 'h3', 'h4', 'h5', 'h6', 'head', 'hr', 'html', 'i', 'iframe',
  474. 'img', 'input', 'ins', 'kbd', 'label', 'legend', 'li', 'link',
  475. 'map', 'meta', 'noframes', 'noscript', 'object', 'ol', 'optgroup',
  476. 'option', 'p', 'param', 'pre', 'q', 'samp', 'script', 'select',
  477. 'small', 'span', 'strong', 'style', 'sub', 'sup', 'table',
  478. 'tbody', 'td', 'textarea', 'tfoot', 'th', 'thead', 'title',
  479. 'tr', 'tt', 'ul', 'var'
  480. );
  481. foreach ($tags as $tag) {
  482. if ($isHtml) {
  483. $elements = self::getElementsByCaseInsensitiveTagName(
  484. $dom, $tag
  485. );
  486. } else {
  487. $elements = $dom->getElementsByTagName($tag);
  488. }
  489. foreach ($elements as $element) {
  490. $nodes[] = $element;
  491. }
  492. }
  493. if (empty($nodes)) {
  494. return FALSE;
  495. }
  496. }
  497. // filter by attributes
  498. if ($options['attributes']) {
  499. foreach ($nodes as $node) {
  500. $invalid = FALSE;
  501. foreach ($options['attributes'] as $name => $value) {
  502. // match by regexp if like "regexp:/foo/i"
  503. if (preg_match('/^regexp\s*:\s*(.*)/i', $value, $matches)) {
  504. if (!preg_match($matches[1], $node->getAttribute($name))) {
  505. $invalid = TRUE;
  506. }
  507. }
  508. // class can match only a part
  509. else if ($name == 'class') {
  510. // split to individual classes
  511. $findClasses = explode(
  512. ' ', preg_replace("/\s+/", " ", $value)
  513. );
  514. $allClasses = explode(
  515. ' ',
  516. preg_replace("/\s+/", " ", $node->getAttribute($name))
  517. );
  518. // make sure each class given is in the actual node
  519. foreach ($findClasses as $findClass) {
  520. if (!in_array($findClass, $allClasses)) {
  521. $invalid = TRUE;
  522. }
  523. }
  524. }
  525. // match by exact string
  526. else {
  527. if ($node->getAttribute($name) != $value) {
  528. $invalid = TRUE;
  529. }
  530. }
  531. }
  532. // if every attribute given matched
  533. if (!$invalid) {
  534. $filtered[] = $node;
  535. }
  536. }
  537. $nodes = $filtered;
  538. $filtered = array();
  539. if (empty($nodes)) {
  540. return FALSE;
  541. }
  542. }
  543. // filter by content
  544. if ($options['content'] !== NULL) {
  545. foreach ($nodes as $node) {
  546. $invalid = FALSE;
  547. // match by regexp if like "regexp:/foo/i"
  548. if (preg_match('/^regexp\s*:\s*(.*)/i', $options['content'], $matches)) {
  549. if (!preg_match($matches[1], self::getNodeText($node))) {
  550. $invalid = TRUE;
  551. }
  552. }
  553. // match empty string
  554. else if ($options['content'] === '') {
  555. if (self::getNodeText($node) !== '') {
  556. $invalid = TRUE;
  557. }
  558. }
  559. // match by exact string
  560. else if (strstr(self::getNodeText($node), $options['content']) === FALSE) {
  561. $invalid = TRUE;
  562. }
  563. if (!$invalid) {
  564. $filtered[] = $node;
  565. }
  566. }
  567. $nodes = $filtered;
  568. $filtered = array();
  569. if (empty($nodes)) {
  570. return FALSE;
  571. }
  572. }
  573. // filter by parent node
  574. if ($options['parent']) {
  575. $parentNodes = self::findNodes($dom, $options['parent'], $isHtml);
  576. $parentNode = isset($parentNodes[0]) ? $parentNodes[0] : NULL;
  577. foreach ($nodes as $node) {
  578. if ($parentNode !== $node->parentNode) {
  579. continue;
  580. }
  581. $filtered[] = $node;
  582. }
  583. $nodes = $filtered;
  584. $filtered = array();
  585. if (empty($nodes)) {
  586. return FALSE;
  587. }
  588. }
  589. // filter by child node
  590. if ($options['child']) {
  591. $childNodes = self::findNodes($dom, $options['child'], $isHtml);
  592. $childNodes = !empty($childNodes) ? $childNodes : array();
  593. foreach ($nodes as $node) {
  594. foreach ($node->childNodes as $child) {
  595. foreach ($childNodes as $childNode) {
  596. if ($childNode === $child) {
  597. $filtered[] = $node;
  598. }
  599. }
  600. }
  601. }
  602. $nodes = $filtered;
  603. $filtered = array();
  604. if (empty($nodes)) {
  605. return FALSE;
  606. }
  607. }
  608. // filter by ancestor
  609. if ($options['ancestor']) {
  610. $ancestorNodes = self::findNodes($dom, $options['ancestor'], $isHtml);
  611. $ancestorNode = isset($ancestorNodes[0]) ? $ancestorNodes[0] : NULL;
  612. foreach ($nodes as $node) {
  613. $parent = $node->parentNode;
  614. while ($parent && $parent->nodeType != XML_HTML_DOCUMENT_NODE) {
  615. if ($parent === $ancestorNode) {
  616. $filtered[] = $node;
  617. }
  618. $parent = $parent->parentNode;
  619. }
  620. }
  621. $nodes = $filtered;
  622. $filtered = array();
  623. if (empty($nodes)) {
  624. return FALSE;
  625. }
  626. }
  627. // filter by descendant
  628. if ($options['descendant']) {
  629. $descendantNodes = self::findNodes($dom, $options['descendant'], $isHtml);
  630. $descendantNodes = !empty($descendantNodes) ? $descendantNodes : array();
  631. foreach ($nodes as $node) {
  632. foreach (self::getDescendants($node) as $descendant) {
  633. foreach ($descendantNodes as $descendantNode) {
  634. if ($descendantNode === $descendant) {
  635. $filtered[] = $node;
  636. }
  637. }
  638. }
  639. }
  640. $nodes = $filtered;
  641. $filtered = array();
  642. if (empty($nodes)) {
  643. return FALSE;
  644. }
  645. }
  646. // filter by children
  647. if ($options['children']) {
  648. $validChild = array('count', 'greater_than', 'less_than', 'only');
  649. $childOptions = self::assertValidKeys(
  650. $options['children'], $validChild
  651. );
  652. foreach ($nodes as $node) {
  653. $childNodes = $node->childNodes;
  654. foreach ($childNodes as $childNode) {
  655. if ($childNode->nodeType !== XML_CDATA_SECTION_NODE &&
  656. $childNode->nodeType !== XML_TEXT_NODE) {
  657. $children[] = $childNode;
  658. }
  659. }
  660. // we must have children to pass this filter
  661. if (!empty($children)) {
  662. // exact count of children
  663. if ($childOptions['count'] !== NULL) {
  664. if (count($children) !== $childOptions['count']) {
  665. break;
  666. }
  667. }
  668. // range count of children
  669. else if ($childOptions['less_than'] !== NULL &&
  670. $childOptions['greater_than'] !== NULL) {
  671. if (count($children) >= $childOptions['less_than'] ||
  672. count($children) <= $childOptions['greater_than']) {
  673. break;
  674. }
  675. }
  676. // less than a given count
  677. else if ($childOptions['less_than'] !== NULL) {
  678. if (count($children) >= $childOptions['less_than']) {
  679. break;
  680. }
  681. }
  682. // more than a given count
  683. else if ($childOptions['greater_than'] !== NULL) {
  684. if (count($children) <= $childOptions['greater_than']) {
  685. break;
  686. }
  687. }
  688. // match each child against a specific tag
  689. if ($childOptions['only']) {
  690. $onlyNodes = self::findNodes(
  691. $dom, $childOptions['only'], $isHtml
  692. );
  693. // try to match each child to one of the 'only' nodes
  694. foreach ($children as $child) {
  695. $matched = FALSE;
  696. foreach ($onlyNodes as $onlyNode) {
  697. if ($onlyNode === $child) {
  698. $matched = TRUE;
  699. }
  700. }
  701. if (!$matched) {
  702. break(2);
  703. }
  704. }
  705. }
  706. $filtered[] = $node;
  707. }
  708. }
  709. $nodes = $filtered;
  710. $filtered = array();
  711. if (empty($nodes)) {
  712. return;
  713. }
  714. }
  715. // return the first node that matches all criteria
  716. return !empty($nodes) ? $nodes : array();
  717. }
  718. /**
  719. * Recursively get flat array of all descendants of this node.
  720. *
  721. * @param DOMNode $node
  722. * @return array
  723. * @since Method available since Release 3.3.0
  724. * @author Mike Naberezny <mike@maintainable.com>
  725. * @author Derek DeVries <derek@maintainable.com>
  726. */
  727. protected static function getDescendants(DOMNode $node)
  728. {
  729. $allChildren = array();
  730. $childNodes = $node->childNodes ? $node->childNodes : array();
  731. foreach ($childNodes as $child) {
  732. if ($child->nodeType === XML_CDATA_SECTION_NODE ||
  733. $child->nodeType === XML_TEXT_NODE) {
  734. continue;
  735. }
  736. $children = self::getDescendants($child);
  737. $allChildren = array_merge($allChildren, $children, array($child));
  738. }
  739. return isset($allChildren) ? $allChildren : array();
  740. }
  741. /**
  742. * Gets elements by case insensitive tagname.
  743. *
  744. * @param DOMDocument $dom
  745. * @param string $tag
  746. * @return DOMNodeList
  747. * @since Method available since Release 3.4.0
  748. */
  749. protected static function getElementsByCaseInsensitiveTagName(DOMDocument $dom, $tag)
  750. {
  751. $elements = $dom->getElementsByTagName(strtolower($tag));
  752. if ($elements->length == 0) {
  753. $elements = $dom->getElementsByTagName(strtoupper($tag));
  754. }
  755. return $elements;
  756. }
  757. /**
  758. * Get the text value of this node's child text node.
  759. *
  760. * @param DOMNode $node
  761. * @return string
  762. * @since Method available since Release 3.3.0
  763. * @author Mike Naberezny <mike@maintainable.com>
  764. * @author Derek DeVries <derek@maintainable.com>
  765. */
  766. protected static function getNodeText(DOMNode $node)
  767. {
  768. if (!$node->childNodes instanceof DOMNodeList) {
  769. return '';
  770. }
  771. $result = '';
  772. foreach ($node->childNodes as $childNode) {
  773. if ($childNode->nodeType === XML_TEXT_NODE ||
  774. $childNode->nodeType === XML_CDATA_SECTION_NODE) {
  775. $result .= trim($childNode->data) . ' ';
  776. } else {
  777. $result .= self::getNodeText($childNode);
  778. }
  779. }
  780. return str_replace(' ', ' ', $result);
  781. }
  782. }