PageRenderTime 51ms CodeModel.GetById 14ms RepoModel.GetById 0ms app.codeStats 1ms

/vendor/phpunit/phpunit/src/Util/XML.php

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