See Release Notes
Long Term Support Release
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 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body