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