See Release Notes
Long Term Support Release
Differences Between: [Versions 311 and 401] [Versions 400 and 401] [Versions 401 and 402] [Versions 401 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 namespace tool_brickfield\local\htmlchecker\common; 18 19 use tool_brickfield\local\htmlchecker\brickfield_accessibility_report_item; 20 use tool_brickfield\manager; 21 22 /** 23 * This handles importing DOM objects, adding items to the report and provides a few DOM-traversing methods 24 * 25 * @package tool_brickfield 26 * @copyright 2020 onward: Brickfield Education Labs, www.brickfield.ie 27 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 28 */ 29 class brickfield_accessibility_test { 30 /** @var object The DOMDocument object */ 31 public $dom; 32 33 /** @var object The brickfieldCSS object */ 34 public $css; 35 36 /** @var array The path for the request */ 37 public $path; 38 39 /** @var bool Whether the test can be used in a CMS (content without HTML head) */ 40 public $cms = true; 41 42 /** @var string The base path for this request */ 43 public $basepath; 44 45 /** @var array An array of ReportItem objects */ 46 public $report = array(); 47 48 /** @var int The fallback severity level for all tests */ 49 public $defaultseverity = \tool_brickfield\local\htmlchecker\brickfield_accessibility::BA_TEST_SUGGESTION; 50 51 /** @var array An array of all the extensions that are images */ 52 public $imageextensions = array('gif', 'jpg', 'png', 'jpeg', 'tiff', 'svn'); 53 54 /** @var string The language domain */ 55 public $lang = 'en'; 56 57 /** @var array An array of translatable strings */ 58 public $strings = array('en' => ''); 59 60 /** 61 * The class constructor. We pass items by reference so we can alter the DOM if necessary 62 * @param object $dom The DOMDocument object 63 * @param object $css The brickfieldCSS object 64 * @param array $path The path of this request 65 * @param string $languagedomain The langauge domain to user 66 * @param mixed $options Any additional options passed by htmlchecker. 67 */ 68 public function __construct(&$dom, &$css, &$path, $languagedomain = 'en', $options = null) { 69 $this->dom = $dom; 70 $this->css = $css; 71 $this->path = $path; 72 $this->lang = $languagedomain; 73 $this->options = $options; 74 $this->report = array(); 75 $this->check(); 76 } 77 78 /** 79 * Helper method to collect the report from this test. Some 80 * tests do additional cleanup by overriding this method 81 * @return array An array of ReportItem objects 82 */ 83 public function get_report(): array { 84 $this->report['severity'] = $this->defaultseverity; 85 return $this->report; 86 } 87 88 /** 89 * Returns the default severity of the test 90 * @return int The severity level 91 */ 92 public function get_severity(): int { 93 return $this->defaultseverity; 94 } 95 96 /** 97 * Adds a new ReportItem to this current tests collection of reports. 98 * Most reports pertain to a particular element (like an IMG with no Alt attribute); 99 * however, some are document-level and just either pass or don't pass 100 * @param object $element The DOMElement object that pertains to this report 101 * @param string $message An additional message to add to the report 102 * @param bool $pass Whether or not this report passed 103 * @param object $state Extra information about the error state 104 * @param bool $manual Whether the report needs a manual check 105 */ 106 public function add_report($element = null, $message = null, $pass = null, $state = null, $manual = null) { 107 $report = new brickfield_accessibility_report_item(); 108 $report->element = $element; 109 $report->message = $message; 110 $report->pass = $pass; 111 $report->state = $state; 112 $report->manual = $manual; 113 $report->line = $report->get_line(); 114 $this->report[] = $report; 115 } 116 117 /** 118 * Retrieves the full path for a file. 119 * @param string $file The path to a file 120 * @return string The absolute path to the file. 121 */ 122 public function get_path($file): string { 123 if ((substr($file, 0, 7) == 'http://') || (substr($file, 0, 8) == 'https://')) { 124 return $file; 125 } 126 $file = explode('/', $file); 127 if (count($file) == 1) { 128 return implode('/', $this->path) . '/' . $file[0]; 129 } 130 131 $path = $this->path; 132 foreach ($file as $directory) { 133 if ($directory == '..') { 134 array_pop($path); 135 } else { 136 $filepath[] = $directory; 137 } 138 } 139 return implode('/', $path) .'/'. implode('/', $filepath); 140 } 141 142 /** 143 * Returns a translated variable. If the translation is unavailable, English is returned 144 * Because tests only really have one string array, we can get all of this info locally 145 * @return mixed The translation for the object 146 */ 147 public function translation() { 148 if (isset($this->strings[$this->lang])) { 149 return $this->strings[$this->lang]; 150 } 151 if (isset($this->strings['en'])) { 152 return $this->strings['en']; 153 } 154 return false; 155 } 156 157 /** 158 * Helper method to find all the elements that fit a particular query 159 * in the document (either by tag name, or by attributes from the htmlElements object) 160 * @param mixed $tags Either a single tag name in a string, or an array of tag names 161 * @param string $options The kind of option to select an element by (see htmlElements) 162 * @param bool $value The value of the above option 163 * @return array An array of elements that fit the description 164 */ 165 public function get_all_elements($tags = null, string $options = '', bool $value = true): array { 166 if (!is_array($tags)) { 167 $tags = [$tags]; 168 } 169 if ($options !== '') { 170 $temp = new html_elements(); 171 $tags = $temp->get_elements_by_option($options, $value); 172 } 173 $result = []; 174 175 if (!is_array($tags)) { 176 return []; 177 } 178 foreach ($tags as $tag) { 179 $elements = $this->dom->getElementsByTagName($tag); 180 if ($elements) { 181 foreach ($elements as $element) { 182 $result[] = $element; 183 } 184 } 185 } 186 if (count($result) == 0) { 187 return []; 188 } 189 return $result; 190 } 191 192 /** 193 * Returns true if an element has a child with a given tag name 194 * @param object $element A DOMElement object 195 * @param string $childtag The tag name of the child to find 196 * @return bool TRUE if the element does have a child with 197 * the given tag name, otherwise FALSE 198 */ 199 public function element_has_child($element, string $childtag): bool { 200 foreach ($element->childNodes as $child) { 201 if (property_exists($child, 'tagName') && $child->tagName == $childtag) { 202 return true; 203 } 204 } 205 return false; 206 } 207 208 /** 209 * Returns the first ancestor reached of a tag, or false if it hits 210 * the document root or a given tag. 211 * @param object $element A DOMElement object 212 * @param string $ancestortag The name of the tag we are looking for 213 * @param string $limittag Where to stop searching 214 * @return bool 215 */ 216 public function get_element_ancestor($element, string $ancestortag, string $limittag = 'body') { 217 while (property_exists($element, 'parentNode')) { 218 if ($element->parentNode->tagName == $ancestortag) { 219 return $element->parentNode; 220 } 221 if ($element->parentNode->tagName == $limittag) { 222 return false; 223 } 224 $element = $element->parentNode; 225 } 226 return false; 227 } 228 229 /** 230 * Finds all the elements with a given tag name that has 231 * an attribute 232 * @param string $tag The tag name to search for 233 * @param string $attribute The attribute to search on 234 * @param bool $unique Whether we only want one result per attribute 235 * @return array An array of DOMElements with the attribute 236 * value as the key. 237 */ 238 public function get_elements_by_attribute(string $tag, string $attribute, bool $unique = false): array { 239 $results = array(); 240 foreach ($this->get_all_elements($tag) as $element) { 241 if ($element->hasAttribute($attribute)) { 242 if ($unique) { 243 $results[$element->getAttribute($attribute)] = $element; 244 } else { 245 $results[$element->getAttribute($attribute)][] = $element; 246 } 247 } 248 } 249 return $results; 250 } 251 252 /** 253 * Returns the next element after the current one. 254 * @param object $element A DOMElement object 255 * @return mixed FALSE if there is no other element, or a DOMElement object 256 */ 257 public function get_next_element($element) { 258 $parent = $element->parentNode; 259 $next = false; 260 foreach ($parent->childNodes as $child) { 261 if ($next) { 262 return $child; 263 } 264 if ($child->isSameNode($element)) { 265 $next = true; 266 } 267 } 268 return false; 269 } 270 271 /** 272 * To minimize notices, this compares an object's property to the valus 273 * and returns true or false. False will also be returned if the object is 274 * not really an object, or if the property doesn't exist at all 275 * @param object $object The object too look at 276 * @param string $property The name of the property 277 * @param mixed $value The value to check against 278 * @param bool $trim Whether the property value should be trimmed 279 * @param bool $lower Whether the property value should be compared on lower case 280 * 281 * @return bool 282 */ 283 public function property_is_equal($object, string $property, $value, bool $trim = false, bool $lower = false) { 284 if (!is_object($object)) { 285 return false; 286 } 287 if (!property_exists($object, $property)) { 288 return false; 289 } 290 $propertyvalue = $object->$property; 291 if ($trim) { 292 $propertyvalue = trim($propertyvalue); 293 $value = trim($value); 294 } 295 if ($lower) { 296 $propertyvalue = strtolower($propertyvalue); 297 $value = strtolower($value); 298 } 299 return ($propertyvalue == $value); 300 } 301 302 /** 303 * Returns the parent of an elment that has a given tag Name, but 304 * stops the search if it hits the $limiter tag 305 * @param object $element The DOMElement object to search on 306 * @param string $tagname The name of the tag of the parent to find 307 * @param string $limiter The tag name of the element to stop searching on 308 * regardless of the results (like search for a parent "P" tag 309 * of this node but stop if you reach "body") 310 * @return mixed FALSE if no parent is found, or the DOMElement object of the found parent 311 */ 312 public function get_parent($element, string $tagname, string $limiter) { 313 while ($element) { 314 if ($element->tagName == $tagname) { 315 return $element; 316 } 317 if ($element->tagName == $limiter) { 318 return false; 319 } 320 $element = $element->parentNode; 321 } 322 return false; 323 } 324 325 /** 326 * Returns if a GIF files is animated or not http://us.php.net/manual/en/function.imagecreatefromgif.php#88005 327 * @param string $filename 328 * @return int 329 */ 330 public function image_is_animated($filename): int { 331 if (!($fh = @fopen($filename, 'rb'))) { 332 return false; 333 } 334 $count = 0; 335 // An animated gif contains multiple "frames", with each frame having a 336 // header made up of: 337 // * a static 4-byte sequence (\x00\x21\xF9\x04) 338 // * 4 variable bytes 339 // * a static 2-byte sequence (\x00\x2C). 340 341 // We read through the file til we reach the end of the file, or we've found 342 // at least 2 frame headers. 343 while (!feof($fh) && $count < 2) { 344 $chunk = fread($fh, 1024 * 100); // Read 100kb at a time. 345 $count += preg_match_all('#\x00\x21\xF9\x04.{4}\x00\x2C#s', $chunk, $matches); 346 } 347 348 fclose($fh); 349 return $count > 1; 350 } 351 352 /** 353 * Returns if there are any printable/readable characters within an element. 354 * This finds both node values or images with alt text. 355 * @param object $element The given element to look at 356 * @return bool TRUE if contains readable text, FALSE if otherwise 357 */ 358 public function element_contains_readable_text($element): bool { 359 if (is_a($element, 'DOMText')) { 360 if (trim($element->wholeText) != '') { 361 return true; 362 } 363 } else { 364 if (trim($element->nodeValue) != '' || 365 ($element->hasAttribute('alt') && trim($element->getAttribute('alt')) != '')) { 366 return true; 367 } 368 if (method_exists($element, 'hasChildNodes') && $element->hasChildNodes()) { 369 foreach ($element->childNodes as $child) { 370 if ($this->element_contains_readable_text($child)) { 371 return true; 372 } 373 } 374 } 375 } 376 return false; 377 } 378 379 /** 380 * Returns an array of the newwindowphrases for all enabled language packs. 381 * @return array of the newwindowphrases for all enabled language packs. 382 */ 383 public static function get_all_newwindowphrases(): array { 384 // Need to process all enabled lang versions of newwindowphrases. 385 return static::get_all_phrases('newwindowphrases'); 386 } 387 388 /** 389 * Returns an array of the invalidlinkphrases for all enabled language packs. 390 * @return array of the invalidlinkphrases for all enabled language packs. 391 */ 392 public static function get_all_invalidlinkphrases(): array { 393 // Need to process all enabled lang versions of invalidlinkphrases. 394 return static::get_all_phrases('invalidlinkphrases'); 395 } 396 397 /** 398 * Returns an array of the relevant phrases for all enabled language packs. 399 * @param string $stringname the language string identifier you want get the phrases for. 400 * @return array of the invalidlinkphrases for all enabled language packs. 401 */ 402 protected static function get_all_phrases(string $stringname): array { 403 $stringmgr = get_string_manager(); 404 $allstrings = []; 405 406 // Somehow, an invalid string was requested. Add exception handling for this in the future. 407 if (!$stringmgr->string_exists($stringname, manager::PLUGINNAME)) { 408 return $allstrings; 409 } 410 411 // Need to process all enabled lang versions of invalidlinkphrases. 412 $enabledlangs = $stringmgr->get_list_of_translations(); 413 foreach ($enabledlangs as $lang => $value) { 414 $tmpstring = (string)new \lang_string($stringname, manager::PLUGINNAME, null, $lang); 415 $tmplangarray = explode('|', $tmpstring); 416 $allstrings = array_merge($allstrings, $tmplangarray); 417 } 418 // Removing duplicates if a lang is enabled, yet using default 'en' due to no relevant lang file. 419 $allstrings = array_unique($allstrings); 420 return $allstrings; 421 } 422 423 /** 424 * Assesses whether a string contains any readable text, which is text that 425 * contains any characters other than whitespace characters. 426 * 427 * @param string $text 428 * @return bool 429 */ 430 public static function is_text_readable(string $text): bool { 431 // These characters in order are a space, tab, line feed, carriage return, 432 // NUL-byte, vertical tab and non-breaking space unicode character \xc2\xa0. 433 $emptycharacters = " \t\n\r\0\x0B\xc2\xa0"; 434 return trim($text, $emptycharacters) != ''; 435 } 436 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body