PageRenderTime 56ms CodeModel.GetById 21ms RepoModel.GetById 0ms app.codeStats 0ms

/vendor/phpunit/phpunit/PHPUnit/Util/XML.php

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