Differences Between: [Versions 310 and 311] [Versions 39 and 311]
1 <?php 2 // This file is part of Moodle - http://moodle.org/ 3 // 4 // Moodle is free software: you can redistribute it and/or modify 5 // it under the terms of the GNU General Public License as published by 6 // the Free Software Foundation, either version 3 of the License, or 7 // (at your option) any later version. 8 // 9 // Moodle is distributed in the hope that it will be useful, 10 // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 // GNU General Public License for more details. 13 // 14 // You should have received a copy of the GNU General Public License 15 // along with Moodle. If not, see <http://www.gnu.org/licenses/>. 16 17 /** 18 * Base test case class. 19 * 20 * @package core 21 * @category test 22 * @author Tony Levi <tony.levi@blackboard.com> 23 * @copyright 2015 Blackboard (http://www.blackboard.com) 24 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 25 */ 26 27 28 /** 29 * Base class for PHPUnit test cases customised for Moodle 30 * 31 * It is intended for functionality common to both basic and advanced_testcase. 32 * 33 * @package core 34 * @category test 35 * @author Tony Levi <tony.levi@blackboard.com> 36 * @copyright 2015 Blackboard (http://www.blackboard.com) 37 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 38 */ 39 abstract class base_testcase extends PHPUnit\Framework\TestCase { 40 // phpcs:disable 41 // Following code is legacy code from phpunit to support assertTag 42 // and assertNotTag. 43 44 /** 45 * Note: we are overriding this method to remove the deprecated error 46 * @see https://tracker.moodle.org/browse/MDL-47129 47 * 48 * @param array $matcher 49 * @param string $actual 50 * @param string $message 51 * @param boolean $ishtml 52 * 53 * @deprecated 3.0 54 */ 55 public static function assertTag($matcher, $actual, $message = '', $ishtml = true) { 56 $dom = (new PHPUnit\Util\Xml\Loader)->load($actual, $ishtml); 57 $tags = self::findNodes($dom, $matcher, $ishtml); 58 $matched = (is_array($tags) && count($tags) > 0) && $tags[0] instanceof DOMNode; 59 self::assertTrue($matched, $message); 60 } 61 62 /** 63 * Note: we are overriding this method to remove the deprecated error 64 * @see https://tracker.moodle.org/browse/MDL-47129 65 * 66 * @param array $matcher 67 * @param string $actual 68 * @param string $message 69 * @param boolean $ishtml 70 * 71 * @deprecated 3.0 72 */ 73 public static function assertNotTag($matcher, $actual, $message = '', $ishtml = true) { 74 $dom = (new PHPUnit\Util\Xml\Loader)->load($actual, $ishtml); 75 $tags = self::findNodes($dom, $matcher, $ishtml); 76 $matched = (is_array($tags) && count($tags) > 0) && $tags[0] instanceof DOMNode; 77 self::assertFalse($matched, $message); 78 } 79 80 /** 81 * Validate list of keys in the associative array. 82 * 83 * @param array $hash 84 * @param array $validKeys 85 * 86 * @return array 87 * 88 * @throws PHPUnit\Framework\Exception 89 */ 90 public static function assertValidKeys(array $hash, array $validKeys) { 91 $valids = array(); 92 93 // Normalize validation keys so that we can use both indexed and 94 // associative arrays. 95 foreach ($validKeys as $key => $val) { 96 is_int($key) ? $valids[$val] = null : $valids[$key] = $val; 97 } 98 99 $validKeys = array_keys($valids); 100 101 // Check for invalid keys. 102 foreach ($hash as $key => $value) { 103 if (!in_array($key, $validKeys)) { 104 $unknown[] = $key; 105 } 106 } 107 108 if (!empty($unknown)) { 109 throw new PHPUnit\Framework\Exception( 110 'Unknown key(s): ' . implode(', ', $unknown) 111 ); 112 } 113 114 // Add default values for any valid keys that are empty. 115 foreach ($valids as $key => $value) { 116 if (!isset($hash[$key])) { 117 $hash[$key] = $value; 118 } 119 } 120 121 return $hash; 122 } 123 124 /** 125 * Parse out the options from the tag using DOM object tree. 126 * 127 * @param DOMDocument $dom 128 * @param array $options 129 * @param bool $isHtml 130 * 131 * @return array 132 */ 133 public static function findNodes(DOMDocument $dom, array $options, $isHtml = true) { 134 $valid = array( 135 'id', 'class', 'tag', 'content', 'attributes', 'parent', 136 'child', 'ancestor', 'descendant', 'children', 'adjacent-sibling' 137 ); 138 139 $filtered = array(); 140 $options = self::assertValidKeys($options, $valid); 141 142 // find the element by id 143 if ($options['id']) { 144 $options['attributes']['id'] = $options['id']; 145 } 146 147 if ($options['class']) { 148 $options['attributes']['class'] = $options['class']; 149 } 150 151 $nodes = array(); 152 153 // find the element by a tag type 154 if ($options['tag']) { 155 if ($isHtml) { 156 $elements = self::getElementsByCaseInsensitiveTagName( 157 $dom, 158 $options['tag'] 159 ); 160 } else { 161 $elements = $dom->getElementsByTagName($options['tag']); 162 } 163 164 foreach ($elements as $element) { 165 $nodes[] = $element; 166 } 167 168 if (empty($nodes)) { 169 return false; 170 } 171 } // no tag selected, get them all 172 else { 173 $tags = array( 174 'a', 'abbr', 'acronym', 'address', 'area', 'b', 'base', 'bdo', 175 'big', 'blockquote', 'body', 'br', 'button', 'caption', 'cite', 176 'code', 'col', 'colgroup', 'dd', 'del', 'div', 'dfn', 'dl', 177 'dt', 'em', 'fieldset', 'form', 'frame', 'frameset', 'h1', 'h2', 178 'h3', 'h4', 'h5', 'h6', 'head', 'hr', 'html', 'i', 'iframe', 179 'img', 'input', 'ins', 'kbd', 'label', 'legend', 'li', 'link', 180 'map', 'meta', 'noframes', 'noscript', 'object', 'ol', 'optgroup', 181 'option', 'p', 'param', 'pre', 'q', 'samp', 'script', 'select', 182 'small', 'span', 'strong', 'style', 'sub', 'sup', 'table', 183 'tbody', 'td', 'textarea', 'tfoot', 'th', 'thead', 'title', 184 'tr', 'tt', 'ul', 'var', 185 // HTML5 186 'article', 'aside', 'audio', 'bdi', 'canvas', 'command', 187 'datalist', 'details', 'dialog', 'embed', 'figure', 'figcaption', 188 'footer', 'header', 'hgroup', 'keygen', 'mark', 'meter', 'nav', 189 'output', 'progress', 'ruby', 'rt', 'rp', 'track', 'section', 190 'source', 'summary', 'time', 'video', 'wbr' 191 ); 192 193 foreach ($tags as $tag) { 194 if ($isHtml) { 195 $elements = self::getElementsByCaseInsensitiveTagName( 196 $dom, 197 $tag 198 ); 199 } else { 200 $elements = $dom->getElementsByTagName($tag); 201 } 202 203 foreach ($elements as $element) { 204 $nodes[] = $element; 205 } 206 } 207 208 if (empty($nodes)) { 209 return false; 210 } 211 } 212 213 // filter by attributes 214 if ($options['attributes']) { 215 foreach ($nodes as $node) { 216 $invalid = false; 217 218 foreach ($options['attributes'] as $name => $value) { 219 // match by regexp if like "regexp:/foo/i" 220 if (preg_match('/^regexp\s*:\s*(.*)/i', $value, $matches)) { 221 if (!preg_match($matches[1], $node->getAttribute($name))) { 222 $invalid = true; 223 } 224 } // class can match only a part 225 elseif ($name == 'class') { 226 // split to individual classes 227 $findClasses = explode( 228 ' ', 229 preg_replace("/\s+/", ' ', $value) 230 ); 231 232 $allClasses = explode( 233 ' ', 234 preg_replace("/\s+/", ' ', $node->getAttribute($name)) 235 ); 236 237 // make sure each class given is in the actual node 238 foreach ($findClasses as $findClass) { 239 if (!in_array($findClass, $allClasses)) { 240 $invalid = true; 241 } 242 } 243 } // match by exact string 244 else { 245 if ($node->getAttribute($name) !== (string) $value) { 246 $invalid = true; 247 } 248 } 249 } 250 251 // if every attribute given matched 252 if (!$invalid) { 253 $filtered[] = $node; 254 } 255 } 256 257 $nodes = $filtered; 258 $filtered = array(); 259 260 if (empty($nodes)) { 261 return false; 262 } 263 } 264 265 // filter by content 266 if ($options['content'] !== null) { 267 foreach ($nodes as $node) { 268 $invalid = false; 269 270 // match by regexp if like "regexp:/foo/i" 271 if (preg_match('/^regexp\s*:\s*(.*)/i', $options['content'], $matches)) { 272 if (!preg_match($matches[1], self::getNodeText($node))) { 273 $invalid = true; 274 } 275 } // match empty string 276 elseif ($options['content'] === '') { 277 if (self::getNodeText($node) !== '') { 278 $invalid = true; 279 } 280 } // match by exact string 281 elseif (strstr(self::getNodeText($node), $options['content']) === false) { 282 $invalid = true; 283 } 284 285 if (!$invalid) { 286 $filtered[] = $node; 287 } 288 } 289 290 $nodes = $filtered; 291 $filtered = array(); 292 293 if (empty($nodes)) { 294 return false; 295 } 296 } 297 298 // filter by parent node 299 if ($options['parent']) { 300 $parentNodes = self::findNodes($dom, $options['parent'], $isHtml); 301 $parentNode = isset($parentNodes[0]) ? $parentNodes[0] : null; 302 303 foreach ($nodes as $node) { 304 if ($parentNode !== $node->parentNode) { 305 continue; 306 } 307 308 $filtered[] = $node; 309 } 310 311 $nodes = $filtered; 312 $filtered = array(); 313 314 if (empty($nodes)) { 315 return false; 316 } 317 } 318 319 // filter by child node 320 if ($options['child']) { 321 $childNodes = self::findNodes($dom, $options['child'], $isHtml); 322 $childNodes = !empty($childNodes) ? $childNodes : array(); 323 324 foreach ($nodes as $node) { 325 foreach ($node->childNodes as $child) { 326 foreach ($childNodes as $childNode) { 327 if ($childNode === $child) { 328 $filtered[] = $node; 329 } 330 } 331 } 332 } 333 334 $nodes = $filtered; 335 $filtered = array(); 336 337 if (empty($nodes)) { 338 return false; 339 } 340 } 341 342 // filter by adjacent-sibling 343 if ($options['adjacent-sibling']) { 344 $adjacentSiblingNodes = self::findNodes($dom, $options['adjacent-sibling'], $isHtml); 345 $adjacentSiblingNodes = !empty($adjacentSiblingNodes) ? $adjacentSiblingNodes : array(); 346 347 foreach ($nodes as $node) { 348 $sibling = $node; 349 350 while ($sibling = $sibling->nextSibling) { 351 if ($sibling->nodeType !== XML_ELEMENT_NODE) { 352 continue; 353 } 354 355 foreach ($adjacentSiblingNodes as $adjacentSiblingNode) { 356 if ($sibling === $adjacentSiblingNode) { 357 $filtered[] = $node; 358 break; 359 } 360 } 361 362 break; 363 } 364 } 365 366 $nodes = $filtered; 367 $filtered = array(); 368 369 if (empty($nodes)) { 370 return false; 371 } 372 } 373 374 // filter by ancestor 375 if ($options['ancestor']) { 376 $ancestorNodes = self::findNodes($dom, $options['ancestor'], $isHtml); 377 $ancestorNode = isset($ancestorNodes[0]) ? $ancestorNodes[0] : null; 378 379 foreach ($nodes as $node) { 380 $parent = $node->parentNode; 381 382 while ($parent && $parent->nodeType != XML_HTML_DOCUMENT_NODE) { 383 if ($parent === $ancestorNode) { 384 $filtered[] = $node; 385 } 386 387 $parent = $parent->parentNode; 388 } 389 } 390 391 $nodes = $filtered; 392 $filtered = array(); 393 394 if (empty($nodes)) { 395 return false; 396 } 397 } 398 399 // filter by descendant 400 if ($options['descendant']) { 401 $descendantNodes = self::findNodes($dom, $options['descendant'], $isHtml); 402 $descendantNodes = !empty($descendantNodes) ? $descendantNodes : array(); 403 404 foreach ($nodes as $node) { 405 foreach (self::getDescendants($node) as $descendant) { 406 foreach ($descendantNodes as $descendantNode) { 407 if ($descendantNode === $descendant) { 408 $filtered[] = $node; 409 } 410 } 411 } 412 } 413 414 $nodes = $filtered; 415 $filtered = array(); 416 417 if (empty($nodes)) { 418 return false; 419 } 420 } 421 422 // filter by children 423 if ($options['children']) { 424 $validChild = array('count', 'greater_than', 'less_than', 'only'); 425 $childOptions = self::assertValidKeys( 426 $options['children'], 427 $validChild 428 ); 429 430 foreach ($nodes as $node) { 431 $childNodes = $node->childNodes; 432 433 foreach ($childNodes as $childNode) { 434 if ($childNode->nodeType !== XML_CDATA_SECTION_NODE && 435 $childNode->nodeType !== XML_TEXT_NODE) { 436 $children[] = $childNode; 437 } 438 } 439 440 // we must have children to pass this filter 441 if (!empty($children)) { 442 // exact count of children 443 if ($childOptions['count'] !== null) { 444 if (count($children) !== $childOptions['count']) { 445 break; 446 } 447 } // range count of children 448 elseif ($childOptions['less_than'] !== null && 449 $childOptions['greater_than'] !== null) { 450 if (count($children) >= $childOptions['less_than'] || 451 count($children) <= $childOptions['greater_than']) { 452 break; 453 } 454 } // less than a given count 455 elseif ($childOptions['less_than'] !== null) { 456 if (count($children) >= $childOptions['less_than']) { 457 break; 458 } 459 } // more than a given count 460 elseif ($childOptions['greater_than'] !== null) { 461 if (count($children) <= $childOptions['greater_than']) { 462 break; 463 } 464 } 465 466 // match each child against a specific tag 467 if ($childOptions['only']) { 468 $onlyNodes = self::findNodes( 469 $dom, 470 $childOptions['only'], 471 $isHtml 472 ); 473 474 // try to match each child to one of the 'only' nodes 475 foreach ($children as $child) { 476 $matched = false; 477 478 foreach ($onlyNodes as $onlyNode) { 479 if ($onlyNode === $child) { 480 $matched = true; 481 } 482 } 483 484 if (!$matched) { 485 break 2; 486 } 487 } 488 } 489 490 $filtered[] = $node; 491 } 492 } 493 494 $nodes = $filtered; 495 496 if (empty($nodes)) { 497 return; 498 } 499 } 500 501 // return the first node that matches all criteria 502 return !empty($nodes) ? $nodes : array(); 503 } 504 505 /** 506 * Recursively get flat array of all descendants of this node. 507 * 508 * @param DOMNode $node 509 * 510 * @return array 511 */ 512 protected static function getDescendants(DOMNode $node) { 513 $allChildren = array(); 514 $childNodes = $node->childNodes ? $node->childNodes : array(); 515 516 foreach ($childNodes as $child) { 517 if ($child->nodeType === XML_CDATA_SECTION_NODE || 518 $child->nodeType === XML_TEXT_NODE) { 519 continue; 520 } 521 522 $children = self::getDescendants($child); 523 $allChildren = array_merge($allChildren, $children, array($child)); 524 } 525 526 return isset($allChildren) ? $allChildren : array(); 527 } 528 529 /** 530 * Gets elements by case insensitive tagname. 531 * 532 * @param DOMDocument $dom 533 * @param string $tag 534 * 535 * @return DOMNodeList 536 */ 537 protected static function getElementsByCaseInsensitiveTagName(DOMDocument $dom, $tag) { 538 $elements = $dom->getElementsByTagName(strtolower($tag)); 539 540 if ($elements->length == 0) { 541 $elements = $dom->getElementsByTagName(strtoupper($tag)); 542 } 543 544 return $elements; 545 } 546 547 /** 548 * Get the text value of this node's child text node. 549 * 550 * @param DOMNode $node 551 * 552 * @return string 553 */ 554 protected static function getNodeText(DOMNode $node) { 555 if (!$node->childNodes instanceof DOMNodeList) { 556 return ''; 557 } 558 559 $result = ''; 560 561 foreach ($node->childNodes as $childNode) { 562 if ($childNode->nodeType === XML_TEXT_NODE || 563 $childNode->nodeType === XML_CDATA_SECTION_NODE) { 564 $result .= trim($childNode->data) . ' '; 565 } else { 566 $result .= self::getNodeText($childNode); 567 } 568 } 569 570 return str_replace(' ', ' ', $result); 571 } 572 // phpcs:enable 573 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body