Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 4.3.x will end 7 October 2024 (12 months).
  • Bug fixes for security issues in 4.3.x will end 21 April 2025 (18 months).
  • PHP version: minimum PHP 8.0.0 Note: minimum PHP version has increased since Moodle 4.1. PHP 8.2.x is supported too.

Differences Between: [Versions 310 and 403] [Versions 39 and 403]

   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  }