Search moodle.org's
Developer Documentation

See Release Notes
Long Term Support Release

  • Bug fixes for general core bugs in 4.1.x will end 13 November 2023 (12 months).
  • Bug fixes for security issues in 4.1.x will end 10 November 2025 (36 months).
  • PHP version: minimum PHP 7.4.0 Note: minimum PHP version has increased since Moodle 4.0. PHP 8.0.x is supported too.
   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  namespace tool_brickfield\local\htmlchecker;
  18  
  19  use DOMDocument;
  20  use tool_brickfield\local\htmlchecker\common\brickfield_accessibility_css;
  21  
  22  /**
  23   * Brickfield accessibility HTML checker library.
  24   *
  25   * The main interface class for brickfield_accessibility.
  26   *
  27   * @package    tool_brickfield
  28   * @copyright  2020 onward: Brickfield Education Labs, www.brickfield.ie
  29   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  30   */
  31  class brickfield_accessibility {
  32  
  33      /** @var int Failure level severe. */
  34      const BA_TEST_SEVERE = 1;
  35  
  36      /** @var int Failure level moderate. */
  37      const BA_TEST_MODERATE = 2;
  38  
  39      /** @var int Failure level seggestion. */
  40      const BA_TEST_SUGGESTION = 3;
  41  
  42      /** @var string Tag identifier to enclose all error HTML fragments in. */
  43      const BA_ERROR_TAG = 'bferror';
  44  
  45      /** @var object The central DOMDocument object */
  46      public $dom;
  47  
  48      /** @var string The type of request this is (either 'string', 'file', or 'uri' */
  49      public $type;
  50  
  51      /** @var string The value of the request. Either HTML, a URI, or the path to a file */
  52      public $value;
  53  
  54      /** @var string The base URI of the current request (used to rebuild page if necessary) */
  55      public $uri = '';
  56  
  57      /** @var string The translation domain of the current library */
  58      public $domain;
  59  
  60      /** @var string The name of the guideline */
  61      public $guidelinename = 'wcag';
  62  
  63      /** @var string The name of the reporter to use */
  64      public $reportername = 'static';
  65  
  66      /** @var object A reporting object */
  67      public $reporter;
  68  
  69      /** @var object The central guideline object */
  70      public $guideline;
  71  
  72      /** @var string The base URL for any request of type URI */
  73      public $baseurl;
  74  
  75      /** @var array An array of the current file or URI path */
  76      public $path = [];
  77  
  78      /** @var array An array of additional CSS files to load (useful for CMS content) */
  79      public $cssfiles = [];
  80  
  81      /** @var object The brickfieldCSS object */
  82      public $css;
  83  
  84      /** @var array An array of additional options */
  85      public $options = [
  86              'cms_mode'      => false,
  87              'start_element' => 0,
  88              'end_element'   => 0,
  89              'cms_template'  => []
  90          ];
  91  
  92      /** @var bool An indicator if the DOMDocument loaded. If not, this means that the
  93       * HTML given to it was so munged it wouldn't even load.
  94       */
  95      public $isvalid = true;
  96  
  97      /**
  98       * The class constructor
  99       * @param string $value Either the HTML string to check or the file/uri of the request
 100       * @param string $guideline The name of the guideline
 101       * @param string $type The type of the request (either file, uri, or string)
 102       * @param string $reporter The name of the reporter to use
 103       * @param string $domain The domain of the translation language to use
 104       */
 105      public function __construct(string $value = '', string $guideline = 'wcag2aaa', string $type = 'string',
 106                                  string $reporter = 'static', string $domain = 'en') {
 107          $this->dom = new DOMDocument();
 108          $this->type = $type;
 109          if ($type == 'uri' || $type == 'file') {
 110              $this->uri = $value;
 111          }
 112          $this->domain = $domain;
 113          $this->guidelinename = $guideline;
 114          $this->reportername = $reporter;
 115          $this->value = $value;
 116      }
 117  
 118      /**
 119       * Prepares the DOMDocument object for brickfield_accessibility. It loads based on the file type
 120       * declaration and first scrubs the value using prepareValue().
 121       */
 122      public function prepare_dom() {
 123          $this->prepare_value();
 124          $this->isvalid = @$this->dom->loadHTML('<?xml encoding="utf-8" ?>' . $this->value);
 125          $this->prepare_base_url($this->value, $this->type);
 126      }
 127  
 128      /**
 129       * If the CMS mode options are set, then we remove some items front the
 130       * HTML value before sending it back.
 131       */
 132      public function prepare_value() {
 133          // We ignore the 'string' type because it would mean the value already contains HTML.
 134          if ($this->type == 'file' || $this->type == 'uri') {
 135              $this->value = @file_get_contents($this->value);
 136          }
 137  
 138          // If there are no surrounding tags, add self::BA_ERROR_TAG to prevent the DOM from adding a <p> tag.
 139          if (strpos(trim($this->value), '<') !== 0) {
 140              $this->value = '<' . self::BA_ERROR_TAG . '>' . $this->value . '</' . self::BA_ERROR_TAG . '>';
 141          }
 142      }
 143  
 144      /**
 145       * Set global predefined options for brickfield_accessibility. First we check that the
 146       * array key has been defined.
 147       * @param mixed $variable Either an array of values, or a variable name of the option
 148       * @param mixed $value If this is a single option, the value of the option
 149       */
 150      public function set_option($variable, $value = null) {
 151          if (!is_array($variable)) {
 152              $variable = [$variable => $value];
 153          }
 154          foreach ($variable as $k => $value) {
 155              if (isset($this->options[$k])) {
 156                  $this->options[$k] = $value;
 157              }
 158          }
 159      }
 160  
 161      /**
 162       * Returns an absolute path from a relative one.
 163       * @param string $absolute The absolute URL
 164       * @param string $relative The relative path
 165       * @return string A new path
 166       */
 167      public function get_absolute_path(string $absolute, string $relative): string {
 168          if (substr($relative, 0, 2) == '//') {
 169              if ($this->uri) {
 170                  $current = parse_url($this->uri);
 171              } else {
 172                  $current = ['scheme' => 'http'];
 173              }
 174              return $current['scheme'] .':'. $relative;
 175          }
 176  
 177          $relativeurl = parse_url($relative);
 178  
 179          if (isset($relativeurl['scheme'])) {
 180              return $relative;
 181          }
 182  
 183          $absoluteurl = parse_url($absolute);
 184  
 185          if (isset($absoluteurl['path'])) {
 186              $path = dirname($absoluteurl['path']);
 187          }
 188  
 189          if ($relative[0] == '/') {
 190              $cparts = array_filter(explode('/', $relative));
 191          } else {
 192              $aparts = array_filter(explode('/', $path));
 193              $rparts = array_filter(explode('/', $relative));
 194              $cparts = array_merge($aparts, $rparts);
 195  
 196              foreach ($cparts as $i => $part) {
 197                  if ($part == '.') {
 198                      $cparts[$i] = null;
 199                  }
 200  
 201                  if ($part == '..') {
 202                      $cparts[$i - 1] = null;
 203                      $cparts[$i] = null;
 204                  }
 205              }
 206  
 207              $cparts = array_filter($cparts);
 208          }
 209  
 210          $path = implode('/', $cparts);
 211          $url  = "";
 212  
 213          if (isset($absoluteurl['scheme'])) {
 214              $url = $absoluteurl['scheme'] .'://';
 215          }
 216  
 217          if (isset($absoluteurl['user'])) {
 218              $url .= $absoluteurl['user'];
 219  
 220              if ($absoluteurl['pass']) {
 221                  $url .= ':'. $absoluteurl['user'];
 222              }
 223  
 224              $url .= '@';
 225          }
 226  
 227          if (isset($absoluteurl['host'])) {
 228              $url .= $absoluteurl['host'];
 229  
 230              if (isset($absoluteurl['port'])) {
 231                  $url .= ':'. $absoluteurl['port'];
 232              }
 233  
 234              $url .= '/';
 235          }
 236  
 237          $url .= $path;
 238  
 239          return $url;
 240      }
 241  
 242      /**
 243       * Sets the URI if this is for a string or to change where
 244       * Will look for resources like CSS files
 245       * @param string $uri The URI to set
 246       */
 247      public function set_uri(string $uri) {
 248          if (parse_url($uri)) {
 249              $this->uri = $uri;
 250          }
 251      }
 252  
 253      /**
 254       * Formats the base URL for either a file or uri request. We are essentially
 255       * formatting a base url for future reporters to use to find CSS files or
 256       * for tests that use external resources (images, objects, etc) to run tests on them.
 257       * @param string $value The path value
 258       * @param string $type The type of request
 259       */
 260      public function prepare_base_url(string $value, string $type) {
 261          if ($type == 'file') {
 262              $path = explode('/', $this->uri);
 263              array_pop($path);
 264              $this->path = $path;
 265          } else if ($type == 'uri' || $this->uri) {
 266              $parts = explode('://', $this->uri);
 267              $this->path[] = $parts[0] .':/';
 268  
 269              if (is_array($parts[1])) {
 270                  foreach (explode('/', $this->get_base_from_file($parts[1])) as $part) {
 271                      $this->path[] = $part;
 272                  }
 273              } else {
 274                  $this->path[] = $parts[1] .'/';
 275              }
 276          }
 277      }
 278  
 279      /**
 280       * Retrieves the absolute path to a file
 281       * @param string $file The path to a file
 282       * @return string The absolute path to a file
 283       */
 284      public function get_base_from_file(string $file): string {
 285           $find = '/';
 286           $afterfind = substr(strrchr($file, $find), 1);
 287           $strlenstr = strlen($afterfind);
 288           $result = substr($file, 0, -$strlenstr);
 289  
 290           return $result;
 291      }
 292  
 293      /**
 294       * Helper method to add an additional CSS file
 295       * @param string $css The URI or file path to a CSS file
 296       */
 297      public function add_css(string $css) {
 298          if (is_array($css)) {
 299              $this->cssfiles = $css;
 300          } else {
 301              $this->cssfiles[] = $css;
 302          }
 303      }
 304  
 305      /**
 306       * Retrives a single error from the current reporter
 307       * @param string $error The error key
 308       * @return object A ReportItem object
 309       */
 310      public function get_error(string $error) {
 311          return $this->reporter->get_error($error);
 312      }
 313  
 314      /**
 315       * A local method to load the required file for a reporter and set it for the current object
 316       * @param array $options An array of options for the reporter
 317       */
 318      public function load_reporter(array $options = []) {
 319          $classname = '\\tool_brickfield\\local\\htmlchecker\\reporters\\'.'report_'.$this->reportername;
 320  
 321          $this->reporter = new $classname($this->dom, $this->css, $this->guideline, $this->path);
 322  
 323          if (count($options)) {
 324              $this->reporter->set_options($options);
 325          }
 326      }
 327  
 328      /**
 329       * Checks that the DOM object is valid or not
 330       * @return bool Whether the DOMDocument is valid
 331       */
 332      public function is_valid(): bool {
 333          return $this->isvalid;
 334      }
 335  
 336      /**
 337       * Starts running automated checks. Loads the CSS file parser
 338       * and the guideline object.
 339       * @param null $options
 340       * @return bool
 341       */
 342      public function run_check($options = null) {
 343          $this->prepare_dom();
 344  
 345          if (!$this->is_valid()) {
 346              return false;
 347          }
 348  
 349          $this->get_css_object();
 350          $classname = 'tool_brickfield\\local\\htmlchecker\\guidelines\\'.strtolower($this->guidelinename).'_guideline';
 351  
 352          $this->guideline = new $classname($this->dom, $this->css, $this->path, $options, $this->domain, $this->options['cms_mode']);
 353      }
 354  
 355      /**
 356       * Loads the brickfield_accessibility_css object
 357       */
 358      public function get_css_object() {
 359          $this->css = new brickfield_accessibility_css($this->dom, $this->uri, $this->type, $this->path, false, $this->cssfiles);
 360      }
 361  
 362      /**
 363       * Returns a formatted report from the current reporter.
 364       * @param array $options An array of all the options
 365       * @return mixed See the documentation on your reporter's getReport method.
 366       */
 367      public function get_report(array $options = []) {
 368          if (!$this->reporter) {
 369              $this->load_reporter($options);
 370          }
 371          if ($options) {
 372              $this->reporter->set_options($options);
 373          }
 374          $report = $this->reporter->get_report();
 375          $path = $this->path;
 376          return ['report' => $report, 'path' => $path];
 377      }
 378  
 379      /**
 380       * Runs one test on the current DOMDocument
 381       * @param string $test The name of the test to run
 382       * @return bool|array The ReportItem returned from the test
 383       */
 384      public function get_test(string $test) {
 385          $test = 'tool_brickfield\local\htmlchecker\common\checks\\' . $test;
 386  
 387          if (!class_exists($test)) {
 388              return false;
 389          }
 390  
 391          $testclass = new $test($this->dom, $this->css, $this->path);
 392  
 393          return $testclass->report;
 394      }
 395  
 396      /**
 397       * Retrieves the default severity of a test
 398       * @param string $test The name of the test to run
 399       * @return object The severity level of the test
 400       */
 401      public function get_test_severity(string $test) {
 402          $testclass = new $test($this->dom, $this->css, $this->path);
 403  
 404          return $testclass->get_severity();
 405      }
 406  
 407      /**
 408       * A general cleanup function which just does some memory
 409       * cleanup by unsetting the particularly large local vars.
 410       */
 411      public function cleanup() {
 412          unset($this->dom);
 413          unset($this->css);
 414          unset($this->guideline);
 415          unset($this->reporter);
 416      }
 417  
 418      /**
 419       * Determines if the link text is the same as the link URL, without necessarily being an exact match.
 420       * For example, 'www.google.com' matches 'https://www.google.com'.
 421       * @param string $text
 422       * @param string $href
 423       * @return bool
 424       */
 425      public static function match_urls(string $text, string $href): bool {
 426          $parsetext = parse_url($text);
 427          $parsehref = parse_url($href);
 428          $parsetextfull = (isset($parsetext['host'])) ? $parsetext['host'] : '';
 429          $parsetextfull .= (isset($parsetext['path'])) ? $parsetext['path'] : '';
 430          $parsehreffull = (isset($parsehref['host'])) ? $parsehref['host'] : '';
 431          $parsehreffull .= (isset($parsehref['path'])) ? $parsehref['path'] : '';
 432  
 433          // Remove any last '/' character before comparing.
 434          return (rtrim($parsetextfull, '/') === rtrim($parsehreffull, '/'));
 435      }
 436  }