tag. + if (strpos(trim($this->value), '<') !== 0) { + $this->value = '<' . self::BA_ERROR_TAG . '>' . $this->value . '' . self::BA_ERROR_TAG . '>'; + } + } + + /** + * Set global predefined options for brickfield_accessibility. First we check that the + * array key has been defined. + * @param mixed $variable Either an array of values, or a variable name of the option + * @param mixed $value If this is a single option, the value of the option + */ + public function set_option($variable, $value = null) { + if (!is_array($variable)) { + $variable = [$variable => $value]; + } + foreach ($variable as $k => $value) { + if (isset($this->options[$k])) { + $this->options[$k] = $value; + } + } + } + + /** + * Returns an absolute path from a relative one. + * @param string $absolute The absolute URL + * @param string $relative The relative path + * @return string A new path + */ + public function get_absolute_path(string $absolute, string $relative): string { + if (substr($relative, 0, 2) == '//') { + if ($this->uri) { + $current = parse_url($this->uri); + } else { + $current = ['scheme' => 'http']; + } + return $current['scheme'] .':'. $relative; + } + + $relativeurl = parse_url($relative); + + if (isset($relativeurl['scheme'])) { + return $relative; + } + + $absoluteurl = parse_url($absolute); + + if (isset($absoluteurl['path'])) { + $path = dirname($absoluteurl['path']); + } + + if ($relative[0] == '/') { + $cparts = array_filter(explode('/', $relative)); + } else { + $aparts = array_filter(explode('/', $path)); + $rparts = array_filter(explode('/', $relative)); + $cparts = array_merge($aparts, $rparts); + + foreach ($cparts as $i => $part) { + if ($part == '.') { + $cparts[$i] = null; + } + + if ($part == '..') { + $cparts[$i - 1] = null; + $cparts[$i] = null; + } + } + + $cparts = array_filter($cparts); + } + + $path = implode('/', $cparts); + $url = ""; + + if (isset($absoluteurl['scheme'])) { + $url = $absoluteurl['scheme'] .'://'; + } + + if (isset($absoluteurl['user'])) { + $url .= $absoluteurl['user']; + + if ($absoluteurl['pass']) { + $url .= ':'. $absoluteurl['user']; + } + + $url .= '@'; + } + + if (isset($absoluteurl['host'])) { + $url .= $absoluteurl['host']; + + if (isset($absoluteurl['port'])) { + $url .= ':'. $absoluteurl['port']; + } + + $url .= '/'; + } + + $url .= $path; + + return $url; + } + + /** + * Sets the URI if this is for a string or to change where + * Will look for resources like CSS files + * @param string $uri The URI to set + */ + public function set_uri(string $uri) { + if (parse_url($uri)) { + $this->uri = $uri; + } + } + + /** + * Formats the base URL for either a file or uri request. We are essentially + * formatting a base url for future reporters to use to find CSS files or + * for tests that use external resources (images, objects, etc) to run tests on them. + * @param string $value The path value + * @param string $type The type of request + */ + public function prepare_base_url(string $value, string $type) { + if ($type == 'file') { + $path = explode('/', $this->uri); + array_pop($path); + $this->path = $path; + } else if ($type == 'uri' || $this->uri) { + $parts = explode('://', $this->uri); + $this->path[] = $parts[0] .':/'; + + if (is_array($parts[1])) { + foreach (explode('/', $this->get_base_from_file($parts[1])) as $part) { + $this->path[] = $part; + } + } else { + $this->path[] = $parts[1] .'/'; + } + } + } + + /** + * Retrieves the absolute path to a file + * @param string $file The path to a file + * @return string The absolute path to a file + */ + public function get_base_from_file(string $file): string { + $find = '/'; + $afterfind = substr(strrchr($file, $find), 1); + $strlenstr = strlen($afterfind); + $result = substr($file, 0, -$strlenstr); + + return $result; + } + + /** + * Helper method to add an additional CSS file + * @param string $css The URI or file path to a CSS file + */ + public function add_css(string $css) { + if (is_array($css)) { + $this->cssfiles = $css; + } else { + $this->cssfiles[] = $css; + } + } + + /** + * Retrives a single error from the current reporter + * @param string $error The error key + * @return object A ReportItem object + */ + public function get_error(string $error) { + return $this->reporter->get_error($error); + } + + /** + * A local method to load the required file for a reporter and set it for the current object + * @param array $options An array of options for the reporter + */ + public function load_reporter(array $options = []) { + $classname = '\\tool_brickfield\\local\\htmlchecker\\reporters\\'.'report_'.$this->reportername; + + $this->reporter = new $classname($this->dom, $this->css, $this->guideline, $this->path); + + if (count($options)) { + $this->reporter->set_options($options); + } + } + + /** + * Checks that the DOM object is valid or not + * @return bool Whether the DOMDocument is valid + */ + public function is_valid(): bool { + return $this->isvalid; + } + + /** + * Starts running automated checks. Loads the CSS file parser + * and the guideline object. + * @param null $options + * @return bool + */ + public function run_check($options = null) { + $this->prepare_dom(); + + if (!$this->is_valid()) { + return false; + } + + $this->get_css_object(); + $classname = 'tool_brickfield\\local\\htmlchecker\\guidelines\\'.strtolower($this->guidelinename).'_guideline'; + + $this->guideline = new $classname($this->dom, $this->css, $this->path, $options, $this->domain, $this->options['cms_mode']); + } + + /** + * Loads the brickfield_accessibility_css object + */ + public function get_css_object() { + $this->css = new brickfield_accessibility_css($this->dom, $this->uri, $this->type, $this->path, false, $this->cssfiles); + } + + /** + * Returns a formatted report from the current reporter. + * @param array $options An array of all the options + * @return mixed See the documentation on your reporter's getReport method. + */ + public function get_report(array $options = []) { + if (!$this->reporter) { + $this->load_reporter($options); + } + if ($options) { + $this->reporter->set_options($options); + } + $report = $this->reporter->get_report(); + $path = $this->path; + return ['report' => $report, 'path' => $path]; + } + + /** + * Runs one test on the current DOMDocument + * @param string $test The name of the test to run + * @return bool|array The ReportItem returned from the test + */ + public function get_test(string $test) { + $test = 'tool_brickfield\local\htmlchecker\common\checks\\' . $test; + + if (!class_exists($test)) { + return false; + } + + $testclass = new $test($this->dom, $this->css, $this->path); + + return $testclass->report; + } + + /** + * Retrieves the default severity of a test + * @param string $test The name of the test to run + * @return object The severity level of the test + */ + public function get_test_severity(string $test) { + $testclass = new $test($this->dom, $this->css, $this->path); + + return $testclass->get_severity(); + } + + /** + * A general cleanup function which just does some memory + * cleanup by unsetting the particularly large local vars. + */ + public function cleanup() { + unset($this->dom); + unset($this->css); + unset($this->guideline); + unset($this->reporter); + } + + /** + * Determines if the link text is the same as the link URL, without necessarily being an exact match. + * For example, 'www.google.com' matches 'https://www.google.com'. + * @param string $text + * @param string $href + * @return bool + */ + public static function match_urls(string $text, string $href): bool { + $parsetext = parse_url($text); + $parsehref = parse_url($href); + $parsetextfull = (isset($parsetext['host'])) ? $parsetext['host'] : ''; + $parsetextfull .= (isset($parsetext['path'])) ? $parsetext['path'] : ''; + $parsehreffull = (isset($parsehref['host'])) ? $parsehref['host'] : ''; + $parsehreffull .= (isset($parsehref['path'])) ? $parsehref['path'] : ''; + + // Remove any last '/' character before comparing. + return (rtrim($parsetextfull, '/') === rtrim($parsehreffull, '/')); + } +} diff --git a/admin/tool/brickfield/classes/local/htmlchecker/brickfield_accessibility_guideline.php b/admin/tool/brickfield/classes/local/htmlchecker/brickfield_accessibility_guideline.php new file mode 100644 index 0000000000000..4e97ac19be3e8 --- /dev/null +++ b/admin/tool/brickfield/classes/local/htmlchecker/brickfield_accessibility_guideline.php @@ -0,0 +1,156 @@ +. + +namespace tool_brickfield\local\htmlchecker; + +/** + * The base class for a guideline + * + * @package tool_brickfield + * @copyright 2020 onward: Brickfield Education Labs, www.brickfield.ie + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class brickfield_accessibility_guideline { + /** @var object The current document's DOMDocument */ + public $dom; + + /** @var object The current brickfield CSS object */ + public $css; + + /** @var array The path to the current document */ + public $path; + + /** @var array An array of report objects */ + public $report; + + /** @var array An array of translations for all this guideline's tests */ + public $translations; + + /** @var bool Whether we are running in CMS mode */ + public $cmsmode = false; + + /** @var array An array of all the severity levels for every test */ + public $severity = []; + + /** + * The class constructor. + * @param object $dom The current DOMDocument object + * @param object $css The current brickfieldCSS object + * @param array $path The current path + * @param null $arg + * @param string $domain + * @param bool $cmsmode + */ + public function __construct(&$dom, &$css, array &$path, + $arg = null, string $domain = 'en', bool $cmsmode = false) { + $this->dom = &$dom; + $this->css = &$css; + $this->path = &$path; + $this->cmsmode = $cmsmode; + $this->load_translations($domain); + $this->run($arg, $domain); + } + + /** + * Returns an array of all the tests associated with the current guideline + * @return array + */ + public function get_tests(): array { + return $this->tests; + } + + /** + * Loads translations from a file. This can be overriden, just as long as the + * local variable 'translations' is an associative array with test function names + * as the key + * @param string $domain + */ + public function load_translations(string $domain) { + $csv = fopen(dirname(__FILE__) .'/guidelines/translations/'. $domain .'.txt', 'r'); + + if ($csv) { + while ($translation = fgetcsv($csv)) { + if (count($translation) == 4) { + $this->translations[$translation[0]] = [ + 'title' => $translation[1], + 'description' => $translation[2], + ]; + } + } + } + } + + /** + * Returns the translation for a test name. + * @param string $testname The function name of the test + * @return mixed + */ + public function get_translation(string $testname) { + return (isset($this->translations[$testname])) + ? $this->translations[$testname] + : $testname; + } + + /** + * Iterates through each test string, makes a new test object, and runs it against the current DOM + * @param null $arg + * @param string $language + */ + public function run($arg = null, string $language = 'en') { + foreach ($this->tests as $testname => $options) { + if (is_numeric($testname) && !is_array($options)) { + $testname = $options; + } + $name = $testname; + $testname = 'tool_brickfield\local\htmlchecker\common\\checks\\'.$testname; + if (class_exists($testname) && $this->dom) { + $testname = new $testname($this->dom, $this->css, $this->path, $language, $arg); + if (!$this->cmsmode || ($testname->cms && $this->cmsmode)) { + $this->report[$name] = $testname->get_report(); + } + $this->severity[$name] = $testname->defaultseverity; + unset($testname); + } else { + $this->report[$name] = false; + } + } + } + + /** + * Returns all the Report variable + * @return mixed Look to your report to see what it returns + */ + public function get_report() { + return $this->report; + } + + /** + * Returns the severity level of a given test + * @param string $testname The name of the test + * @return int The severity level + */ + public function get_severity(string $testname): int { + if (isset($this->tests[$testname]['severity'])) { + return $this->tests[$testname]['severity']; + } + + if (isset($this->severity[$testname])) { + return $this->severity[$testname]; + } + + return brickfield_accessibility::BA_TEST_MODERATE; + } +} diff --git a/admin/tool/brickfield/classes/local/htmlchecker/brickfield_accessibility_report_item.php b/admin/tool/brickfield/classes/local/htmlchecker/brickfield_accessibility_report_item.php new file mode 100644 index 0000000000000..7f67912ee308d --- /dev/null +++ b/admin/tool/brickfield/classes/local/htmlchecker/brickfield_accessibility_report_item.php @@ -0,0 +1,90 @@ +. + +namespace tool_brickfield\local\htmlchecker; + +use DOMDocument; + +/** + * A report item. There is one per issue with the report + * + * @package tool_brickfield + * @copyright 2020 onward: Brickfield Education Labs, www.brickfield.ie + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class brickfield_accessibility_report_item { + + /** @var object The DOMElement that the report item refers to (if any) */ + public $element; + + /** @var string The error message */ + public $message; + + /** @var bool Whether the check needs to be manually verified */ + public $manual; + + /** @var bool For document-level tests, this says whether the test passed or not */ + public $pass; + + /** @var object For issues with more than two possible states, this contains information about the state */ + public $state; + + /** + * Returns the line number of the report item. Unfortunately we can't use getLineNo + * if we are before PHP 5.3, so if not we try to get the line number through a more + * circuitous way. + */ + public function get_line() { + if (is_object($this->element) && method_exists($this->element, 'getLineNo')) { + return $this->element->getLineNo(); + } + return 0; + } + + /** + * Returns the current element in plain HTML form + * @param array $extraattributes An array of extra attributes to add to the element + * @return string An HTML string version of the provided DOMElement object + */ + public function get_html(array $extraattributes = []): string { + if (!$this->element) { + return ''; + } + + $resultdom = new DOMDocument(); + $resultdom->formatOutput = true; + $resultdom->preserveWhiteSpace = false; + + try { + $resultelement = $resultdom->importNode($this->element, true); + } catch (Exception $e) { + return false; + } + + foreach ($this->element->attributes as $attribute) { + if ($attribute->name != 'brickfield_accessibility_style_index') { + $resultelement->setAttribute($attribute->name, $attribute->value); + } + } + + foreach ($extraattributes as $name => $value) { + $resultelement->setAttribute($name, $value); + } + + @$resultdom->appendChild($resultelement); + return @$resultdom->saveHTML(); + } +} diff --git a/admin/tool/brickfield/classes/local/htmlchecker/brickfield_accessibility_reporter.php b/admin/tool/brickfield/classes/local/htmlchecker/brickfield_accessibility_reporter.php new file mode 100644 index 0000000000000..f977a410b5f23 --- /dev/null +++ b/admin/tool/brickfield/classes/local/htmlchecker/brickfield_accessibility_reporter.php @@ -0,0 +1,96 @@ +. + +namespace tool_brickfield\local\htmlchecker; + +use stdClass; + +/** + * The base class for a reporter + * + * @package tool_brickfield + * @copyright 2020 onward: Brickfield Education Labs, www.brickfield.ie + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class brickfield_accessibility_reporter { + /** @var object The current document's DOMDocument */ + public $dom; + + /** @var object The current brickfieldaccessibilitycss object */ + public $css; + + /** @var array An array of test names and the translation for the problems with it */ + public $translation; + + /** @var array A collection of ReportItem objects */ + public $report; + + /** @var array The path to the current document */ + public $path; + + /** @var object Additional options for this reporter */ + public $options; + + /** @var array An array of attributes to search for to turn into absolute paths rather than relative paths */ + public $absoluteattributes = ['src', 'href']; + + /** + * The class constructor + * @param object $dom The current DOMDocument object + * @param object $css The current brickfield CSS object + * @param object $guideline The current guideline object + * @param string $path The current path + */ + public function __construct(&$dom, &$css, &$guideline, $path = '') { + $this->dom = &$dom; + $this->css = &$css; + $this->path = $path; + $this->options = new stdClass; + $this->guideline = &$guideline; + } + + /** + * Sets options for the reporter + * @param array $options an array of options + */ + public function set_options(array $options) { + foreach ($options as $key => $value) { + $this->options->$key = $value; + } + } + + /** + * Sets the absolute path for an element + * @param object $element A DOMElement object to turn into an absolute path + */ + public function set_absolute_path(&$element) { + $attr = false; + foreach ($this->absoluteattributes as $attribute) { + if ($element->hasAttribute($attribute)) { + $attr = $attribute; + } + } + + if ($attr) { + $item = $element->getAttribute($attr); + // We are ignoring items with absolute URLs. + if (strpos($item, '://') === false) { + $item = implode('/', $this->path) . ltrim($item, '/'); + $element->setAttribute($attr, $item); + } + } + } +} diff --git a/admin/tool/brickfield/classes/local/htmlchecker/common/body_color_contrast.php b/admin/tool/brickfield/classes/local/htmlchecker/common/body_color_contrast.php new file mode 100644 index 0000000000000..c9f09426cd6b5 --- /dev/null +++ b/admin/tool/brickfield/classes/local/htmlchecker/common/body_color_contrast.php @@ -0,0 +1,48 @@ +. + +namespace tool_brickfield\local\htmlchecker\common; + +/** + * Helper function to support checking the varous color attributes of the
tag against WCAG standards + * + * @package tool_brickfield + * @copyright 2020 onward: Brickfield Education Labs, www.brickfield.ie + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class body_color_contrast extends brickfield_accessibility_color_test { + /** @var string The attribute to check for the background color of the tag */ + public $background = 'bgcolor'; + + /** @var string The attribute to check for the foreground color of the tag */ + public $foreground = 'text'; + + /** + * Compares the WCAG contrast on the given color attributes of the tag + */ + public function check() { + $body = $this->get_all_elements('body'); + if (!$body) { + return false; + } + $body = $body[0]; + if ($body->hasAttribute($this->foreground) && $body->hasAttribute($this->background)) { + if ($this->get_luminosity($body->getAttribute($this->foreground), $body->getAttribute($this->background)) < 5) { + $this->add_report(null, null, false); + } + } + } +} diff --git a/admin/tool/brickfield/classes/local/htmlchecker/common/body_wai_ert_color_contrast.php b/admin/tool/brickfield/classes/local/htmlchecker/common/body_wai_ert_color_contrast.php new file mode 100644 index 0000000000000..5f4058902dcc0 --- /dev/null +++ b/admin/tool/brickfield/classes/local/htmlchecker/common/body_wai_ert_color_contrast.php @@ -0,0 +1,53 @@ +. + +namespace tool_brickfield\local\htmlchecker\common; + +/** + * Base class for test dealing with WAI ERT color contrast for the document + * + * Because a lot of the tests deal with text, vlink, alink, etc. + * + * @package tool_brickfield + * @copyright 2020 onward: Brickfield Education Labs, www.brickfield.ie + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class body_wai_ert_color_contrast extends brickfield_accessibility_color_test { + /** @var string The attribute to check for the background color of the tag */ + public $background = 'bgcolor'; + + /** @var string The attribute to check for the foreground color of the tag */ + public $foreground = 'text'; + + /** + * Compares the WAI ERT contrast on the given color attributes of the tag + */ + public function check() { + $body = $this->get_all_elements('body'); + if (!$body) { + return false; + } + $body = $body[0]; + if ($body->hasAttribute($this->foreground) && $body->hasAttribute($this->background)) { + if ($this->get_wai_ert_contrast($body->getAttribute($this->foreground), $body->getAttribute($this->background)) < 500) { + $this->add_report(null, null, false); + } else if ($this->get_wai_ert_brightness($body->getAttribute($this->foreground), + $body->getAttribute($this->background)) < 125) { + $this->add_report(null, null, false); + } + } + } +} diff --git a/admin/tool/brickfield/classes/local/htmlchecker/common/brickfield_accessibility_color_test.php b/admin/tool/brickfield/classes/local/htmlchecker/common/brickfield_accessibility_color_test.php new file mode 100644 index 0000000000000..82f6e01396829 --- /dev/null +++ b/admin/tool/brickfield/classes/local/htmlchecker/common/brickfield_accessibility_color_test.php @@ -0,0 +1,347 @@ +. + +namespace tool_brickfield\local\htmlchecker\common; + +/** + * Helper test base for tests dealing with color difference and luminosity. + * + * @package tool_brickfield + * @copyright 2020 onward: Brickfield Education Labs, www.brickfield.ie + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class brickfield_accessibility_color_test extends brickfield_accessibility_test { + + /** @var string[] Mapping of colours to hex codes. */ + public $colornames = [ + 'aliceblue' => 'f0f8ff', + 'antiquewhite' => 'faebd7', + 'aqua' => '00ffff', + 'aquamarine' => '7fffd4', + 'azure' => 'f0ffff', + 'beige' => 'f5f5dc', + 'bisque' => 'ffe4c4', + 'black' => '000000', + 'blanchedalmond' => 'ffebcd', + 'blue' => '0000ff', + 'blueviolet' => '8a2be2', + 'brown' => 'a52a2a', + 'burlywood' => 'deb887', + 'cadetblue' => '5f9ea0', + 'chartreuse' => '7fff00', + 'chocolate' => 'd2691e', + 'coral' => 'ff7f50', + 'cornflowerblue' => '6495ed', + 'cornsilk' => 'fff8dc', + 'crimson' => 'dc143c', + 'cyan' => '00ffff', + 'darkblue' => '00008b', + 'darkcyan' => '008b8b', + 'darkgoldenrod' => 'b8860b', + 'darkgray' => 'a9a9a9', + 'darkgreen' => '006400', + 'darkkhaki' => 'bdb76b', + 'darkmagenta' => '8b008b', + 'darkolivegreen' => '556b2f', + 'darkorange' => 'ff8c00', + 'darkorchid' => '9932cc', + 'darkred' => '8b0000', + 'darksalmon' => 'e9967a', + 'darkseagreen' => '8fbc8f', + 'darkslateblue' => '483d8b', + 'darkslategray' => '2f4f4f', + 'darkturquoise' => '00ced1', + 'darkviolet' => '9400d3', + 'deeppink' => 'ff1493', + 'deepskyblue' => '00bfff', + 'dimgray' => '696969', + 'dodgerblue' => '1e90ff', + 'firebrick' => 'b22222', + 'floralwhite' => 'fffaf0', + 'forestgreen' => '228b22', + 'fuchsia' => 'ff00ff', + 'gainsboro' => 'dcdcdc', + 'ghostwhite' => 'f8f8ff', + 'gold' => 'ffd700', + 'goldenrod' => 'daa520', + 'gray' => '808080', + 'green' => '008000', + 'greenyellow' => 'adff2f', + 'grey' => '808080', + 'honeydew' => 'f0fff0', + 'hotpink' => 'ff69b4', + 'indianred' => 'cd5c5c', + 'indigo' => '4b0082', + 'ivory' => 'fffff0', + 'khaki' => 'f0e68c', + 'lavender' => 'e6e6fa', + 'lavenderblush' => 'fff0f5', + 'lawngreen' => '7cfc00', + 'lemonchiffon' => 'fffacd', + 'lightblue' => 'add8e6', + 'lightcoral' => 'f08080', + 'lightcyan' => 'e0ffff', + 'lightgoldenrodyellow' => 'fafad2', + 'lightgrey' => 'd3d3d3', + 'lightgreen' => '90ee90', + 'lightpink' => 'ffb6c1', + 'lightsalmon' => 'ffa07a', + 'lightseagreen' => '20b2aa', + 'lightskyblue' => '87cefa', + 'lightslategray' => '778899', + 'lightsteelblue' => 'b0c4de', + 'lightyellow' => 'ffffe0', + 'lime' => '00ff00', + 'limegreen' => '32cd32', + 'linen' => 'faf0e6', + 'magenta' => 'ff00ff', + 'maroon' => '800000', + 'mediumaquamarine' => '66cdaa', + 'mediumblue' => '0000cd', + 'mediumorchid' => 'ba55d3', + 'mediumpurple' => '9370d8', + 'mediumseagreen' => '3cb371', + 'mediumslateblue' => '7b68ee', + 'mediumspringgreen' => '00fa9a', + 'mediumturquoise' => '48d1cc', + 'mediumvioletred' => 'c71585', + 'midnightblue' => '191970', + 'mintcream' => 'f5fffa', + 'mistyrose' => 'ffe4e1', + 'moccasin' => 'ffe4b5', + 'navajowhite' => 'ffdead', + 'navy' => '000080', + 'oldlace' => 'fdf5e6', + 'olive' => '808000', + 'olivedrab' => '6b8e23', + 'orange' => 'ffa500', + 'orangered' => 'ff4500', + 'orchid' => 'da70d6', + 'palegoldenrod' => 'eee8aa', + 'palegreen' => '98fb98', + 'paleturquoise' => 'afeeee', + 'palevioletred' => 'd87093', + 'papayawhip' => 'ffefd5', + 'peachpuff' => 'ffdab9', + 'peru' => 'cd853f', + 'pink' => 'ffc0cb', + 'plum' => 'dda0dd', + 'powderblue' => 'b0e0e6', + 'purple' => '800080', + 'red' => 'ff0000', + 'rosybrown' => 'bc8f8f', + 'royalblue' => '4169e1', + 'saddlebrown' => '8b4513', + 'salmon' => 'fa8072', + 'sandybrown' => 'f4a460', + 'seagreen' => '2e8b57', + 'seashell' => 'fff5ee', + 'sienna' => 'a0522d', + 'silver' => 'c0c0c0', + 'skyblue' => '87ceeb', + 'slateblue' => '6a5acd', + 'slategray' => '708090', + 'snow' => 'fffafa', + 'springgreen' => '00ff7f', + 'steelblue' => '4682b4', + 'tan' => 'd2b48c', + 'teal' => '008080', + 'thistle' => 'd8bfd8', + 'tomato' => 'ff6347', + 'turquoise' => '40e0d0', + 'violet' => 'ee82ee', + 'wheat' => 'f5deb3', + 'white' => 'ffffff', + 'whitesmoke' => 'f5f5f5', + 'yellow' => 'ffff00', + 'yellowgreen' => '9acd32' + ]; + + /** + * Helper method that finds the luminosity between the provided + * foreground and background parameters. + * @param string $foreground The HEX value of the foreground color + * @param string $background The HEX value of the background color + * @return float The luminosity contrast ratio between the colors + */ + public function get_luminosity(string $foreground, string $background): float { + if ($foreground == $background) { + return 0; + } + $forergb = $this->get_rgb($foreground); + $backrgb = $this->get_rgb($background); + return $this->luminosity($forergb['r'], $backrgb['r'], + $forergb['g'], $backrgb['g'], + $forergb['b'], $backrgb['b']); + } + + /** + * Returns the luminosity between two colors + * @param string $r The first Red value + * @param string $r2 The second Red value + * @param string $g The first Green value + * @param string $g2 The second Green value + * @param string $b The first Blue value + * @param string $b2 The second Blue value + * @return float The luminosity contrast ratio between the colors + */ + public function luminosity(string $r, string $r2, string $g, string $g2, string $b, string $b2): float { + $rsrgb = $r / 255; + $gsrgb = $g / 255; + $bsrgb = $b / 255; + $r3 = ($rsrgb <= 0.03928) ? $rsrgb / 12.92 : pow(($rsrgb + 0.055) / 1.055, 2.4); + $g3 = ($gsrgb <= 0.03928) ? $gsrgb / 12.92 : pow(($gsrgb + 0.055) / 1.055, 2.4); + $b3 = ($bsrgb <= 0.03928) ? $bsrgb / 12.92 : pow(($bsrgb + 0.055) / 1.055, 2.4); + + $rsrgb2 = $r2 / 255; + $gsrgb2 = $g2 / 255; + $bsrgb2 = $b2 / 255; + $r4 = ($rsrgb2 <= 0.03928) ? $rsrgb2 / 12.92 : pow(($rsrgb2 + 0.055) / 1.055, 2.4); + $g4 = ($gsrgb2 <= 0.03928) ? $gsrgb2 / 12.92 : pow(($gsrgb2 + 0.055) / 1.055, 2.4); + $b4 = ($bsrgb2 <= 0.03928) ? $bsrgb2 / 12.92 : pow(($bsrgb2 + 0.055) / 1.055, 2.4); + + if ($r + $g + $b <= $r2 + $g2 + $b2) { + $l2 = (.2126 * $r3 + 0.7152 * $g3 + 0.0722 * $b3); + $l1 = (.2126 * $r4 + 0.7152 * $g4 + 0.0722 * $b4); + } else { + $l1 = (.2126 * $r3 + 0.7152 * $g3 + 0.0722 * $b3); + $l2 = (.2126 * $r4 + 0.7152 * $g4 + 0.0722 * $b4); + } + + $luminosity = round(($l1 + 0.05) / ($l2 + 0.05), 2); + return $luminosity; + } + + + /** + * Returns the decimal equivalents for a HEX color + * @param string $color The hex color value + * @return array An array where 'r' is the Red value, 'g' is Green, and 'b' is Blue + */ + public function get_rgb(string $color) { + $color = $this->convert_color($color); + $c = str_split($color, 2); + if (count($c) != 3) { + return false; + } + $results = ['r' => hexdec($c[0]), 'g' => hexdec($c[1]), 'b' => hexdec($c[2])]; + return $results; + } + + /** + * Converts multiple color or background styles into a simple hex string + * @param string $color The color attribute to convert (this can also be a multi-value css background value) + * @return string A standard CSS hex value for the color + */ + public function convert_color(string $color): string { + $color = trim($color); + if (strpos($color, ' ') !== false) { + $colors = explode(' ', $color); + foreach ($colors as $backgroundpart) { + if (substr(trim($backgroundpart), 0, 1) == '#' || + in_array(trim($backgroundpart), array_keys($this->colornames)) || + strtolower(substr(trim($backgroundpart), 0, 3)) == 'rgb') { + $color = $backgroundpart; + } + } + } + // Normal hex color. + if (substr($color, 0, 1) == '#') { + if (strlen($color) == 7) { + return str_replace('#', '', $color); + } else if (strlen($color) == 4) { + return substr($color, 1, 1) . substr($color, 1, 1) . + substr($color, 2, 1) . substr($color, 2, 1) . + substr($color, 3, 1) . substr($color, 3, 1); + } else { + return "000000"; + } + } + // Named Color. + if (in_array($color, array_keys($this->colornames))) { + return $this->colornames[$color]; + } + // RGB values. + if (strtolower(substr($color, 0, 3)) == 'rgb') { + $colors = explode(',', trim(str_replace('rgb(', '', $color), '()')); + if (count($colors) != 3) { + return false; + } + $r = intval($colors[0]); + $g = intval($colors[1]); + $b = intval($colors[2]); + + $r = dechex($r < 0 ? 0 : ($r > 255 ? 255 : $r)); + $g = dechex($g < 0 ? 0 : ($g > 255 ? 255 : $g)); + $b = dechex($b < 0 ? 0 : ($b > 255 ? 255 : $b)); + + $color = (strlen($r) < 2 ? '0' : '') . $r; + $color .= (strlen($g) < 2 ? '0' : '') . $g; + $color .= (strlen($b) < 2 ? '0' : '') . $b; + return $color; + } + + return ''; + } + + /** + * Returns the WAIERT contrast between two colors + * @param string $foreground + * @param string $background + * @return array + * @see get_luminosity + */ + public function get_wai_ert_contrast(string $foreground, string $background): array { + $forergb = $this->get_rgb($foreground); + $backrgb = $this->get_rgb($background); + $diffs = $this->get_wai_diffs($forergb, $backrgb); + + return $diffs['red'] + $diffs['green'] + $diffs['blue']; + } + + /** + * Returns the WAI ERT Brightness between two colors + * @param string $foreground + * @param string $background + * @return float|int + */ + public function get_wai_ert_brightness(string $foreground, string $background): float { + $forergb = $this->get_rgb($foreground); + $backrgb = $this->get_rgb($background); + $color = $this->get_wai_diffs($forergb, $backrgb); + return (($color['red'] * 299) + ($color['green'] * 587) + ($color['blue'] * 114)) / 1000; + } + + /** + * Get wai diffs. + * @param array $forergb + * @param array $backrgb + * @return array + */ + public function get_wai_diffs(array $forergb, array $backrgb): array { + $reddiff = ($forergb['r'] > $backrgb['r']) + ? $forergb['r'] - $backrgb['r'] + : $backrgb['r'] - $forergb['r']; + $greendiff = ($forergb['g'] > $backrgb['g']) + ? $forergb['g'] - $backrgb['g'] + : $backrgb['g'] - $forergb['g']; + + $bluediff = ($forergb['b'] > $backrgb['b']) + ? $forergb['b'] - $backrgb['b'] + : $backrgb['b'] - $forergb['b']; + return ['red' => $reddiff, 'green' => $greendiff, 'blue' => $bluediff]; + } +} diff --git a/admin/tool/brickfield/classes/local/htmlchecker/common/brickfield_accessibility_css.php b/admin/tool/brickfield/classes/local/htmlchecker/common/brickfield_accessibility_css.php new file mode 100644 index 0000000000000..3af93874352c9 --- /dev/null +++ b/admin/tool/brickfield/classes/local/htmlchecker/common/brickfield_accessibility_css.php @@ -0,0 +1,541 @@ +. + +namespace tool_brickfield\local\htmlchecker\common; + +/** + * Parse content to check CSS validity. + * + * This class first parses all the CSS in the document and prepares an index of CSS styles to be used by accessibility tests + * to determine color and positioning. + * + * First, in loadCSS we get all the inline and linked style sheet information and merge it into a large CSS file string. + * + * Second, in setStyles we use XPath queries to find all the DOM elements which are effected by CSS styles and then + * build up an index in style_index of all the CSS styles keyed by an attriute we attach to all DOM objects to lookup + * the style quickly. + * + * Most of the second step is to get around the problem where XPath DOMNodeList objects are only marginally referential + * to the original elements and cannot be altered directly. + * + * @package tool_brickfield + * @copyright 2020 onward: Brickfield Education Labs, www.brickfield.ie + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class brickfield_accessibility_css { + + /** @var object The DOMDocument object of the current document */ + public $dom; + + /** @var string The URI of the current document */ + public $uri; + + /** @var string The type of request (inherited from the main htmlchecker object) */ + public $type; + + /** @var array An array of all the CSS elements and attributes */ + public $css; + + /** @var string Additional CSS information (usually for CMS mode requests) */ + public $cssstring; + + /** @var bool Whether or not we are running in CMS mode */ + public $cmsmode; + + /** @var array An array of all the strings which means the current style inherts from above */ + public $inheritancestrings = ['inherit', 'currentColor']; + + /** @var array An array of all the styles keyed by the new attribute brickfield_accessibility_style_index */ + public $styleindex = []; + + /** @var int The next index ID to be applied to a node to lookup later in style_index */ + public $nextindex = 0; + + /** @var array A list of all the elements which support deprecated styles such as 'background' or 'bgcolor' */ + public $deprecatedstyleelements = ['body', 'table', 'tr', 'td', 'th']; + + /** + * Class constructor. We are just building and importing variables here and then loading the CSS + * @param \DOMDocument $dom The DOMDocument object + * @param string $uri The URI of the request + * @param string $type The type of request + * @param array $path + * @param bool $cmsmode Whether we are running in CMS mode + * @param array $cssfiles An array of additional CSS files to load + */ + public function __construct(\DOMDocument &$dom, string $uri, string $type, array $path, bool $cmsmode = false, + array $cssfiles = []) { + $this->dom =& $dom; + $this->type = $type; + $this->uri = $uri; + $this->path = $path; + $this->cmsmode = $cmsmode; + $this->css_files = $cssfiles; + } + + /** + * Loads all the CSS files from the document using LINK elements or @import commands + */ + private function load_css() { + if (count($this->css_files) > 0) { + $css = $this->css_files; + } else { + $css = []; + $headerstyles = $this->dom->getElementsByTagName('style'); + foreach ($headerstyles as $headerstyle) { + if ($headerstyle->nodeValue) { + $this->cssstring .= $headerstyle->nodeValue; + } + } + $stylesheets = $this->dom->getElementsByTagName('link'); + + foreach ($stylesheets as $style) { + if ($style->hasAttribute('rel') && + (strtolower($style->getAttribute('rel')) == 'stylesheet') && + ($style->getAttribute('media') != 'print')) { + $css[] = $style->getAttribute('href'); + } + } + } + foreach ($css as $sheet) { + $this->load_uri($sheet); + } + $this->load_imported_files(); + $this->cssstring = str_replace(':link', '', $this->cssstring); + $this->format_css(); + } + + /** + * Imports files from the CSS file using @import commands + */ + private function load_imported_files() { + $matches = []; + preg_match_all('/@import (.*?);/i', $this->cssstring, $matches); + if (count($matches[1]) == 0) { + return null; + } + foreach ($matches[1] as $match) { + $this->load_uri(trim(str_replace('url', '', $match), '"\')(')); + } + preg_replace('/@import (.*?);/i', '', $this->cssstring); + } + + /** + * Returns a specificity count to the given selector. + * Higher specificity means it overrides other styles. + * @param string $selector The CSS Selector + * @return int $specifity + */ + public function get_specificity(string $selector): int { + $selector = $this->parse_selector($selector); + if ($selector[0][0] == ' ') { + unset($selector[0][0]); + } + $selector = $selector[0]; + $specificity = 0; + foreach ($selector as $part) { + switch(substr(str_replace('*', '', $part), 0, 1)) { + case '.': + $specificity += 10; + case '#': + $specificity += 100; + case ':': + $specificity++; + default: + $specificity++; + } + if (strpos($part, '[id=') != false) { + $specificity += 100; + } + } + return $specificity; + } + + /** + * Interface method for tests to call to lookup the style information for a given DOMNode + * @param \stdClass $element A DOMElement/DOMNode object + * @return array An array of style information (can be empty) + */ + public function get_style($element): array { + // To prevent having to parse CSS unless the info is needed, + // we check here if CSS has been set, and if not, run off the parsing now. + if (!is_a($element, 'DOMElement')) { + return []; + } + $style = $this->get_node_style($element); + if (isset($style['background-color']) || isset($style['color'])) { + $style = $this->walkup_tree_for_inheritance($element, $style); + } + if ($element->hasAttribute('style')) { + $inlinestyles = explode(';', $element->getAttribute('style')); + foreach ($inlinestyles as $inlinestyle) { + $s = explode(':', $inlinestyle); + + if (isset($s[1])) { // Edit: Make sure the style attribute doesn't have a trailing. + $style[trim($s[0])] = trim(strtolower($s[1])); + } + } + } + if ($element->tagName === 'strong') { + $style['font-weight'] = 'bold'; + } + if ($element->tagName === 'em') { + $style['font-style'] = 'italic'; + } + if (!is_array($style)) { + return []; + } + return $style; + } + + /** + * Adds a selector to the CSS index + * @param string $key The CSS selector + * @param string $codestr The CSS Style code string + * @return null + */ + private function add_selector(string $key, string $codestr) { + if (strpos($key, '@import') !== false) { + return null; + } + $key = strtolower($key); + $codestr = strtolower($codestr); + if (!isset($this->css[$key])) { + $this->css[$key] = array(); + } + $codes = explode(';', $codestr); + if (count($codes) > 0) { + foreach ($codes as $code) { + $code = trim($code); + $explode = explode(':', $code, 2); + if (count($explode) > 1) { + list($codekey, $codevalue) = $explode; + if (strlen($codekey) > 0) { + $this->css[$key][trim($codekey)] = trim($codevalue); + } + } + } + } + } + + /** + * Returns the style from the CSS index for a given element by first + * looking into its tag bucket then iterating over every item for an + * element that matches + * @param \stdClass $element + * @return array An array of all the style elements that _directly_ apply to that element (ignoring inheritance) + */ + private function get_node_style($element): array { + $style = []; + + if ($element->hasAttribute('brickfield_accessibility_style_index')) { + $style = $this->styleindex[$element->getAttribute('brickfield_accessibility_style_index')]; + } + // To support the deprecated 'bgcolor' attribute. + if ($element->hasAttribute('bgcolor') && in_array($element->tagName, $this->deprecatedstyleelements)) { + $style['background-color'] = $element->getAttribute('bgcolor'); + } + if ($element->hasAttribute('style')) { + $inlinestyles = explode(';', $element->getAttribute('style')); + foreach ($inlinestyles as $inlinestyle) { + $s = explode(':', $inlinestyle); + if (isset($s[1])) { // Edit: Make sure the style attribute doesn't have a trailing. + $style[trim($s[0])] = trim(strtolower($s[1])); + } + } + } + + return $style; + } + + /** + * A helper function to walk up the DOM tree to the end to build an array of styles. + * @param \stdClass $element The DOMNode object to walk up from + * @param array $style The current style built for the node + * @return array The array of the DOM element, altered if it was overruled through css inheritance + */ + private function walkup_tree_for_inheritance($element, array $style): array { + while (property_exists($element->parentNode, 'tagName')) { + $parentstyle = $this->get_node_style($element->parentNode); + if (is_array($parentstyle)) { + foreach ($parentstyle as $k => $v) { + if (!isset($style[$k])) { + $style[$k] = $v; + } + + if ((!isset($style['background-color'])) || strtolower($style['background-color']) == strtolower("#FFFFFF")) { + if ($k == 'background-color') { + $style['background-color'] = $v; + } + } + + if ((!isset($style['color'])) || strtolower($style['color']) == strtolower("#000000")) { + if ($k == 'color') { + $style['color'] = $v; + } + } + } + } + $element = $element->parentNode; + } + return $style; + } + + /** + * Loads a CSS file from a URI + * @param string $rel The URI of the CSS file + */ + private function load_uri(string $rel) { + if ($this->type == 'file') { + $uri = substr($this->uri, 0, strrpos($this->uri, '/')) .'/'.$rel; + } else { + $bfao = new \tool_brickfield\local\htmlchecker\brickfield_accessibility(); + $uri = $bfao->get_absolute_path($this->uri, $rel); + } + $this->cssstring .= @file_get_contents($uri); + + } + + /** + * Formats the CSS to be ready to import into an array of styles + * @return bool Whether there were elements imported or not + */ + private function format_css(): bool { + // Remove comments. + $str = preg_replace("/\/\*(.*)?\*\//Usi", "", $this->cssstring); + // Parse this csscode. + $parts = explode("}", $str); + if (count($parts) > 0) { + foreach ($parts as $part) { + if (strpos($part, '{') !== false) { + list($keystr, $codestr) = explode("{", $part); + $keys = explode(", ", trim($keystr)); + if (count($keys) > 0) { + foreach ($keys as $key) { + if (strlen($key) > 0) { + $key = str_replace("\n", "", $key); + $key = str_replace("\\", "", $key); + $this->add_selector($key, trim($codestr)); + } + } + } + } + } + } + return (count($this->css) > 0); + } + + /** + * Converts a CSS selector to an Xpath query + * @param string $selector The selector to convert + * @return string An Xpath query string + */ + private function get_xpath(string $selector): string { + $query = $this->parse_selector($selector); + + $xpath = '//'; + foreach ($query[0] as $k => $q) { + if ($q == ' ' && $k) { + $xpath .= '//'; + } else if ($q == '>' && $k) { + $xpath .= '/'; + } else if (substr($q, 0, 1) == '#') { + $xpath .= '[ @id = "' . str_replace('#', '', $q) . '" ]'; + } else if (substr($q, 0, 1) == '.') { + $xpath .= '[ @class = "' . str_replace('.', '', $q) . '" ]'; + } else if (substr($q, 0, 1) == '[') { + $xpath .= str_replace('[id', '[ @ id', $q); + } else { + $xpath .= trim($q); + } + } + return str_replace('//[', '//*[', str_replace('//[ @', '//*[ @', $xpath)); + } + + /** + * Checks that a string is really a regular character + * @param string $char The character + * @return bool Whether the string is a character + */ + private function is_char(string $char): bool { + return extension_loaded('mbstring') ? mb_eregi('\w', $char) : preg_match('@\w@', $char); + } + + /** + * Parses a CSS selector into an array of rules. + * @param string $query The CSS Selector query + * @return array An array of the CSS Selector parsed into rule segments + */ + private function parse_selector(string $query): array { + // Clean spaces. + $query = trim(preg_replace('@\s+@', ' ', preg_replace('@\s*(>|\\+|~)\s*@', '\\1', $query))); + $queries = [[]]; + if (!$query) { + return $queries; + } + $return =& $queries[0]; + $specialchars = ['>', ' ']; + $specialcharsmapping = []; + $strlen = mb_strlen($query); + $classchars = ['.', '-']; + $pseudochars = ['-']; + $tagchars = ['*', '|', '-']; + // Split multibyte string + // http://code.google.com/p/phpquery/issues/detail?id=76. + $newquery = []; + for ($i = 0; $i < $strlen; $i++) { + $newquery[] = mb_substr($query, $i, 1); + } + $query = $newquery; + // It works, but i dont like it... + $i = 0; + while ($i < $strlen) { + $c = $query[$i]; + $tmp = ''; + // TAG. + if ($this->is_char($c) || in_array($c, $tagchars)) { + while (isset($query[$i]) && ($this->is_char($query[$i]) || in_array($query[$i], $tagchars))) { + $tmp .= $query[$i]; + $i++; + } + $return[] = $tmp; + // IDs. + } else if ( $c == '#') { + $i++; + while (isset($query[$i]) && ($this->is_char($query[$i]) || $query[$i] == '-')) { + $tmp .= $query[$i]; + $i++; + } + $return[] = '#'.$tmp; + // SPECIAL CHARS. + } else if (in_array($c, $specialchars)) { + $return[] = $c; + $i++; + // MAPPED SPECIAL CHARS. + } else if ( isset($specialcharsmapping[$c])) { + $return[] = $specialcharsmapping[$c]; + $i++; + // COMMA. + } else if ( $c == ',') { + $queries[] = []; + $return =& $queries[count($queries) - 1]; + $i++; + while (isset($query[$i]) && $query[$i] == ' ') { + $i++; + } + // CLASSES. + } else if ($c == '.') { + while (isset($query[$i]) && ($this->is_char($query[$i]) || in_array($query[$i], $classchars))) { + $tmp .= $query[$i]; + $i++; + } + $return[] = $tmp; + // General Sibling Selector. + } else if ($c == '~') { + $spaceallowed = true; + $tmp .= $query[$i++]; + while (isset($query[$i]) + && ($this->is_char($query[$i]) + || in_array($query[$i], $classchars) + || $query[$i] == '*' + || ($query[$i] == ' ' && $spaceallowed) + )) { + if ($query[$i] != ' ') { + $spaceallowed = false; + } + $tmp .= $query[$i]; + $i++; + } + $return[] = $tmp; + // Adjacent sibling selectors. + } else if ($c == '+') { + $spaceallowed = true; + $tmp .= $query[$i++]; + while (isset($query[$i]) + && ($this->is_char($query[$i]) + || in_array($query[$i], $classchars) + || $query[$i] == '*' + || ($spaceallowed && $query[$i] == ' ') + )) { + if ($query[$i] != ' ') { + $spaceallowed = false; + } + $tmp .= $query[$i]; + $i++; + } + $return[] = $tmp; + // ATTRS. + } else if ($c == '[') { + $stack = 1; + $tmp .= $c; + while (isset($query[++$i])) { + $tmp .= $query[$i]; + if ( $query[$i] == '[') { + $stack++; + } else if ( $query[$i] == ']') { + $stack--; + if (!$stack) { + break; + } + } + } + $return[] = $tmp; + $i++; + // PSEUDO CLASSES. + } else if ($c == ':') { + $stack = 1; + $tmp .= $query[$i++]; + while (isset($query[$i]) && ($this->is_char($query[$i]) || in_array($query[$i], $pseudochars))) { + $tmp .= $query[$i]; + $i++; + } + // With arguments? + if (isset($query[$i]) && $query[$i] == '(') { + $tmp .= $query[$i]; + $stack = 1; + while (isset($query[++$i])) { + $tmp .= $query[$i]; + if ( $query[$i] == '(') { + $stack++; + } else if ( $query[$i] == ')') { + $stack--; + if (!$stack) { + break; + } + } + } + $return[] = $tmp; + $i++; + } else { + $return[] = $tmp; + } + } else { + $i++; + } + } + foreach ($queries as $k => $q) { + if (isset($q[0])) { + if (isset($q[0][0]) && $q[0][0] == ':') { + array_unshift($queries[$k], '*'); + } + if ($q[0] != '>') { + array_unshift($queries[$k], ' '); + } + } + } + return $queries; + } +} diff --git a/admin/tool/brickfield/classes/local/htmlchecker/common/brickfield_accessibility_dom_element.php b/admin/tool/brickfield/classes/local/htmlchecker/common/brickfield_accessibility_dom_element.php new file mode 100644 index 0000000000000..9059bc3238608 --- /dev/null +++ b/admin/tool/brickfield/classes/local/htmlchecker/common/brickfield_accessibility_dom_element.php @@ -0,0 +1,59 @@ +. + +namespace tool_brickfield\local\htmlchecker\common; + +use DOMElement; + +/** + * Brickfield accessibility HTML checker library. + * + * @package tool_brickfield + * @copyright 2020 onward: Brickfield Education Labs, www.brickfield.ie + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +/** + * An older attempt at using dom element extensions to introducefinding the styling of an element. + * @package tool_brickfield + * @deprecated + */ +class brickfield_accessibility_dom_element extends DOMElement { + + /** @var mixed Css style */ + public $cssstyle; + + /** + * Set css. + * @param mixed $css + */ + public function set_css($css) { + $this->cssstyle = $css; + } + + /** + * Get style. + * @param bool $style + * @return mixed + */ + public function get_style(bool $style = false) { + if (!$style) { + return $this->cssstyle; + } else { + return $this->cssstyle[$style]; + } + } +} diff --git a/admin/tool/brickfield/classes/local/htmlchecker/common/brickfield_accessibility_header_test.php b/admin/tool/brickfield/classes/local/htmlchecker/common/brickfield_accessibility_header_test.php new file mode 100644 index 0000000000000..95b33171e41ef --- /dev/null +++ b/admin/tool/brickfield/classes/local/htmlchecker/common/brickfield_accessibility_header_test.php @@ -0,0 +1,54 @@ +. + +namespace tool_brickfield\local\htmlchecker\common; + +/** + * Special base test class that deals with tests concerning the logical heirarchy of headers. + * + * @package tool_brickfield + * @copyright 2020 onward: Brickfield Education Labs, www.brickfield.ie + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class brickfield_accessibility_header_test extends brickfield_accessibility_test { + /** @var string The header tag this test applies to. */ + public $tag = ''; + + /** @var array An array of all the header tags */ + public $headers = ['h1', 'h2', 'h3', 'h4', 'h5', 'h6']; + + /** + * The check method gathers all the headers together and walks through them, making sure that + * the logical display of headers makes sense. + */ + public function check() { + $firstheader = $this->dom->getElementsByTagName($this->tag); + if ($firstheader->item(0)) { + $current = $firstheader->item(0); + $previousnumber = intval(substr($current->tagName, -1, 1)); + while ($current) { + if (property_exists($current, 'tagName') && in_array($current->tagName, $this->headers)) { + $currentnumber = intval(substr($current->tagName, -1, 1)); + if ($currentnumber > ($previousnumber + 1)) { + $this->add_report($current); + } + $previousnumber = intval(substr($current->tagName, -1, 1)); + } + $current = $current->nextSibling; + } + } + } +} diff --git a/admin/tool/brickfield/classes/local/htmlchecker/common/brickfield_accessibility_table_test.php b/admin/tool/brickfield/classes/local/htmlchecker/common/brickfield_accessibility_table_test.php new file mode 100644 index 0000000000000..b8a996da367d2 --- /dev/null +++ b/admin/tool/brickfield/classes/local/htmlchecker/common/brickfield_accessibility_table_test.php @@ -0,0 +1,82 @@ +. + +namespace tool_brickfield\local\htmlchecker\common; + +/** + * Special base class which provides helper methods for tables. + * + * @package tool_brickfield + * @copyright 2020 onward: Brickfield Education Labs, www.brickfield.ie + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class brickfield_accessibility_table_test extends brickfield_accessibility_test { + /** + * Takes the element object of a main table and returns the number of rows and columns in it. + * @param \stdClass $table + * @return array An array with the 'rows' value showing the number of rows, and column showing the number of columns + */ + public function get_table(\stdClass $table): array { + $rows = 0; + $columns = 0; + $firstrow = true; + if ($table->tagName != 'table') { + return false; + } + foreach ($table->childNodes as $child) { + if (property_exists($child, 'tagName') && $child->tagName == 'tr') { + $rows++; + if ($firstrow) { + foreach ($child->childNodes as $columnchild) { + if ($columnchild->tagName == 'th' || $columnchild->tagName == 'td') { + $columns++; + } + } + $firstrow = false; + } + } + } + + return ['rows' => $rows, 'columns' => $columns]; + } + + /** + * Finds whether or not the table is a data table. Checks that the + * table has a logical order and uses 'th' or 'thead' tags to illustrate + * the page author thought it was a data table. + * @param object $table The DOMElement object of the table tag + * @return bool TRUE if the element is a data table, otherwise false + */ + public function is_data($table): bool { + if ($table->tagName != 'table') { + return false; + } + + foreach ($table->childNodes as $child) { + if (property_exists($child, 'tagName') && $child->tagName == 'tr') { + foreach ($child->childNodes as $rowchild) { + if (property_exists($rowchild, 'tagName') && $rowchild->tagName == 'th') { + return true; + } + } + } + if (property_exists($child, 'tagName') && $child->tagName == 'thead') { + return true; + } + } + return false; + } +} diff --git a/admin/tool/brickfield/classes/local/htmlchecker/common/brickfield_accessibility_tag_test.php b/admin/tool/brickfield/classes/local/htmlchecker/common/brickfield_accessibility_tag_test.php new file mode 100644 index 0000000000000..8d5115507376f --- /dev/null +++ b/admin/tool/brickfield/classes/local/htmlchecker/common/brickfield_accessibility_tag_test.php @@ -0,0 +1,41 @@ +. + +namespace tool_brickfield\local\htmlchecker\common; + +/** + * Special class test thats only for file a report whenever it hits the specified tag regardless of anything about the element. + * + * @package tool_brickfield + * @copyright 2020 onward: Brickfield Education Labs, www.brickfield.ie + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class brickfield_accessibility_tag_test extends brickfield_accessibility_test { + /** + * @var string The tag name of this test + */ + public $tag = ''; + + /** + * Shouldn't need to be overridden. We just file one report item for every + * element we find with this class's $tag var. + */ + public function check() { + foreach ($this->get_all_elements($this->tag) as $element) { + $this->add_report($element); + } + } +} diff --git a/admin/tool/brickfield/classes/local/htmlchecker/common/brickfield_accessibility_test.php b/admin/tool/brickfield/classes/local/htmlchecker/common/brickfield_accessibility_test.php new file mode 100644 index 0000000000000..1b2738cf6b355 --- /dev/null +++ b/admin/tool/brickfield/classes/local/htmlchecker/common/brickfield_accessibility_test.php @@ -0,0 +1,394 @@ +. + +namespace tool_brickfield\local\htmlchecker\common; + +use tool_brickfield\local\htmlchecker\brickfield_accessibility_report_item; +use tool_brickfield\manager; + +/** + * This handles importing DOM objects, adding items to the report and provides a few DOM-traversing methods + * + * @package tool_brickfield + * @copyright 2020 onward: Brickfield Education Labs, www.brickfield.ie + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class brickfield_accessibility_test { + /** @var object The DOMDocument object */ + public $dom; + + /** @var object The brickfieldCSS object */ + public $css; + + /** @var array The path for the request */ + public $path; + + /** @var bool Whether the test can be used in a CMS (content without HTML head) */ + public $cms = true; + + /** @var string The base path for this request */ + public $basepath; + + /** @var array An array of ReportItem objects */ + public $report = array(); + + /** @var int The fallback severity level for all tests */ + public $defaultseverity = \tool_brickfield\local\htmlchecker\brickfield_accessibility::BA_TEST_SUGGESTION; + + /** @var array An array of all the extensions that are images */ + public $imageextensions = array('gif', 'jpg', 'png', 'jpeg', 'tiff', 'svn'); + + /** @var string The language domain */ + public $lang = 'en'; + + /** @var array An array of translatable strings */ + public $strings = array('en' => ''); + + /** + * The class constructor. We pass items by reference so we can alter the DOM if necessary + * @param object $dom The DOMDocument object + * @param object $css The brickfieldCSS object + * @param array $path The path of this request + * @param string $languagedomain The langauge domain to user + * @param mixed $options Any additional options passed by htmlchecker. + */ + public function __construct(&$dom, &$css, &$path, $languagedomain = 'en', $options = null) { + $this->dom = $dom; + $this->css = $css; + $this->path = $path; + $this->lang = $languagedomain; + $this->options = $options; + $this->report = array(); + $this->check(); + } + + /** + * Helper method to collect the report from this test. Some + * tests do additional cleanup by overriding this method + * @return array An array of ReportItem objects + */ + public function get_report(): array { + $this->report['severity'] = $this->defaultseverity; + return $this->report; + } + + /** + * Returns the default severity of the test + * @return int The severity level + */ + public function get_severity(): int { + return $this->defaultseverity; + } + + /** + * Adds a new ReportItem to this current tests collection of reports. + * Most reports pertain to a particular element (like an IMG with no Alt attribute); + * however, some are document-level and just either pass or don't pass + * @param object $element The DOMElement object that pertains to this report + * @param string $message An additional message to add to the report + * @param bool $pass Whether or not this report passed + * @param object $state Extra information about the error state + * @param bool $manual Whether the report needs a manual check + */ + public function add_report($element = null, $message = null, $pass = null, $state = null, $manual = null) { + $report = new brickfield_accessibility_report_item(); + $report->element = $element; + $report->message = $message; + $report->pass = $pass; + $report->state = $state; + $report->manual = $manual; + $report->line = $report->get_line(); + $this->report[] = $report; + } + + /** + * Retrieves the full path for a file. + * @param string $file The path to a file + * @return string The absolute path to the file. + */ + public function get_path($file): string { + if ((substr($file, 0, 7) == 'http://') || (substr($file, 0, 8) == 'https://')) { + return $file; + } + $file = explode('/', $file); + if (count($file) == 1) { + return implode('/', $this->path) . '/' . $file[0]; + } + + $path = $this->path; + foreach ($file as $directory) { + if ($directory == '..') { + array_pop($path); + } else { + $filepath[] = $directory; + } + } + return implode('/', $path) .'/'. implode('/', $filepath); + } + + /** + * Returns a translated variable. If the translation is unavailable, English is returned + * Because tests only really have one string array, we can get all of this info locally + * @return mixed The translation for the object + */ + public function translation() { + if (isset($this->strings[$this->lang])) { + return $this->strings[$this->lang]; + } + if (isset($this->strings['en'])) { + return $this->strings['en']; + } + return false; + } + + /** + * Helper method to find all the elements that fit a particular query + * in the document (either by tag name, or by attributes from the htmlElements object) + * @param mixed $tags Either a single tag name in a string, or an array of tag names + * @param string $options The kind of option to select an element by (see htmlElements) + * @param bool $value The value of the above option + * @return array An array of elements that fit the description + */ + public function get_all_elements($tags = null, string $options = '', bool $value = true): array { + if (!is_array($tags)) { + $tags = [$tags]; + } + if ($options !== '') { + $temp = new html_elements(); + $tags = $temp->get_elements_by_option($options, $value); + } + $result = []; + + if (!is_array($tags)) { + return []; + } + foreach ($tags as $tag) { + $elements = $this->dom->getElementsByTagName($tag); + if ($elements) { + foreach ($elements as $element) { + $result[] = $element; + } + } + } + if (count($result) == 0) { + return []; + } + return $result; + } + + /** + * Returns true if an element has a child with a given tag name + * @param object $element A DOMElement object + * @param string $childtag The tag name of the child to find + * @return bool TRUE if the element does have a child with + * the given tag name, otherwise FALSE + */ + public function element_has_child($element, string $childtag): bool { + foreach ($element->childNodes as $child) { + if (property_exists($child, 'tagName') && $child->tagName == $childtag) { + return true; + } + } + return false; + } + + /** + * Returns the first ancestor reached of a tag, or false if it hits + * the document root or a given tag. + * @param object $element A DOMElement object + * @param string $ancestortag The name of the tag we are looking for + * @param string $limittag Where to stop searching + * @return bool + */ + public function get_element_ancestor($element, string $ancestortag, string $limittag = 'body') { + while (property_exists($element, 'parentNode')) { + if ($element->parentNode->tagName == $ancestortag) { + return $element->parentNode; + } + if ($element->parentNode->tagName == $limittag) { + return false; + } + $element = $element->parentNode; + } + return false; + } + + /** + * Finds all the elements with a given tag name that has + * an attribute + * @param string $tag The tag name to search for + * @param string $attribute The attribute to search on + * @param bool $unique Whether we only want one result per attribute + * @return array An array of DOMElements with the attribute + * value as the key. + */ + public function get_elements_by_attribute(string $tag, string $attribute, bool $unique = false): array { + $results = array(); + foreach ($this->get_all_elements($tag) as $element) { + if ($element->hasAttribute($attribute)) { + if ($unique) { + $results[$element->getAttribute($attribute)] = $element; + } else { + $results[$element->getAttribute($attribute)][] = $element; + } + } + } + return $results; + } + + /** + * Returns the next element after the current one. + * @param object $element A DOMElement object + * @return mixed FALSE if there is no other element, or a DOMElement object + */ + public function get_next_element($element) { + $parent = $element->parentNode; + $next = false; + foreach ($parent->childNodes as $child) { + if ($next) { + return $child; + } + if ($child->isSameNode($element)) { + $next = true; + } + } + return false; + } + + /** + * To minimize notices, this compares an object's property to the valus + * and returns true or false. False will also be returned if the object is + * not really an object, or if the property doesn't exist at all + * @param object $object The object too look at + * @param string $property The name of the property + * @param mixed $value The value to check against + * @param bool $trim Whether the property value should be trimmed + * @param bool $lower Whether the property value should be compared on lower case + * + * @return bool + */ + public function property_is_equal($object, string $property, $value, bool $trim = false, bool $lower = false) { + if (!is_object($object)) { + return false; + } + if (!property_exists($object, $property)) { + return false; + } + $propertyvalue = $object->$property; + if ($trim) { + $propertyvalue = trim($propertyvalue); + $value = trim($value); + } + if ($lower) { + $propertyvalue = strtolower($propertyvalue); + $value = strtolower($value); + } + return ($propertyvalue == $value); + } + + /** + * Returns the parent of an elment that has a given tag Name, but + * stops the search if it hits the $limiter tag + * @param object $element The DOMElement object to search on + * @param string $tagname The name of the tag of the parent to find + * @param string $limiter The tag name of the element to stop searching on + * regardless of the results (like search for a parent "P" tag + * of this node but stop if you reach "body") + * @return mixed FALSE if no parent is found, or the DOMElement object of the found parent + */ + public function get_parent($element, string $tagname, string $limiter) { + while ($element) { + if ($element->tagName == $tagname) { + return $element; + } + if ($element->tagName == $limiter) { + return false; + } + $element = $element->parentNode; + } + return false; + } + + /** + * Returns if a GIF files is animated or not http://us.php.net/manual/en/function.imagecreatefromgif.php#88005 + * @param string $filename + * @return int + */ + public function image_is_animated($filename): int { + if (!($fh = @fopen($filename, 'rb'))) { + return false; + } + $count = 0; + // An animated gif contains multiple "frames", with each frame having a + // header made up of: + // * a static 4-byte sequence (\x00\x21\xF9\x04) + // * 4 variable bytes + // * a static 2-byte sequence (\x00\x2C). + + // We read through the file til we reach the end of the file, or we've found + // at least 2 frame headers. + while (!feof($fh) && $count < 2) { + $chunk = fread($fh, 1024 * 100); // Read 100kb at a time. + $count += preg_match_all('#\x00\x21\xF9\x04.{4}\x00\x2C#s', $chunk, $matches); + } + + fclose($fh); + return $count > 1; + } + + /** + * Returns if there are any printable/readable characters within an element. + * This finds both node values or images with alt text. + * @param object $element The given element to look at + * @return bool TRUE if contains readable text, FALSE if otherwise + */ + public function element_contains_readable_text($element): bool { + if (is_a($element, 'DOMText')) { + if (trim($element->wholeText) != '') { + return true; + } + } else { + if (trim($element->nodeValue) != '' || + ($element->hasAttribute('alt') && trim($element->getAttribute('alt')) != '')) { + return true; + } + if (method_exists($element, 'hasChildNodes') && $element->hasChildNodes()) { + foreach ($element->childNodes as $child) { + if ($this->element_contains_readable_text($child)) { + return true; + } + } + } + } + return false; + } + + /** + * Returns an array of the invalidlinkphrases for all enabled language packs. + * @return array of the invalidlinkphrases for all enabled language packs. + */ + public static function get_all_invalidlinkphrases(): array { + // Need to process all enabled lang versions of invalidlinkphrases. + $allstrings = []; + $enabledlangs = get_string_manager()->get_list_of_translations(); + foreach ($enabledlangs as $lang => $value) { + $tmpstring = (string)new \lang_string('invalidlinkphrases', manager::PLUGINNAME, null, $lang); + $tmplangarray = explode('|', $tmpstring); + $allstrings = array_merge($allstrings, $tmplangarray); + } + return $allstrings; + } +} diff --git a/admin/tool/brickfield/classes/local/htmlchecker/common/checks/a_links_dont_open_new_window.php b/admin/tool/brickfield/classes/local/htmlchecker/common/checks/a_links_dont_open_new_window.php new file mode 100644 index 0000000000000..87a263e37d366 --- /dev/null +++ b/admin/tool/brickfield/classes/local/htmlchecker/common/checks/a_links_dont_open_new_window.php @@ -0,0 +1,48 @@ +. + +namespace tool_brickfield\local\htmlchecker\common\checks; + +use tool_brickfield\local\htmlchecker\common\brickfield_accessibility_test; + +/** + * Brickfield accessibility HTML checker library. + * + * Anchor should not open new window without warning. + * a (anchor) element must not contain a target attribute unless the target attribute value is either _self, _top, or _parent. + * + * @package tool_brickfield + * @copyright 2020 onward: Brickfield Education Labs, www.brickfield.ie + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class a_links_dont_open_new_window extends brickfield_accessibility_test { + /** @var int $defaultseverity The default severity code for this test. */ + public $defaultseverity = \tool_brickfield\local\htmlchecker\brickfield_accessibility::BA_TEST_SEVERE; + + /** @var string[] A list of targets allowed that don't open a new window. */ + public $allowedtargets = array('_self', '_parent', '_top', ''); + + /** + * The main check function. This is called by the parent class to actually check content. + */ + public function check(): void { + foreach ($this->get_all_elements('a') as $a) { + if ($a->hasAttribute('target') && !in_array($a->getAttribute('target'), $this->allowedtargets)) { + $this->add_report($a); + } + } + } +} diff --git a/admin/tool/brickfield/classes/local/htmlchecker/common/checks/a_must_contain_text.php b/admin/tool/brickfield/classes/local/htmlchecker/common/checks/a_must_contain_text.php new file mode 100644 index 0000000000000..f7a1d8cfe9dce --- /dev/null +++ b/admin/tool/brickfield/classes/local/htmlchecker/common/checks/a_must_contain_text.php @@ -0,0 +1,56 @@ +. + +namespace tool_brickfield\local\htmlchecker\common\checks; + +use tool_brickfield\local\htmlchecker\common\brickfield_accessibility_test; + +/** + * Brickfield accessibility HTML checker library. + * + * Each source anchor contains text. + * a (anchor) element must contain text. The text may occur in the anchor text or in the title attribute of the anchor + * or in the Alt text of an image used within the anchor. + * + * @package tool_brickfield + * @copyright 2020 onward: Brickfield Education Labs, www.brickfield.ie + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class a_must_contain_text extends brickfield_accessibility_test { + /** @var int The default severity code for this test. */ + public $defaultseverity = \tool_brickfield\local\htmlchecker\brickfield_accessibility::BA_TEST_SEVERE; + + /** + * The main check function. This is called by the parent class to actually check content. + */ + public function check(): void { + foreach ($this->get_all_elements('a') as $a) { + if (!$this->element_contains_readable_text($a) && ($a->hasAttribute('href'))) { + $this->add_report($a); + } + } + } + + /** + * Returns if a link is not a candidate to be an anchor (which does + * not need text). + * @param \DOMElement $a + * @return bool Whether is is a link (TRUE) or an anchor (FALSE) + */ + public function is_not_anchor(\DOMElement $a): bool { + return (!($a->hasAttribute('name') && !$a->hasAttribute('href'))); + } +} diff --git a/admin/tool/brickfield/classes/local/htmlchecker/common/checks/a_suspicious_link_text.php b/admin/tool/brickfield/classes/local/htmlchecker/common/checks/a_suspicious_link_text.php new file mode 100644 index 0000000000000..b1708d07f7237 --- /dev/null +++ b/admin/tool/brickfield/classes/local/htmlchecker/common/checks/a_suspicious_link_text.php @@ -0,0 +1,70 @@ +. + +namespace tool_brickfield\local\htmlchecker\common\checks; + +use tool_brickfield\local\htmlchecker\brickfield_accessibility; +use tool_brickfield\local\htmlchecker\common\brickfield_accessibility_test; +use tool_brickfield\manager; + +/** + * Brickfield accessibility HTML checker library. + * + * Suspicious link text. + * 'a' (anchor) element cannot contain any of the following text, such as (English): "click here". + * + * @package tool_brickfield + * @copyright 2020 onward: Brickfield Education Labs, www.brickfield.ie + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class a_suspicious_link_text extends brickfield_accessibility_test { + /** + * @var int The default severity code for this test. + */ + public $defaultseverity = brickfield_accessibility::BA_TEST_SEVERE; + + /** + * The main check function. This is called by the parent class to actually check content + */ + public function check(): void { + // Need to process all enabled lang versions of invalidlinkphrases. + $badtext = brickfield_accessibility_test::get_all_invalidlinkphrases(); + + foreach ($this->get_all_elements('a') as $a) { + if (in_array(strtolower(trim($a->nodeValue)), $badtext) || $a->nodeValue == $a->getAttribute('href')) { + // If the link text matches invalid phrases. + $this->add_report($a); + } else if (brickfield_accessibility::match_urls($a->nodeValue, $a->getAttribute('href'))) { + // If the link text is the same as the link URL. + $this->add_report($a); + } + } + } + + /** + * Return all 'a' elements. + * + * @return array + */ + public function search(): array { + $data = []; + foreach ($this->get_all_elements('a') as $a) { + $data[] = $a; + } + + return $data; + } +} diff --git a/admin/tool/brickfield/classes/local/htmlchecker/common/checks/area_dont_open_new_window.php b/admin/tool/brickfield/classes/local/htmlchecker/common/checks/area_dont_open_new_window.php new file mode 100644 index 0000000000000..8fa55250bc55a --- /dev/null +++ b/admin/tool/brickfield/classes/local/htmlchecker/common/checks/area_dont_open_new_window.php @@ -0,0 +1,48 @@ +. + +namespace tool_brickfield\local\htmlchecker\common\checks; + +use tool_brickfield\local\htmlchecker\common\brickfield_accessibility_test; + +/** + * Brickfield accessibility HTML checker library. + * + * Area should not open new window without warning. + * Area element, target attribute values must contain any one of (case insensitive) _self, _top, _parent. + * + * @package tool_brickfield + * @copyright 2020 onward: Brickfield Education Labs, www.brickfield.ie + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class area_dont_open_new_window extends brickfield_accessibility_test { + /** @var int The default severity code for this test. */ + public $defaultseverity = \tool_brickfield\local\htmlchecker\brickfield_accessibility::BA_TEST_SEVERE; + + /** @var string[] A list of targets which are allowed. */ + public $allowedtargets = array('_self', '_parent', '_top'); + + /** + * The main check function. This is called by the parent class to actually check content + */ + public function check(): void { + foreach ($this->get_all_elements('area') as $area) { + if ($area->hasAttribute('target') && !in_array($area->getAttribute('target'), $this->allowedtargets)) { + $this->add_report($area); + } + } + } +} diff --git a/admin/tool/brickfield/classes/local/htmlchecker/common/checks/area_has_alt_value.php b/admin/tool/brickfield/classes/local/htmlchecker/common/checks/area_has_alt_value.php new file mode 100644 index 0000000000000..054e4684f3a11 --- /dev/null +++ b/admin/tool/brickfield/classes/local/htmlchecker/common/checks/area_has_alt_value.php @@ -0,0 +1,47 @@ +. + +namespace tool_brickfield\local\htmlchecker\common\checks; + +use tool_brickfield\local\htmlchecker\common\brickfield_accessibility_test; + +/** + * Brickfield accessibility HTML checker library. + * + * All area elements have an alt attribute. + * Area elements must contain a alt attribute. + * + * @package tool_brickfield + * @copyright 2020 onward: Brickfield Education Labs, www.brickfield.ie + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class area_has_alt_value extends brickfield_accessibility_test { + /** + * @var int The default severity code for this test. + */ + public $defaultseverity = \tool_brickfield\local\htmlchecker\brickfield_accessibility::BA_TEST_SEVERE; + + /** + * The main check function. This is called by the parent class to actually check content + */ + public function check(): void { + foreach ($this->get_all_elements('area') as $area) { + if (!$area->hasAttribute('alt')) { + $this->add_report($area); + } + } + } +} diff --git a/admin/tool/brickfield/classes/local/htmlchecker/common/checks/basefont_is_not_used.php b/admin/tool/brickfield/classes/local/htmlchecker/common/checks/basefont_is_not_used.php new file mode 100644 index 0000000000000..f610d3502966d --- /dev/null +++ b/admin/tool/brickfield/classes/local/htmlchecker/common/checks/basefont_is_not_used.php @@ -0,0 +1,37 @@ +. + +namespace tool_brickfield\local\htmlchecker\common\checks; + +use tool_brickfield\local\htmlchecker\common\brickfield_accessibility_tag_test; + +/** + * Brickfield accessibility HTML checker library. + * + * 'basefont' must not be used. + * This error is generated for all basefont elements. + * + * @package tool_brickfield + * @copyright 2020 onward: Brickfield Education Labs, www.brickfield.ie + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class basefont_is_not_used extends brickfield_accessibility_tag_test { + /** @var int The default severity code for this test. */ + public $defaultseverity = \tool_brickfield\local\htmlchecker\brickfield_accessibility::BA_TEST_SEVERE; + + /** @var string The tag this test will fire on. */ + public $tag = 'basefont'; +} diff --git a/admin/tool/brickfield/classes/local/htmlchecker/common/checks/blink_is_not_used.php b/admin/tool/brickfield/classes/local/htmlchecker/common/checks/blink_is_not_used.php new file mode 100644 index 0000000000000..d26cf5f7667ca --- /dev/null +++ b/admin/tool/brickfield/classes/local/htmlchecker/common/checks/blink_is_not_used.php @@ -0,0 +1,37 @@ +. + +namespace tool_brickfield\local\htmlchecker\common\checks; + +use tool_brickfield\local\htmlchecker\common\brickfield_accessibility_tag_test; + +/** + * Brickfield accessibility HTML checker library. + * + * 'blink' element is not used. + * This error is generated for all blink elements. + * + * @package tool_brickfield + * @copyright 2020 onward: Brickfield Education Labs, www.brickfield.ie + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class blink_is_not_used extends brickfield_accessibility_tag_test { + /** @var int The default severity code for this test. */ + public $defaultseverity = \tool_brickfield\local\htmlchecker\brickfield_accessibility::BA_TEST_SEVERE; + + /** @var string The tag this test will fire on. */ + public $tag = 'blink'; +} diff --git a/admin/tool/brickfield/classes/local/htmlchecker/common/checks/bold_is_not_used.php b/admin/tool/brickfield/classes/local/htmlchecker/common/checks/bold_is_not_used.php new file mode 100644 index 0000000000000..76460fdb04482 --- /dev/null +++ b/admin/tool/brickfield/classes/local/htmlchecker/common/checks/bold_is_not_used.php @@ -0,0 +1,37 @@ +. + +namespace tool_brickfield\local\htmlchecker\common\checks; + +use tool_brickfield\local\htmlchecker\common\brickfield_accessibility_tag_test; + +/** + * Brickfield accessibility HTML checker library. + * + * 'b' (bold) element is not used. + * This error will be generated for all B elements. + * + * @package tool_brickfield + * @copyright 2020 onward: Brickfield Education Labs, www.brickfield.ie + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class bold_is_not_used extends brickfield_accessibility_tag_test { + /** @var int The default severity code for this test. */ + public $defaultseverity = \tool_brickfield\local\htmlchecker\brickfield_accessibility::BA_TEST_SEVERE; + + /** @var string The tag this test will fire on. */ + public $tag = 'b'; +} diff --git a/admin/tool/brickfield/classes/local/htmlchecker/common/checks/content_too_long.php b/admin/tool/brickfield/classes/local/htmlchecker/common/checks/content_too_long.php new file mode 100644 index 0000000000000..f05ac7b304c22 --- /dev/null +++ b/admin/tool/brickfield/classes/local/htmlchecker/common/checks/content_too_long.php @@ -0,0 +1,55 @@ +. + +namespace tool_brickfield\local\htmlchecker\common\checks; + +use tool_brickfield\local\htmlchecker\common\brickfield_accessibility_test; + +/** + * Brickfield accessibility HTML checker library. + * + * Test counts words for all text elements on page and suggests content chunking for pages longer than 3000 words. + * + * @package tool_brickfield + * @copyright 2020 onward: Brickfield Education Labs, www.brickfield.ie + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class content_too_long extends brickfield_accessibility_test { + + /** @var int The default severity code for this test. */ + public $defaultseverity = \tool_brickfield\local\htmlchecker\brickfield_accessibility::BA_TEST_SUGGESTION; + + /** + * The main check function. This is called by the parent class to actually check content. + */ + public function check(): void { + global $contentlengthlimit; + + $contentlengthlimit = 500; + $pagetext = ''; + foreach ($this->get_all_elements(null, 'text') as $element) { + $text = $element->nodeValue; + if ($text != null) { + $pagetext = $pagetext . $text; + } + } + + $wordcount = str_word_count($pagetext); + if ($wordcount > $contentlengthlimit) { + $this->add_report(null, "Word Count: " . $wordcount . "
", false); + } + } +} diff --git a/admin/tool/brickfield/classes/local/htmlchecker/common/checks/css_text_has_contrast.php b/admin/tool/brickfield/classes/local/htmlchecker/common/checks/css_text_has_contrast.php new file mode 100644 index 0000000000000..1ae273c3180e0 --- /dev/null +++ b/admin/tool/brickfield/classes/local/htmlchecker/common/checks/css_text_has_contrast.php @@ -0,0 +1,148 @@ +. + +namespace tool_brickfield\local\htmlchecker\common\checks; + +use DOMXPath; +use tool_brickfield\local\htmlchecker\common\brickfield_accessibility_color_test; + +/** + * Brickfield accessibility HTML checker library. + * + * @package tool_brickfield + * @copyright 2020 onward: Brickfield Education Labs, www.brickfield.ie + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class css_text_has_contrast extends brickfield_accessibility_color_test { + + /** @var int The default severity code for this test. */ + public $defaultseverity = \tool_brickfield\local\htmlchecker\brickfield_accessibility::BA_TEST_SEVERE; + + /** @var string The default background color. */ + public $defaultbackground = '#ffffff'; + + /** @var string The default color. */ + public $defaultcolor = '#000000'; + + /** + * The main check function. This is called by the parent class to actually check content + */ + public function check(): void { + if (isset($this->options['css_background'])) { + $this->defaultbackground = $this->options['css_background']; + } + + if (isset($this->options['css_foreground'])) { + $this->defaultcolor = $this->options['css_foreground']; + } + + $xpath = new DOMXPath($this->dom); + + // Selects all nodes that have a style attribute OR 'strong' OR 'em' elements that: + // Contain only the text in their text nodes + // OR Have text nodes AND text nodes that are not equal to the string-value of the context node + // OR Have a text node descendant that equals the string-value of the context node and has no style attributes. + + $entries = $xpath->query('//*[(text() = . or ( ./*[text() != .]) or (.//*[text() = . and not(@style)])) + and ((@style) or (name() = "strong") or (name() = "em"))]'); + + foreach ($entries as $element) { + $style = $this->css->get_style($element); + + if (isset($style['background-color']) || isset($style['color'])) { + if (!isset($style['background-color'])) { + $style['background-color'] = $this->defaultbackground; + } + + if (!isset($style['color'])) { + $style['color'] = $this->defaultcolor; + } + + if ((isset($style['background']) || isset($style['background-color'])) && isset($style['color']) && + $element->nodeValue) { + + $background = (isset($style['background-color'])) ? $style['background-color'] : $style['background']; + if (!$background || !empty($this->options['css_only_use_default'])) { + $background = $this->defaultbackground; + } + + $style['color'] = '#' . $this->convert_color($style['color']); + $style['background-color'] = '#' . $this->convert_color($background); + + if (substr($background, 0, 3) == "rgb") { + $background = '#' . $this->convert_color($background); + } + + $luminosity = $this->get_luminosity($style['color'], $background); + $fontsize = 0; + $bold = false; + $italic = false; + + if (isset($style['font-size'])) { + preg_match_all('!\d+!', $style['font-size'], $matches); + $fontsize = $matches[0][0]; + } + + if (isset($style['font-weight'])) { + preg_match_all('!\d+!', $style['font-weight'], $matches); + + if (count($matches) > 0) { + if ($matches >= 700) { + $bold = true; + } else { + if ($style['font-weight'] === 'bold' || $style['font-weight'] === 'bolder') { + $bold = true; + } + } + } + } else if ($element->tagName === "strong") { + $bold = true; + $style['font-weight'] = "bold"; + } else { + $style['font-weight'] = "normal"; + } + + if (isset($style['font-style'])) { + if ($style['font-style'] === "italic") { + $italic = true; + } + } else if ($element->tagName === "em") { + $italic = true; + $style['font-style'] = "italic"; + } else { + $style['font-style'] = "normal"; + } + + if ($element->tagName === 'h1' || $element->tagName === 'h2' || $element->tagName === 'h3' || + $element->tagName === 'h4' || $element->tagName === 'h5' || $element->tagName === 'h6' || + $fontsize >= 18 || $fontsize >= 14 && $bold) { + if ($luminosity < 3) { + $message = 'heading: background-color: ' . $background . '; color:' . $style["color"] . + '; font-style: ' . $style['font-style'] . '; font-weight: ' . $style['font-weight'] . '; '; + $this->add_report($element, $message); + } + } else { + if ($luminosity < 4.5) { + $message = 'text: background-color: ' . $background . '; color:' . $style["color"] . '; font-style: ' . + $style['font-style'] . '; font-weight: ' . $style['font-weight'] . '; '; + $this->add_report($element, $message); + } + } + } + } + } + } +} diff --git a/admin/tool/brickfield/classes/local/htmlchecker/common/checks/embed_has_associated_no_embed.php b/admin/tool/brickfield/classes/local/htmlchecker/common/checks/embed_has_associated_no_embed.php new file mode 100644 index 0000000000000..8f58cca0ac7b6 --- /dev/null +++ b/admin/tool/brickfield/classes/local/htmlchecker/common/checks/embed_has_associated_no_embed.php @@ -0,0 +1,53 @@ +. + +namespace tool_brickfield\local\htmlchecker\common\checks; + +use tool_brickfield\local\htmlchecker\common\brickfield_accessibility_test; + +/** + * Brickfield accessibility HTML checker library. + * + * All embed elements have an associated noembed element that contains a text equivalent to the embed element. + * Provide a text equivalent for the embed element. + * + * @package tool_brickfield + * @copyright 2020 onward: Brickfield Education Labs, www.brickfield.ie + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class embed_has_associated_no_embed extends brickfield_accessibility_test { + + /** @var int The default severity code for this test. */ + public $defaultseverity = \tool_brickfield\local\htmlchecker\brickfield_accessibility::BA_TEST_SEVERE; + + /** + * The main check function. This is called by the parent class to actually check content + */ + public function check(): void { + foreach ($this->get_all_elements('embed') as $embed) { + // If one of the child nodes is a 'noembed', then content is fine. + foreach ($embed->childNodes as $child) { + if ($child->nodeName == 'noembed') { + return; + } + } + // There were no 'noembed' child nodes. If the next sibling isn't, then the content is flawed. + if (!$this->property_is_equal($embed->nextSibling, 'tagName', 'noembed')) { + $this->add_report($embed); + } + } + } +} diff --git a/admin/tool/brickfield/classes/local/htmlchecker/common/checks/header_h3.php b/admin/tool/brickfield/classes/local/htmlchecker/common/checks/header_h3.php new file mode 100644 index 0000000000000..4af96f349d341 --- /dev/null +++ b/admin/tool/brickfield/classes/local/htmlchecker/common/checks/header_h3.php @@ -0,0 +1,35 @@ +. + +namespace tool_brickfield\local\htmlchecker\common\checks; + +use tool_brickfield\local\htmlchecker\common\brickfield_accessibility_header_test; + +/** + * Brickfield accessibility HTML checker library. + * + * The header following an h3 is h1, h2, h3 or h4. + * The following header must be equal, one level greater or any level less. + * + * @package tool_brickfield + * @copyright 2020 onward: Brickfield Education Labs, www.brickfield.ie + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class header_h3 extends brickfield_accessibility_header_test { + + /** @var string The tag this test will fire on. */ + public $tag = 'h3'; +} diff --git a/admin/tool/brickfield/classes/local/htmlchecker/common/checks/headers_have_text.php b/admin/tool/brickfield/classes/local/htmlchecker/common/checks/headers_have_text.php new file mode 100644 index 0000000000000..6fe8e22e535d6 --- /dev/null +++ b/admin/tool/brickfield/classes/local/htmlchecker/common/checks/headers_have_text.php @@ -0,0 +1,45 @@ +. + +namespace tool_brickfield\local\htmlchecker\common\checks; + +use tool_brickfield\local\htmlchecker\common\brickfield_accessibility_test; + +/** + * Brickfield accessibility HTML checker library. + * + * Headers should have text content so as not to confuse screen-reader users. + * + * @package tool_brickfield + * @copyright 2020 onward: Brickfield Education Labs, www.brickfield.ie + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class headers_have_text extends brickfield_accessibility_test { + + /** @var int The default severity code for this test. */ + public $defaultseverity = \tool_brickfield\local\htmlchecker\brickfield_accessibility::BA_TEST_SEVERE; + + /** + * The main check function. This is called by the parent class to actually check content + */ + public function check(): void { + foreach ($this->get_all_elements(null, 'header', true) as $header) { + if (!$this->element_contains_readable_text($header)) { + $this->add_report($header); + } + } + } +} diff --git a/admin/tool/brickfield/classes/local/htmlchecker/common/checks/i_is_not_used.php b/admin/tool/brickfield/classes/local/htmlchecker/common/checks/i_is_not_used.php new file mode 100644 index 0000000000000..a9a7799b32a06 --- /dev/null +++ b/admin/tool/brickfield/classes/local/htmlchecker/common/checks/i_is_not_used.php @@ -0,0 +1,38 @@ +. + +namespace tool_brickfield\local\htmlchecker\common\checks; + +use tool_brickfield\local\htmlchecker\common\brickfield_accessibility_tag_test; + +/** + * Brickfield accessibility HTML checker library. + * + * 'i' (italic) element is not used. + * This error will be generated for all i elements. + * + * @package tool_brickfield + * @copyright 2020 onward: Brickfield Education Labs, www.brickfield.ie + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class i_is_not_used extends brickfield_accessibility_tag_test { + + /** @var int The default severity code for this test. */ + public $defaultseverity = \tool_brickfield\local\htmlchecker\brickfield_accessibility::BA_TEST_SEVERE; + + /** @var string The tag this test will fire on. */ + public $tag = 'i'; +} diff --git a/admin/tool/brickfield/classes/local/htmlchecker/common/checks/img_alt_is_different.php b/admin/tool/brickfield/classes/local/htmlchecker/common/checks/img_alt_is_different.php new file mode 100644 index 0000000000000..f6b943ce49ff6 --- /dev/null +++ b/admin/tool/brickfield/classes/local/htmlchecker/common/checks/img_alt_is_different.php @@ -0,0 +1,48 @@ +. + +namespace tool_brickfield\local\htmlchecker\common\checks; + +use tool_brickfield\local\htmlchecker\common\brickfield_accessibility_test; + +/** + * Brickfield accessibility HTML checker library. + * + * Alt text is not the same as the filename unless author has confirmed it is correct. + * 'img' element cannot have alt attribute value that is the same as its src attribute. + * + * @package tool_brickfield + * @copyright 2020 onward: Brickfield Education Labs, www.brickfield.ie + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class img_alt_is_different extends brickfield_accessibility_test { + + /** @var int The default severity code for this test. */ + public $defaultseverity = \tool_brickfield\local\htmlchecker\brickfield_accessibility::BA_TEST_SEVERE; + + /** + * The main check function. This is called by the parent class to actually check content + */ + public function check(): void { + foreach ($this->get_all_elements('img') as $img) { + if (trim($img->getAttribute('src')) == trim($img->getAttribute('alt'))) { + $this->add_report($img); + } else if (preg_match("/.jpg|.JPG|.png|.PNG|.gif|.GIF|.jpeg|.JPEG$/", trim($img->getAttribute('alt')))) { + $this->add_report($img); + } + } + } +} diff --git a/admin/tool/brickfield/classes/local/htmlchecker/common/checks/img_alt_is_too_long.php b/admin/tool/brickfield/classes/local/htmlchecker/common/checks/img_alt_is_too_long.php new file mode 100644 index 0000000000000..e19812c33c5db --- /dev/null +++ b/admin/tool/brickfield/classes/local/htmlchecker/common/checks/img_alt_is_too_long.php @@ -0,0 +1,49 @@ +. + +namespace tool_brickfield\local\htmlchecker\common\checks; + +use tool_brickfield\local\htmlchecker\common\brickfield_accessibility_test; + +/** + * Brickfield accessibility HTML checker library. + * + * Image Alt text is long. + * Image Alt text is long or user must confirm that Alt text is as short as possible. + * + * @package tool_brickfield + * @copyright 2020 onward: Brickfield Education Labs, www.brickfield.ie + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class img_alt_is_too_long extends brickfield_accessibility_test { + + /** @var int The default severity code for this test. */ + public $defaultseverity = \tool_brickfield\local\htmlchecker\brickfield_accessibility::BA_TEST_SEVERE; + + /** + * The main check function. This is called by the parent class to actually check content. + */ + public function check(): void { + global $alttextlengthlimit; + + foreach ($this->get_all_elements('img') as $img) { + $alttextlengthlimit = 125; + if ($img->hasAttribute('alt') && strlen($img->getAttribute('alt')) > $alttextlengthlimit) { + $this->add_report($img); + } + } + } +} diff --git a/admin/tool/brickfield/classes/local/htmlchecker/common/checks/img_alt_not_empty_in_anchor.php b/admin/tool/brickfield/classes/local/htmlchecker/common/checks/img_alt_not_empty_in_anchor.php new file mode 100644 index 0000000000000..461649773b79c --- /dev/null +++ b/admin/tool/brickfield/classes/local/htmlchecker/common/checks/img_alt_not_empty_in_anchor.php @@ -0,0 +1,52 @@ +. + +namespace tool_brickfield\local\htmlchecker\common\checks; + +use tool_brickfield\local\htmlchecker\common\brickfield_accessibility_test; + +/** + * Brickfield accessibility HTML checker library. + * + * Alt text for all img elements used as source anchors is not empty when there is no other text in the anchor. + * img element cannot have alt attribute value of null or whitespace if the img element is contained by an + * a element and there is no other link text. + * + * @package tool_brickfield + * @copyright 2020 onward: Brickfield Education Labs, www.brickfield.ie + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class img_alt_not_empty_in_anchor extends brickfield_accessibility_test { + + /** @var int The default severity code for this test. */ + public $defaultseverity = \tool_brickfield\local\htmlchecker\brickfield_accessibility::BA_TEST_SEVERE; + + /** + * The main check function. This is called by the parent class to actually check content + */ + public function check(): void { + foreach ($this->get_all_elements('a') as $a) { + if (!$a->nodeValue && $a->childNodes) { + foreach ($a->childNodes as $child) { + if ($this->property_is_equal($child, 'tagName', 'img') + && trim($child->getAttribute('alt')) == '') { + $this->add_report($child); + } + } + } + } + } +} diff --git a/admin/tool/brickfield/classes/local/htmlchecker/common/checks/img_alt_not_place_holder.php b/admin/tool/brickfield/classes/local/htmlchecker/common/checks/img_alt_not_place_holder.php new file mode 100644 index 0000000000000..b1c6c4becfe5b --- /dev/null +++ b/admin/tool/brickfield/classes/local/htmlchecker/common/checks/img_alt_not_place_holder.php @@ -0,0 +1,60 @@ +. + +namespace tool_brickfield\local\htmlchecker\common\checks; + +use tool_brickfield\local\htmlchecker\common\brickfield_accessibility_test; + +/** + * Brickfield accessibility HTML checker library. + * + * Alt text for all img elements is not placeholder text unless author has confirmed it is correct. + * 'img' element cannot have alt attribute value of "nbsp" or "spacer". + * + * @package tool_brickfield + * @copyright 2020 onward: Brickfield Education Labs, www.brickfield.ie + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class img_alt_not_place_holder extends brickfield_accessibility_test { + /** @var int The default severity code for this test. */ + public $defaultseverity = \tool_brickfield\local\htmlchecker\brickfield_accessibility::BA_TEST_SEVERE; + + /** @var \string[][] An array of strings, broken up by language domain. */ + public $strings = + [ + 'en' => array('nbsp', ' ', 'spacer', 'image', 'img', 'photo'), + 'es' => array('nbsp', ' ', 'spacer', 'espacio', 'imagen', 'img', 'foto'), + ]; + + /** + * The main check function. This is called by the parent class to actually check content. + */ + public function check(): void { + foreach ($this->get_all_elements('img') as $img) { + if ($img->hasAttribute('alt')) { + if (strlen($img->getAttribute('alt')) > 0) { + if (in_array($img->getAttribute('alt'), $this->translation()) + || ord($img->getAttribute('alt')) == 194) { + $this->add_report($img); + } else if (preg_match("/^([0-9]*)(k|kb|mb|k bytes|k byte)?$/", + strtolower($img->getAttribute('alt')))) { + $this->add_report($img); + } + } + } + } + } +} diff --git a/admin/tool/brickfield/classes/local/htmlchecker/common/checks/img_has_alt.php b/admin/tool/brickfield/classes/local/htmlchecker/common/checks/img_has_alt.php new file mode 100644 index 0000000000000..8339560ffc7d5 --- /dev/null +++ b/admin/tool/brickfield/classes/local/htmlchecker/common/checks/img_has_alt.php @@ -0,0 +1,47 @@ +. + +namespace tool_brickfield\local\htmlchecker\common\checks; + +use tool_brickfield\local\htmlchecker\common\brickfield_accessibility_test; + +/** + * Brickfield accessibility HTML checker library. + * + * All img elements must have an alt attribute. + * + * @package tool_brickfield + * @copyright 2020 onward: Brickfield Education Labs, www.brickfield.ie + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class img_has_alt extends brickfield_accessibility_test { + + /** @var int The default severity code for this test. */ + public $defaultseverity = \tool_brickfield\local\htmlchecker\brickfield_accessibility::BA_TEST_SEVERE; + + /** + * The main check function. This is called by the parent class to actually check content. + */ + public function check(): void { + foreach ($this->get_all_elements('img') as $img) { + if (!$img->hasAttribute('alt') || ($img->getAttribute('alt') == '') || ($img->getAttribute('alt') == ' ')) { + if (!($img->hasAttribute('role') && ($img->getAttribute('role') == 'presentation'))) { + $this->add_report($img); + } + } + } + } +} diff --git a/admin/tool/brickfield/classes/local/htmlchecker/common/checks/img_with_map_has_use_map.php b/admin/tool/brickfield/classes/local/htmlchecker/common/checks/img_with_map_has_use_map.php new file mode 100644 index 0000000000000..7f17fbb4b22ac --- /dev/null +++ b/admin/tool/brickfield/classes/local/htmlchecker/common/checks/img_with_map_has_use_map.php @@ -0,0 +1,56 @@ +. + +namespace tool_brickfield\local\htmlchecker\common\checks; + +use tool_brickfield\local\htmlchecker\common\brickfield_accessibility_test; + +/** + * Brickfield accessibility HTML checker library. + * + * All img elements with a valid usemap attribute has a matching .This registration process allows you to use the Brickfield accessibility toolkit freemium version for your registered Moodle site.
This usage is subject to the terms and conditions as stated here and which you agree to, by using this product.
'; +$string['release'] = 'Moodle release ({$a})'; +$string['secretkey'] = 'API key'; +$string['secretkey_help'] = 'This code is received by email after registration.'; +$string['sendfollowinginfo'] = 'The following information will be sent to contribute to overall statistics only. It will not be made public on any central listing.
{$a}'; +$string['sitehash'] = 'Secret key'; +$string['sitehash_help'] = 'This code is received by email after registration.'; +$string['sitename'] = 'Site name'; +$string['sitename_help'] = 'The name of the site'; +$string['siteurl'] = 'Site URL'; +$string['siteurl_help'] = 'The URL of the site'; +$string['usedifferentemail'] = 'Use different email'; +$string['usersmobileregistered'] = 'Number of users with registered mobile devices ({$a})'; +$string['validationerror'] = 'Registration key validation has failed. Check that your keys are correct.'; + +// Tool section. +$string['activityresults:pluginname'] = 'Activity breakdown accessibility tool report'; +$string['activityresults:toolname'] = 'Activity breakdown summary'; +$string['activityresults:toolshortname'] = 'Activity breakdown'; +$string['advanced:pluginname'] = 'Advanced accessibility tool report'; +$string['advanced:toolname'] = 'Advanced summary'; +$string['advanced:toolshortname'] = 'Advanced'; +$string['checktyperesults:pluginname'] = 'Content types accessibility tool report'; +$string['checktyperesults:toolname'] = 'Content types summary'; +$string['checktyperesults:toolshortname'] = 'Content types'; +$string['errors:pluginname'] = 'Error list accessibility tool'; +$string['errors:toolname'] = 'Error list summary'; +$string['errors:toolshortname'] = 'Error list'; +$string['printable:pluginname'] = 'Summary report accessibility tool report'; +$string['printable:toolname'] = 'Summary report'; +$string['printable:toolshortname'] = 'Summary report'; +$string['printable:downloadpdf'] = 'Download PDF'; +$string['printable:printreport'] = 'Printable report'; +$string['error:nocoursespecified'] = 'This summary report requires a valid courseid.
Please access the accessibility toolkit from within a course, by using its Actions menu administration link to the Accessibility toolkit, which will then supply this required courseid.
'; +$string['pdf:filename'] = 'Brickfield_Summaryreport_CourseID-{$a}'; + +// Advanced page. +$string['bannercontentone'] = 'The Enterprise Accessibility Toolkit has a full set of features to help your organisation improve accessibility of your courses. {$a} to book a free demo of the advanced features.'; +$string['bannercontenttwo'] = 'Build an effective and inclusive teaching and learning platform by Finding content that does not meet the guidelines, Fixing the issues and Future-proofing your Moodle course content with accessible files, editor and enhanced features.'; +$string['bannerheadingone'] = 'Upgrade to the Enterprise Accessibility Toolkit'; +$string['contactus'] = 'Contact us'; +$string['buttonone'] = 'Get a Free Demo'; +$string['contentone'] = 'Automatically evaluate your course content and assessments for accessibility issues.'; +$string['contenttwo'] = 'Bulk update unclear or missing text for web links, image descriptions and video subtitles.'; +$string['contentthree'] = 'Provide your students with content in accessible formats including Audio, ePub and Electronic Braille.'; +$string['contentfour'] = 'Identify which activities have the most accessibility issues to prioritise effort.'; +$string['contentfive'] = 'Automatically fix out of date HTML tags.'; +$string['contentsix'] = 'Provide teachers with just in time tips for creating better content.'; +$string['footerheading'] = 'Footer section'; +$string['headingone'] = 'Evaluate content'; +$string['headingtwo'] = 'Remediation'; +$string['headingthree'] = 'Accessible file formats'; +$string['headingfour'] = 'Focus effort'; +$string['headingfive'] = 'HTML fixes'; +$string['headingsix'] = 'Performance support'; diff --git a/admin/tool/brickfield/lib.php b/admin/tool/brickfield/lib.php new file mode 100644 index 0000000000000..bde66b289f9ab --- /dev/null +++ b/admin/tool/brickfield/lib.php @@ -0,0 +1,88 @@ +. + +/** + * This file contains hooks and callbacks needed for the accessibility toolkit. + * + * @package tool_brickfield + * @category admin + * @copyright 2020 Brickfield Education Labs, https://www.brickfield.ie - Author: Karen Holland + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +use tool_brickfield\accessibility; +use tool_brickfield\manager; +use tool_brickfield\registration; + +/** + * This function extends the navigation with the report items + * + * @param navigation_node $navigation The navigation node to extend + * @param stdClass $course The course to object for the report + * @param context $context The context of the course + * @throws coding_exception + * @throws moodle_exception + */ +function tool_brickfield_extend_navigation_course(\navigation_node $navigation, \stdClass $course, \context $context) { + if (!accessibility::is_accessibility_enabled()) { + // The feature has been explicitly disabled. + return; + } + + if (!has_capability(accessibility::get_capability_name('viewcoursetools'), $context)) { + // The user does not have the capability to view the course tools. + return; + } + + // Display in the navigation if the user has site:config ability, or if the site is registered. + $enabled = has_capability('moodle/site:config', \context_system::instance()); + $enabled = $enabled || (new registration())->toolkit_is_active(); + if (!$enabled) { + return; + } + + $url = new moodle_url(accessibility::get_plugin_url(), ['courseid' => $course->id]); + $navigation->add( + get_string('pluginname', manager::PLUGINNAME), + $url, + navigation_node::TYPE_SETTING, + null, + null, + new pix_icon('i/report', '') + ); +} + +/** + * Get icon mapping for font-awesome. + * @return string[] + */ +function tool_brickfield_get_fontawesome_icon_map() { + return [ + manager::PLUGINNAME . ':f/award' => 'fa-tachometer', + manager::PLUGINNAME . ':f/done' => 'fa-check-circle-o', + manager::PLUGINNAME . ':f/done2' => 'fa-check-square-o', + manager::PLUGINNAME . ':f/error' => 'fa-times-circle-o', + manager::PLUGINNAME . ':f/find' => 'fa-bar-chart', + manager::PLUGINNAME . ':f/total' => 'fa-calculator', + manager::PLUGINNAME . ':f/form' => 'fa-pencil-square-o', + manager::PLUGINNAME . ':f/image' => 'fa-image', + manager::PLUGINNAME . ':f/layout' => 'fa-th-large', + manager::PLUGINNAME . ':f/link' => 'fa-link', + manager::PLUGINNAME . ':f/media' => 'fa-play-circle-o', + manager::PLUGINNAME . ':f/table' => 'fa-table', + manager::PLUGINNAME . ':f/text' => 'fa-font', + ]; +} diff --git a/admin/tool/brickfield/pix/b/pdfdown.svg b/admin/tool/brickfield/pix/b/pdfdown.svg new file mode 100644 index 0000000000000..3313da6cae97b --- /dev/null +++ b/admin/tool/brickfield/pix/b/pdfdown.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/admin/tool/brickfield/pix/brickfield-logo-medium.png b/admin/tool/brickfield/pix/brickfield-logo-medium.png new file mode 100644 index 0000000000000..fba9b26573a56 Binary files /dev/null and b/admin/tool/brickfield/pix/brickfield-logo-medium.png differ diff --git a/admin/tool/brickfield/pix/brickfield-logo-small.png b/admin/tool/brickfield/pix/brickfield-logo-small.png new file mode 100644 index 0000000000000..2ab479146bc97 Binary files /dev/null and b/admin/tool/brickfield/pix/brickfield-logo-small.png differ diff --git a/admin/tool/brickfield/pix/brickfield-logo.png b/admin/tool/brickfield/pix/brickfield-logo.png new file mode 100644 index 0000000000000..157fb92226a5f Binary files /dev/null and b/admin/tool/brickfield/pix/brickfield-logo.png differ diff --git a/admin/tool/brickfield/pix/i/Brickfield-logo-black.png b/admin/tool/brickfield/pix/i/Brickfield-logo-black.png new file mode 100644 index 0000000000000..bd7f7db7a1e32 Binary files /dev/null and b/admin/tool/brickfield/pix/i/Brickfield-logo-black.png differ diff --git a/admin/tool/brickfield/pix/i/Brickfield-logo-white.png b/admin/tool/brickfield/pix/i/Brickfield-logo-white.png new file mode 100644 index 0000000000000..937380bbb60f7 Binary files /dev/null and b/admin/tool/brickfield/pix/i/Brickfield-logo-white.png differ diff --git a/admin/tool/brickfield/pix/i/analytics-custom.png b/admin/tool/brickfield/pix/i/analytics-custom.png new file mode 100644 index 0000000000000..c8b04eae8e6f1 Binary files /dev/null and b/admin/tool/brickfield/pix/i/analytics-custom.png differ diff --git a/admin/tool/brickfield/pix/i/brickfield_logo.jpeg b/admin/tool/brickfield/pix/i/brickfield_logo.jpeg new file mode 100644 index 0000000000000..55f94c653b2dd Binary files /dev/null and b/admin/tool/brickfield/pix/i/brickfield_logo.jpeg differ diff --git a/admin/tool/brickfield/pix/i/chart-network-custom.png b/admin/tool/brickfield/pix/i/chart-network-custom.png new file mode 100644 index 0000000000000..97e52593cde3c Binary files /dev/null and b/admin/tool/brickfield/pix/i/chart-network-custom.png differ diff --git a/admin/tool/brickfield/pix/i/edit-custom.png b/admin/tool/brickfield/pix/i/edit-custom.png new file mode 100644 index 0000000000000..cc7569fd35f45 Binary files /dev/null and b/admin/tool/brickfield/pix/i/edit-custom.png differ diff --git a/admin/tool/brickfield/pix/i/file-edit-custom.png b/admin/tool/brickfield/pix/i/file-edit-custom.png new file mode 100644 index 0000000000000..86e4c3342841a Binary files /dev/null and b/admin/tool/brickfield/pix/i/file-edit-custom.png differ diff --git a/admin/tool/brickfield/pix/i/hands-helping-custom.png b/admin/tool/brickfield/pix/i/hands-helping-custom.png new file mode 100644 index 0000000000000..ab732361431b4 Binary files /dev/null and b/admin/tool/brickfield/pix/i/hands-helping-custom.png differ diff --git a/admin/tool/brickfield/pix/i/lightbulb-custom.png b/admin/tool/brickfield/pix/i/lightbulb-custom.png new file mode 100644 index 0000000000000..b4ac5b416091d Binary files /dev/null and b/admin/tool/brickfield/pix/i/lightbulb-custom.png differ diff --git a/admin/tool/brickfield/pix/i/search-plus-custom.png b/admin/tool/brickfield/pix/i/search-plus-custom.png new file mode 100644 index 0000000000000..23862c3c73754 Binary files /dev/null and b/admin/tool/brickfield/pix/i/search-plus-custom.png differ diff --git a/admin/tool/brickfield/pix/i/tools-custom.png b/admin/tool/brickfield/pix/i/tools-custom.png new file mode 100644 index 0000000000000..a14b432dea43e Binary files /dev/null and b/admin/tool/brickfield/pix/i/tools-custom.png differ diff --git a/admin/tool/brickfield/pix/i/wand-magic-custom.png b/admin/tool/brickfield/pix/i/wand-magic-custom.png new file mode 100644 index 0000000000000..d7c1057c596e9 Binary files /dev/null and b/admin/tool/brickfield/pix/i/wand-magic-custom.png differ diff --git a/admin/tool/brickfield/pix/moodle-logo.png b/admin/tool/brickfield/pix/moodle-logo.png new file mode 100644 index 0000000000000..2da7b4af1b8a6 Binary files /dev/null and b/admin/tool/brickfield/pix/moodle-logo.png differ diff --git a/admin/tool/brickfield/pix/pdf/check-square-regular.svg b/admin/tool/brickfield/pix/pdf/check-square-regular.svg new file mode 100644 index 0000000000000..f17eed7250be9 --- /dev/null +++ b/admin/tool/brickfield/pix/pdf/check-square-regular.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/admin/tool/brickfield/pix/pdf/font-solid.svg b/admin/tool/brickfield/pix/pdf/font-solid.svg new file mode 100644 index 0000000000000..48ad54dc2b8e6 --- /dev/null +++ b/admin/tool/brickfield/pix/pdf/font-solid.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/admin/tool/brickfield/pix/pdf/image-regular.svg b/admin/tool/brickfield/pix/pdf/image-regular.svg new file mode 100644 index 0000000000000..1d85822473c2e --- /dev/null +++ b/admin/tool/brickfield/pix/pdf/image-regular.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/admin/tool/brickfield/pix/pdf/link.png b/admin/tool/brickfield/pix/pdf/link.png new file mode 100644 index 0000000000000..12dd1876cbd2e Binary files /dev/null and b/admin/tool/brickfield/pix/pdf/link.png differ diff --git a/admin/tool/brickfield/pix/pdf/logo-black.png b/admin/tool/brickfield/pix/pdf/logo-black.png new file mode 100644 index 0000000000000..abbc392e7e552 Binary files /dev/null and b/admin/tool/brickfield/pix/pdf/logo-black.png differ diff --git a/admin/tool/brickfield/pix/pdf/play-circle-regular.svg b/admin/tool/brickfield/pix/pdf/play-circle-regular.svg new file mode 100644 index 0000000000000..452a779192792 --- /dev/null +++ b/admin/tool/brickfield/pix/pdf/play-circle-regular.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/admin/tool/brickfield/pix/pdf/table-solid.svg b/admin/tool/brickfield/pix/pdf/table-solid.svg new file mode 100644 index 0000000000000..79e417b49cd8f --- /dev/null +++ b/admin/tool/brickfield/pix/pdf/table-solid.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/admin/tool/brickfield/pix/pdf/tachometer-alt-solid.svg b/admin/tool/brickfield/pix/pdf/tachometer-alt-solid.svg new file mode 100644 index 0000000000000..857928212e255 --- /dev/null +++ b/admin/tool/brickfield/pix/pdf/tachometer-alt-solid.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/admin/tool/brickfield/pix/pdf/th-large-solid.svg b/admin/tool/brickfield/pix/pdf/th-large-solid.svg new file mode 100644 index 0000000000000..0618c42ed447f --- /dev/null +++ b/admin/tool/brickfield/pix/pdf/th-large-solid.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/admin/tool/brickfield/pix/pdf/times-circle-regular.svg b/admin/tool/brickfield/pix/pdf/times-circle-regular.svg new file mode 100644 index 0000000000000..3b489cb7bcc93 --- /dev/null +++ b/admin/tool/brickfield/pix/pdf/times-circle-regular.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/admin/tool/brickfield/registration.php b/admin/tool/brickfield/registration.php new file mode 100644 index 0000000000000..f63e5cd321c41 --- /dev/null +++ b/admin/tool/brickfield/registration.php @@ -0,0 +1,91 @@ +. + +/** + * Registration configuration for the Brickfield too. + * + * @package tool_brickfield + * @author 2020 JM Tomas+ {{# str }}contentone, tool_brickfield{{/ str }} +
++ {{# str }}contenttwo, tool_brickfield{{/ str }} +
++ {{# str }}contentthree, tool_brickfield{{/ str }} +
++ {{# str }}contentfour, tool_brickfield{{/ str }} +
++ {{# str }}contentfive, tool_brickfield{{/ str }} +
++ {{# str }}contentsix, tool_brickfield{{/ str }} +
+{{tableheading1}} | +{{tableheading2}} | +{{tableheading3}} | +{{tableheading4}} | +{{{tableheading5}}} | +
---|---|---|---|---|
{{activity}} | +{{check}} | +{{{edit}}} | +{{line}} | +{{html}} | +
{{{noerrorsfound}}}
diff --git a/admin/tool/brickfield/templates/tabtree.mustache b/admin/tool/brickfield/templates/tabtree.mustache new file mode 100644 index 0000000000000..f6b075c08965f --- /dev/null +++ b/admin/tool/brickfield/templates/tabtree.mustache @@ -0,0 +1,49 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, seeBecause many users of screen readers use links to ' . + 'navigate the page, providing links with no text (or with images that have empty \'alt\' attributes and no other ' . + 'readable text) hinders these users.
', $output['a_must_contain_text']['description']); + } + + /** + * Test get_category_courseids(). + * + * @throws \dml_exception + */ + public function test_get_category_courseids() { + $this->resetAfterTest(); + $object = new accessibility(); + $category = $this->getDataGenerator()->create_category(); + $course = $this->getDataGenerator()->create_course((object)['category' => $category->id]); + + $output = $object->get_category_courseids($category->id); + $this->assertEquals($output[0], $course->id); + } +} diff --git a/admin/tool/brickfield/tests/activityresults_test.php b/admin/tool/brickfield/tests/activityresults_test.php new file mode 100644 index 0000000000000..4366623f916fc --- /dev/null +++ b/admin/tool/brickfield/tests/activityresults_test.php @@ -0,0 +1,66 @@ +. + +namespace tool_brickfield\local\tool; + +/** + * Unit tests for {@activityresults tool_brickfield\local\tool\activityresults\tool}. + * + * @package tool_brickfield + * @copyright 2020 onward: Brickfield Education Labs, www.brickfield.ie + * @author Jay Churchward (jay.churchward@poetopensource.org) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class activityresults_test extends \advanced_testcase { + + public function test_toolname() { + $this->resetAfterTest(); + + $object = new activityresults(); + $output = $object->toolname(); + $this->assertEquals($output, 'Activity breakdown summary'); + } + + public function test_toolshortname() { + $this->resetAfterTest(); + + $object = new activityresults(); + $output = $object->toolshortname(); + $this->assertEquals($output, 'Activity breakdown'); + } + + public function test_pluginname() { + $this->resetAfterTest(); + + $object = new activityresults(); + $output = $object->pluginname(); + $this->assertEquals($output, 'activityresults'); + } + + public function test_get_output() { + $this->resetAfterTest(); + + $category = $this->getDataGenerator()->create_category(); + $filter = new filter(1, $category->id, 'tab', 3, 4); + $filter->courseids = []; + + $object = new activityresults(); + $object->set_filter($filter); + $output = $object->get_output(); + $this->assertIsString($output); + $this->assertStringContainsString('Results per activity :', $output); + } +} diff --git a/admin/tool/brickfield/tests/area_test.php b/admin/tool/brickfield/tests/area_test.php new file mode 100644 index 0000000000000..29db239f1efef --- /dev/null +++ b/admin/tool/brickfield/tests/area_test.php @@ -0,0 +1,240 @@ +. + +namespace tool_brickfield; + +/** + * Class tool_brickfield_area_testcase + * + * @package tool_brickfield + * @copyright 2020 onward: Brickfield Education Labs, https://www.brickfield.ie + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class area_test extends \advanced_testcase { + + /** + * Test for the area assign intro + */ + public function test_assign() { + $this->resetAfterTest(); + $course = $this->getDataGenerator()->create_course(); + $assign1 = $this->getDataGenerator()->create_module('assign', array( + 'course' => $course->id, 'name' => 'Test!', 'intro' => 'Here we go
', + 'introformat' => FORMAT_HTML)); + list($course1, $cm1) = get_course_and_cm_from_instance($assign1->id, 'assign'); + $assign2 = $this->getDataGenerator()->create_module('assign', array( + 'course' => SITEID, 'name' => 'Test2!', 'intro' => 'Something', + 'introformat' => FORMAT_MOODLE)); + list($course2, $cm2) = get_course_and_cm_from_instance($assign2->id, 'assign'); + + $c = new \tool_brickfield\local\areas\mod_assign\intro(); + $this->assertEquals('mod_assign', $c->get_component()); + $this->assertEquals('assign', $c->get_tablename()); + $resultsrs = $c->find_course_areas($course1->id); + $resultsrs2 = $c->find_course_areas($course2->id); + // Set up a results array from the recordset for easier testing. + $results = array_merge(self::array_from_recordset($resultsrs), self::array_from_recordset($resultsrs2)); + $this->assertEquals([ + (object)[ + 'type' => area_base::TYPE_FIELD, + 'contextid' => \context_module::instance($cm1->id)->id, + 'component' => $c->get_component(), + 'tablename' => $c->get_tablename(), + 'fieldorarea' => $c->get_fieldname(), + 'itemid' => $assign1->id, + 'cmid' => $cm1->id, + 'courseid' => $course1->id, + 'content' => $assign1->intro, + ], + (object)[ + 'type' => area_base::TYPE_FIELD, + 'contextid' => \context_module::instance($cm2->id)->id, + 'component' => $c->get_component(), + 'tablename' => $c->get_tablename(), + 'fieldorarea' => $c->get_fieldname(), + 'itemid' => $assign2->id, + 'cmid' => $cm2->id, + 'courseid' => $course2->id, + 'content' => $assign2->intro, + ] + ], $results); + + // Emulate the course_module_updated event. + $event = \core\event\course_module_updated::create_from_cm($cm1); + $relevantresultsrs = $c->find_relevant_areas($event); + // Set up a relevantresults array from the recordset for easier testing. + $relevantresults = self::array_from_recordset($relevantresultsrs); + $this->assertEquals([$results[0]], $relevantresults); + } + + /** + * Test for the area questiontext + */ + public function test_questiontext() { + $this->resetAfterTest(); + /** @var \core_question_generator $generator */ + $generator = $this->getDataGenerator()->get_plugin_generator('core_question'); + list($category1, $course1, $qcat1, $questions1) = $generator->setup_course_and_questions('course'); + list($category2, $course2, $qcat2, $questions2) = $generator->setup_course_and_questions('category'); + list($category3, $course3, $qcat3, $questions3) = $generator->setup_course_and_questions('system'); + + $c = new \tool_brickfield\local\areas\core_question\questiontext(); + $results1 = self::array_from_recordset($c->find_course_areas($course1->id)); + $results2 = self::array_from_recordset($c->find_course_areas($course2->id)); + $results3 = self::array_from_recordset($c->find_course_areas($course3->id)); + $results4 = self::array_from_recordset($c->find_system_areas()); + // Set up a results array from the recordset for easier testing. + + $this->assertCount(2, $results1); + $this->assertCount(0, $results2); + $this->assertCount(0, $results3); + $this->assertCount(4, $results4); + + // Validate the contexts, courseid and categoryid of the returned results. + $this->assertEquals(\context_course::instance($course1->id)->id, $results1[0]->contextid); + $this->assertEquals($course1->id, $results1[0]->courseid); + $this->assertEmpty($results1[0]->categoryid); + $this->assertEquals(\context_coursecat::instance($category2->id)->id, $results4[0]->contextid); + $this->assertEquals(SITEID, $results4[0]->courseid); + $this->assertEquals($category2->id, $results4[0]->categoryid); + $this->assertEquals(\context_system::instance()->id, $results4[2]->contextid); + $this->assertEmpty($results4[2]->categoryid); + $this->assertEquals(SITEID, $results4[2]->courseid); + // Results4 should contain id's for questions 1 and 2. + $this->assertTrue(($questions3[0]->id == $results4[2]->itemid) || ($questions3[0]->id == $results4[3]->itemid)); + $this->assertTrue(($questions3[1]->id == $results4[2]->itemid) || ($questions3[1]->id == $results4[3]->itemid)); + + // Emulate the question_created event. + $event = \core\event\question_created::create_from_question_instance($questions1[1], + \context_course::instance($course1->id)); + $relevantresultsrs = $c->find_relevant_areas($event); + // Set up a relevantresults array from the recordset for easier testing. + $relevantresults = self::array_from_recordset($relevantresultsrs); + $this->assertEquals([$results1[1]], $relevantresults); + } + + /** + * test for the area questionanswers + */ + public function test_questionanswers() { + global $DB; + + $this->resetAfterTest(); + /** @var \core_question_generator $generator */ + $generator = $this->getDataGenerator()->get_plugin_generator('core_question'); + $course = $this->getDataGenerator()->create_course(); + $cat = $generator->create_question_category(['contextid' => \context_course::instance($course->id)->id]); + $question1 = $generator->create_question('multichoice', null, + ['name' => 'Example multichoice question', 'category' => $cat->id]); + $question2 = $generator->create_question('numerical', null, + ['name' => 'Example numerical question', 'category' => $cat->id]); + + $dbanswers = $DB->get_records('question_answers', [] , 'id'); + $this->assertNotEmpty(count($dbanswers)); + + $c = new \tool_brickfield\local\areas\core_question\questionanswers(); + $resultsrs = $c->find_course_areas($course->id); + $results = self::array_from_recordset($resultsrs); + + // There will be the same number of results as the number of records in the question_answers table. + $this->assertEquals(count($dbanswers), count($results)); + + // Emulate the question_updated event. + $event = \core\event\question_updated::create_from_question_instance($question1, + \context_course::instance($course->id)); + $relevantresultsrs = $c->find_relevant_areas($event); + // Set up a relevantresults array from the recordset for easier testing. + $relevantresults = self::array_from_recordset($relevantresultsrs); + + $dbanswers = array_values($DB->get_records('question_answers', ['question' => $question1->id], 'id')); + $this->assertEquals(count($dbanswers), count($relevantresults)); + foreach ($dbanswers as $i => $dbanswer) { + $relevantresult = $relevantresults[$i]; + $this->assertEquals($dbanswer->answer, $relevantresult->content); + $this->assertEquals('question', $relevantresult->reftable); + $this->assertEquals($question1->id, $relevantresult->refid); + $this->assertEquals($dbanswer->id, $relevantresult->itemid); + } + } + + /** + * Test for the areas choice intro and choice options + */ + public function test_choice() { + global $DB; + $this->resetAfterTest(); + $course = $this->getDataGenerator()->create_course(); + $choice1 = $this->getDataGenerator()->create_module('choice', [ + 'course' => $course->id, 'option' => ['fried rice', 'spring rolls', 'sweet and sour pork'] + ]); + list($course1, $cm1) = get_course_and_cm_from_instance($choice1->id, 'choice'); + $choice2 = $this->getDataGenerator()->create_module('choice', [ + 'course' => $course->id, 'option' => ['blue', 'red'] + ]); + list($course2, $cm2) = get_course_and_cm_from_instance($choice2->id, 'choice'); + + // Testing the choice intro. + $c = new \tool_brickfield\local\areas\mod_choice\intro(); + $resultsrs = $c->find_course_areas($course->id); + // Set up a results array from the recordset for easier testing. + $results = self::array_from_recordset($resultsrs); + + $this->assertCount(2, $results); + $this->assertEquals($cm1->id, $results[0]->cmid); + $this->assertEquals($choice2->id, $results[1]->itemid); + + // Emulate the course_module_created event. + $event = \core\event\course_module_created::create_from_cm($cm1); + $relevantresultsrs = $c->find_relevant_areas($event); + $relevantresults = self::array_from_recordset($relevantresultsrs); + $this->assertEquals([$results[0]], $relevantresults); + + // Testing the choice options. + $c = new \tool_brickfield\local\areas\mod_choice\option(); + $resultsrs = $c->find_course_areas($course->id); + // Set up a results array from the recordset for easier testing. + $results = self::array_from_recordset($resultsrs); + + $this->assertCount(5, $results); + $this->assertEquals($cm2->id, $results[3]->cmid); + $this->assertEquals('choice_options', $results[3]->tablename); + $this->assertEquals('choice', $results[3]->reftable); + $this->assertEquals($choice2->id, $results[3]->refid); + $options3 = $DB->get_records_menu('choice_options', ['choiceid' => $choice2->id], 'id', 'text,id'); + $this->assertEquals($options3['blue'], $results[3]->itemid); + $this->assertEquals('blue', $results[3]->content); + + // Emulate the course_module_updated event. + $event = \core\event\course_module_updated::create_from_cm($cm2); + $relevantresultsrs = $c->find_relevant_areas($event); + $relevantresults = self::array_from_recordset($relevantresultsrs); + $this->assertEquals([$results[3], $results[4]], $relevantresults); + } + + /** + * Array from recordset. + * @param \moodle_recordset $rs + * @return array + */ + private static function array_from_recordset($rs) { + $records = []; + foreach ($rs as $record) { + $records[] = $record; + } + $rs->close(); + return $records; + } +} diff --git a/admin/tool/brickfield/tests/checktyperesults_test.php b/admin/tool/brickfield/tests/checktyperesults_test.php new file mode 100644 index 0000000000000..21252a534cb22 --- /dev/null +++ b/admin/tool/brickfield/tests/checktyperesults_test.php @@ -0,0 +1,67 @@ +. + +namespace tool_brickfield\local\tool; + +/** + * Unit tests for {@checktyperesults tool_brickfield\local\tool\checktyperesults\tool}. + * + * @package tool_brickfield + * @copyright 2020 onward: Brickfield Education Labs, www.brickfield.ie + * @author Jay Churchward (jay.churchward@poetopensource.org) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class checktyperesults_test extends \advanced_testcase { + + public function test_toolname() { + $this->resetAfterTest(); + + $object = new checktyperesults(); + $output = $object->toolname(); + $this->assertEquals($output, 'Content types summary'); + } + + public function test_toolshortname() { + $this->resetAfterTest(); + + $object = new checktyperesults(); + $output = $object->toolshortname(); + $this->assertEquals($output, 'Content types'); + } + + public function test_pluginname() { + $this->resetAfterTest(); + + $object = new checktyperesults(); + $output = $object->pluginname(); + $this->assertEquals($output, 'checktyperesults'); + } + + public function test_get_output() { + $this->resetAfterTest(); + $category = $this->getDataGenerator()->create_category(); + + $filter = new filter(1, $category->id, 'checktyperesults', 3, 4); + $filter->courseids = []; + + $object = new checktyperesults(); + $object->set_filter($filter); + $object->get_data(); + $output = $object->get_output(); + $this->assertIsString($output); + $this->assertStringContainsString('Results per content type :', $output); + } +} diff --git a/admin/tool/brickfield/tests/errors_test.php b/admin/tool/brickfield/tests/errors_test.php new file mode 100644 index 0000000000000..5299ab0403300 --- /dev/null +++ b/admin/tool/brickfield/tests/errors_test.php @@ -0,0 +1,66 @@ +. + +namespace tool_brickfield\local\tool; + +/** + * Unit tests for {@errors tool_brickfield\local\tool\errors\tool}. + * + * @package tool_brickfield + * @copyright 2020 onward: Brickfield Education Labs, www.brickfield.ie + * @author Jay Churchward (jay.churchward@poetopensource.org) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class errors_test extends \advanced_testcase { + + public function test_toolname() { + $this->resetAfterTest(); + + $object = new errors(); + $output = $object->toolname(); + $this->assertEquals($output, 'Error list summary'); + } + + public function test_toolshortname() { + $this->resetAfterTest(); + + $object = new errors(); + $output = $object->toolshortname(); + $this->assertEquals($output, 'Error list'); + } + + public function test_pluginname() { + $this->resetAfterTest(); + + $object = new errors(); + $output = $object->pluginname(); + $this->assertEquals($output, 'errors'); + } + + public function test_get_output() { + $this->resetAfterTest(); + $category = $this->getDataGenerator()->create_category(); + + $filter = new filter(1, $category->id, 'tab', 3, 4); + $filter->courseids = []; + + $object = new errors(); + $object->set_filter($filter); + $output = $object->get_output(); + $this->assertIsString($output); + $this->assertStringContainsString('Error details :', $output); + } +} diff --git a/admin/tool/brickfield/tests/filters_test.php b/admin/tool/brickfield/tests/filters_test.php new file mode 100644 index 0000000000000..f68171b19b00c --- /dev/null +++ b/admin/tool/brickfield/tests/filters_test.php @@ -0,0 +1,216 @@ +. + +namespace tool_brickfield; + +use tool_brickfield\local\tool\filter; + +/** + * Unit tests for {@filter tool_brickfield\local\tool\filter}. + * + * @package tool_brickfield + * @copyright 2020 onward: Brickfield Education Labs, www.brickfield.ie + * @author Jay Churchward (jay.churchward@poetopensource.org) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class filter_test extends \advanced_testcase { + public function test_constructor() { + $this->resetAfterTest(); + + // Variables. + $courseid = 1; + $categoryid = 2; + $tab = 'tab'; + $page = 3; + $perpage = 4; + $url = 'url'; + + // Test responses. + $object = new filter(); + $this->assertEquals($object->courseid, 0); + $this->assertEquals($object->categoryid, 0); + $this->assertEquals($object->tab, ''); + $this->assertEquals($object->page, 0); + $this->assertEquals($object->perpage, 0); + $this->assertEquals($object->url, ''); + + $object = new filter($courseid, $categoryid, $tab, $page, $perpage, $url); + $this->assertEquals($object->courseid, $courseid); + $this->assertEquals($object->categoryid, $categoryid); + $this->assertEquals($object->tab, $tab); + $this->assertEquals($object->page, $page); + $this->assertEquals($object->perpage, $perpage); + $this->assertEquals($object->url, $url); + } + + public function test_get_course_sql() { + $this->resetAfterTest(); + $object = new filter(); + + $output = $object->get_course_sql(); + + $this->assertIsArray($output); + $this->assertEquals($output[0], ''); + + $object = $this->create_object_with_params(); + $output = $object->get_course_sql(); + + $this->assertEquals($output[0], ' AND (courseid = ?)'); + $this->assertEquals($output[1][0], $object->courseid); + + } + + public function test_validate_filters() { + $this->resetAfterTest(); + // Variables. + $courseid = 0; + $categoryid = 2; + $tab = 'tab'; + $page = 3; + $perpage = 4; + $url = 'url'; + $object = new filter(); + + $output = $object->validate_filters(); + $this->assertTrue($output); + + $object = $this->create_object(); + $output = $object->validate_filters(); + $this->assertTrue($output); + + $object = new filter($courseid, $categoryid, $tab, $page, $perpage); + $output = $object->validate_filters(); + $this->assertFalse($output); + + $category = $this->getDataGenerator()->create_category(); + + $object = new filter($courseid, $category->id, $tab, $page, $perpage); + $output = $object->validate_filters(); + $this->assertFalse($output); + } + + public function test_has_course_filters() { + $this->resetAfterTest(); + + $object = new filter(); + $output = $object->has_course_filters(); + $this->assertFalse($output); + + $object = $this->create_object(); + $output = $object->has_course_filters(); + $this->assertTrue($output); + } + + public function test_has_capability_in_context() { + global $DB; + + $this->resetAfterTest(); + + $object = $this->create_object_with_params(); + $capability = accessibility::get_capability_name('viewcoursetools'); + $output = $object->has_capability_in_context($capability, \context_system::instance()); + $this->assertFalse($output); + + $output = $object->has_capability_in_context($capability, \context_coursecat::instance($object->categoryid)); + $this->assertFalse($output); + + $output = $object->has_capability_in_context($capability, \context_course::instance($object->courseid)); + $this->assertFalse($output); + + $course = $this->getDataGenerator()->create_course(); + $user = $this->getDataGenerator()->create_and_enrol($course, 'teacher'); + $this->setUser($user); + + $output = $object->has_capability_in_context($capability, \context_system::instance()); + $this->assertFalse($output); + + $output = $object->has_capability_in_context($capability, \context_coursecat::instance($object->categoryid)); + $this->assertFalse($output); + + $output = $object->has_capability_in_context($capability, \context_course::instance($course->id)); + $this->assertTrue($output); + + $teacherrole = $DB->get_record('role', ['shortname' => 'teacher']); + $categorycontext = \context_coursecat::instance($object->categoryid); + $this->getDataGenerator()->role_assign($teacherrole->id, $user->id, $categorycontext->id); + + $output = $object->has_capability_in_context($capability, $categorycontext); + $this->assertTrue($output); + } + + public function test_get_errormessage() { + $this->resetAfterTest(); + // Variables. + $courseid = 0; + $categoryid = 2; + $tab = 'tab'; + $page = 3; + $perpage = 4; + $url = 'url'; + + $object = new filter(); + $output = $object->get_errormessage(); + $this->assertNull($output); + + $object = new filter($courseid, $categoryid, $tab, $page, $perpage); + $object->validate_filters(); + $output = $object->get_errormessage(); + $this->assertEquals($output, 'Invalid category, please check your input'); + + $category = $this->getDataGenerator()->create_category(); + $object = new filter($courseid, $category->id, $tab, $page, $perpage); + $object->validate_filters(); + $output = $object->get_errormessage(); + $this->assertEquals($output, 'No courses found for category ' . $category->id); + } + + /** + * Create a filter object and return it. + * @return filter + */ + private function create_object() { + // Variables. + $courseid = 1; + $categoryid = 2; + $tab = 'tab'; + $page = 3; + $perpage = 4; + $url = 'url'; + + $object = new filter($courseid, $categoryid, $tab, $page, $perpage); + + return $object; + } + + /** + * Create a filter object with some parameters and return it. + * @return filter + */ + private function create_object_with_params() { + // Variables. + $tab = 'tab'; + $page = 3; + $perpage = 4; + $url = 'url'; + + $category = $this->getDataGenerator()->create_category(); + $course = $this->getDataGenerator()->create_course((object)['category' => $category->id]); + + $object = new filter($course->id, $category->id, $tab, $page, $perpage); + + return $object; + } +} diff --git a/admin/tool/brickfield/tests/generator/mock_brickfieldconnect.php b/admin/tool/brickfield/tests/generator/mock_brickfieldconnect.php new file mode 100644 index 0000000000000..46c829073169f --- /dev/null +++ b/admin/tool/brickfield/tests/generator/mock_brickfieldconnect.php @@ -0,0 +1,70 @@ +. + +/** + * PHPUnit tool_brickfield tests + * + * @package tool_brickfield + * @copyright 2020 onward: Brickfield Education Labs, www.brickfield.ie + * @author Mike Churchward (mike@brickfieldlabs.ie) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace tool_brickfield; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Mock brickfield connect. + */ +class mock_brickfieldconnect extends brickfieldconnect { + + /** + * Valid api key. + */ + const VALIDAPIKEY = '123456789012345678901234567890ab'; + + /** + * Valid secret key. + */ + const VALIDSECRETKEY = 'ab123456789012345678901234567890'; + + /** @var string api key. */ + protected $apikey = ''; + + /** @var string Secret key. */ + protected $secretkey = ''; + + /** + * Is registered. + * @return bool is registered + */ + public function is_registered(): bool { + return ($this->apikey == self::VALIDAPIKEY) && ($this->secretkey == self::VALIDSECRETKEY); + } + + /** + * Update Registration. + * @param string $apikey + * @param string $secretkey + * @return bool + */ + public function update_registration(string $apikey, string $secretkey): bool { + $this->apikey = $apikey; + $this->secretkey = $secretkey; + return $this->is_registered(); + } +} diff --git a/admin/tool/brickfield/tests/generator/mock_registration.php b/admin/tool/brickfield/tests/generator/mock_registration.php new file mode 100644 index 0000000000000..05f560f58c964 --- /dev/null +++ b/admin/tool/brickfield/tests/generator/mock_registration.php @@ -0,0 +1,72 @@ +. + +/** + * PHPUnit tool_brickfield tests + * + * @package tool_brickfield + * @copyright 2020 onward: Brickfield Education Labs, www.brickfield.ie + * @author Mike Churchward (mike@brickfieldlabs.ie) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace tool_brickfield; + +defined('MOODLE_INTERNAL') || die(); + +global $CFG; +require_once($CFG->dirroot . '/admin/tool/brickfield/tests/generator/mock_brickfieldconnect.php'); + +/** + * Mock registration. + */ +class mock_registration extends registration { + /** + * Get registration connection. + * @return brickfieldconnect + */ + protected function get_registration_connection(): brickfieldconnect { + return new mock_brickfieldconnect(); + } + + /** + * Is not entered. + * @return bool + */ + public function is_not_entered() { + return $this->status_is_not_entered(); + } + + /** + * Invalidate validation time. + * @return int + * @throws \dml_exception + */ + public function invalidate_validation_time() { + $this->set_validation_time(time() - (7 * 24 * 60 * 60)); + return $this->get_validation_time(); + } + + /** + * Invalidate summary time. + * @return int + * @throws \dml_exception + */ + public function invalidate_summary_time() { + $this->set_summary_time(time() - (7 * 24 * 60 * 60) - 1); + return $this->get_summary_time(); + } +} \ No newline at end of file diff --git a/admin/tool/brickfield/tests/local/areas/core_question/questionanswers_test.php b/admin/tool/brickfield/tests/local/areas/core_question/questionanswers_test.php new file mode 100644 index 0000000000000..30d7c842384bf --- /dev/null +++ b/admin/tool/brickfield/tests/local/areas/core_question/questionanswers_test.php @@ -0,0 +1,191 @@ +. + +namespace tool_brickfield\local\areas\core_question; + +/** + * Tests for questionanswer. + * + * @package tool_brickfield + * @copyright 2020 onward: Brickfield Education Labs, https://www.brickfield.ie + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class questionanswers_test extends \advanced_testcase { + + /** + * Set up before class. + */ + public static function setUpBeforeClass(): void { + global $CFG; + require_once($CFG->dirroot . '/mod/quiz/locallib.php'); + } + + /** + * Test find course areas. + */ + public function test_find_course_areas() { + $this->resetAfterTest(); + $this->setAdminUser(); + + $category = $this->getDataGenerator()->create_category(); + $course = $this->getDataGenerator()->create_course(['category' => $category->id]); + $coursecontext = \context_course::instance($course->id); + $catcontext = \context_coursecat::instance($category->id); + $generator = $this->getDataGenerator()->get_plugin_generator('core_question'); + $cat1 = $generator->create_question_category(['contextid' => $coursecontext->id]); + $question1 = $generator->create_question('multichoice', null, ['category' => $cat1->id]); + $question2 = $generator->create_question('multichoice', null, ['category' => $cat1->id]); + $questionanswers = new questionanswers(); + $rs = $questionanswers->find_course_areas($course->id); + $this->assertNotNull($rs); + + // Each multichoice question generated has four answers. So there should be eight records. + $count = 0; + foreach ($rs as $rec) { + $count++; + $this->assertEquals($coursecontext->id, $rec->contextid); + $this->assertEquals($course->id, $rec->courseid); + if ($count <= 4) { + $this->assertEquals($question1->id, $rec->refid); + } else { + $this->assertEquals($question2->id, $rec->refid); + } + } + $rs->close(); + $this->assertEquals(8, $count); + + // Add a question to a quiz in the course. + $quiz = $this->getDataGenerator()->create_module('quiz', ['course' => $course->id, 'name' => 'Quiz1']); + $quizmodule = get_coursemodule_from_instance('quiz', $quiz->id, $course->id); + $quizcontext = \context_module::instance($quizmodule->id); + + // Add a question to the quiz context. + $cat2 = $generator->create_question_category(['contextid' => $quizcontext->id]); + $question3 = $generator->create_question('multichoice', null, ['category' => $cat2->id]); + $rs2 = $questionanswers->find_course_areas($course->id); + $this->assertNotNull($rs2); + + // Each multichoice question generated has four answers. So there should be twelve records now. + $count = 0; + foreach ($rs2 as $rec) { + $count++; + if ($count <= 4) { + $this->assertEquals($coursecontext->id, $rec->contextid); + $this->assertEquals($course->id, $rec->courseid); + $this->assertEquals($question1->id, $rec->refid); + } else if ($count <= 8) { + $this->assertEquals($coursecontext->id, $rec->contextid); + $this->assertEquals($course->id, $rec->courseid); + $this->assertEquals($question2->id, $rec->refid); + } else { + $this->assertEquals($quizcontext->id, $rec->contextid); + $this->assertEquals($course->id, $rec->courseid); + $this->assertEquals($question3->id, $rec->refid); + } + } + $rs2->close(); + $this->assertEquals(12, $count); + + // Add a question to the category context. + $cat3 = $generator->create_question_category(['contextid' => $catcontext->id]); + $question4 = $generator->create_question('multichoice', null, ['category' => $cat3->id]); + $rs3 = $questionanswers->find_course_areas($course->id); + $this->assertNotNull($rs3); + + // The category level questions should not be found. + $count = 0; + foreach ($rs3 as $rec) { + $count++; + if ($count > 8) { + $this->assertEquals($quizcontext->id, $rec->contextid); + $this->assertEquals($course->id, $rec->courseid); + $this->assertEquals($question3->id, $rec->refid); + } + } + $rs2->close(); + $this->assertEquals(12, $count); + } + + /** + * Test find relevant areas. + */ + public function test_find_relevant_areas() { + $this->resetAfterTest(); + $this->setAdminUser(); + + $course = $this->getDataGenerator()->create_course(); + $coursecontext = \context_course::instance($course->id); + $generator = $this->getDataGenerator()->get_plugin_generator('core_question'); + $cat1 = $generator->create_question_category(['contextid' => $coursecontext->id]); + $question1 = $generator->create_question('multichoice', null, ['category' => $cat1->id]); + $question2 = $generator->create_question('multichoice', null, ['category' => $cat1->id]); + $questionanswers = new questionanswers(); + $event = \core\event\question_updated::create_from_question_instance($question1, + \context_course::instance($course->id)); + $rs = $questionanswers->find_relevant_areas($event); + $this->assertNotNull($rs); + + // Each multichoice question generated has four answers. + $count = 0; + foreach ($rs as $rec) { + $count++; + $this->assertEquals($coursecontext->id, $rec->contextid); + $this->assertEquals($course->id, $rec->courseid); + $this->assertEquals($question1->id, $rec->refid); + } + $rs->close(); + $this->assertEquals(4, $count); + } + + /** + * Test find system areas. + */ + public function test_find_system_areas() { + $this->resetAfterTest(); + $this->setAdminUser(); + + $category = $this->getDataGenerator()->create_category(); + $catcontext = \context_coursecat::instance($category->id); + $systemcontext = \context_system::instance(); + $generator = $this->getDataGenerator()->get_plugin_generator('core_question'); + $cat = $generator->create_question_category(['contextid' => $catcontext->id]); + $cat2 = $generator->create_question_category(['contextid' => $systemcontext->id]); + $question = $generator->create_question('multichoice', null, ['category' => $cat2->id]); + $question2 = $generator->create_question('multichoice', null, ['category' => $cat->id]); + $questionanswers = new questionanswers(); + $rs = $questionanswers->find_system_areas(); + $this->assertNotNull($rs); + + // Each multichoice question generated has four answers. + $count = 0; + foreach ($rs as $rec) { + $count++; + if ($count <= 4) { + $this->assertEquals($systemcontext->id, $rec->contextid); + $this->assertEquals(1, $rec->courseid); + $this->assertEquals(0, $rec->categoryid); + $this->assertEquals($question->id, $rec->refid); + } else { + $this->assertEquals($catcontext->id, $rec->contextid); + $this->assertEquals(1, $rec->courseid); + $this->assertEquals($category->id, $rec->categoryid); + $this->assertEquals($question2->id, $rec->refid); + } + } + $rs->close(); + $this->assertEquals(8, $count); + } +} diff --git a/admin/tool/brickfield/tests/local/areas/core_question/questiontext_test.php b/admin/tool/brickfield/tests/local/areas/core_question/questiontext_test.php new file mode 100644 index 0000000000000..0f779e53905a0 --- /dev/null +++ b/admin/tool/brickfield/tests/local/areas/core_question/questiontext_test.php @@ -0,0 +1,187 @@ +. + +namespace tool_brickfield\local\areas\core_question; + +/** + * Tests for questiontext. + * + * @package tool_brickfield + * @copyright 2020 onward: Brickfield Education Labs, https://www.brickfield.ie + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class questiontext_test extends \advanced_testcase { + + /** + * Set up before class. + */ + public static function setUpBeforeClass(): void { + global $CFG; + require_once($CFG->dirroot . '/mod/quiz/locallib.php'); + } + + /** + * Test find course areas. + */ + public function test_find_course_areas() { + $this->resetAfterTest(); + $this->setAdminUser(); + + $category = $this->getDataGenerator()->create_category(); + $course = $this->getDataGenerator()->create_course(['category' => $category->id]); + $coursecontext = \context_course::instance($course->id); + $catcontext = \context_coursecat::instance($category->id); + $generator = $this->getDataGenerator()->get_plugin_generator('core_question'); + $cat1 = $generator->create_question_category(['contextid' => $coursecontext->id]); + $question1 = $generator->create_question('multichoice', null, ['category' => $cat1->id]); + $question2 = $generator->create_question('multichoice', null, ['category' => $cat1->id]); + $questiontext = new questiontext(); + $rs = $questiontext->find_course_areas($course->id); + $this->assertNotNull($rs); + + $count = 0; + foreach ($rs as $rec) { + $count++; + $this->assertEquals($coursecontext->id, $rec->contextid); + $this->assertEquals($course->id, $rec->courseid); + if ($count <= 1) { + $this->assertEquals($question1->id, $rec->itemid); + } else { + $this->assertEquals($question2->id, $rec->itemid); + } + } + $rs->close(); + $this->assertEquals(2, $count); + + // Add a question to a quiz in the course. + $quiz = $this->getDataGenerator()->create_module('quiz', ['course' => $course->id, 'name' => 'Quiz1']); + $quizmodule = get_coursemodule_from_instance('quiz', $quiz->id, $course->id); + $quizcontext = \context_module::instance($quizmodule->id); + + // Add a question to the quiz context. + $cat2 = $generator->create_question_category(['contextid' => $quizcontext->id]); + $question3 = $generator->create_question('multichoice', null, ['category' => $cat2->id]); + $rs2 = $questiontext->find_course_areas($course->id); + $this->assertNotNull($rs2); + + $count = 0; + foreach ($rs2 as $rec) { + $count++; + if ($count <= 1) { + $this->assertEquals($coursecontext->id, $rec->contextid); + $this->assertEquals($course->id, $rec->courseid); + $this->assertEquals($question1->id, $rec->itemid); + } else if ($count <= 2) { + $this->assertEquals($coursecontext->id, $rec->contextid); + $this->assertEquals($course->id, $rec->courseid); + $this->assertEquals($question2->id, $rec->itemid); + } else { + $this->assertEquals($quizcontext->id, $rec->contextid); + $this->assertEquals($course->id, $rec->courseid); + $this->assertEquals($question3->id, $rec->itemid); + } + } + $rs2->close(); + $this->assertEquals(3, $count); + + // Add a question to the category context. + $cat3 = $generator->create_question_category(['contextid' => $catcontext->id]); + $question4 = $generator->create_question('multichoice', null, ['category' => $cat3->id]); + $rs3 = $questiontext->find_course_areas($course->id); + $this->assertNotNull($rs3); + + // The category level questions should not be found. + $count = 0; + foreach ($rs3 as $rec) { + $count++; + if ($count > 2) { + $this->assertEquals($quizcontext->id, $rec->contextid); + $this->assertEquals($course->id, $rec->courseid); + $this->assertEquals($question3->id, $rec->itemid); + } + } + $rs2->close(); + $this->assertEquals(3, $count); + } + + /** + * Test find relevant areas. + */ + public function test_find_relevant_areas() { + $this->resetAfterTest(); + $this->setAdminUser(); + + $course = $this->getDataGenerator()->create_course(); + $coursecontext = \context_course::instance($course->id); + $generator = $this->getDataGenerator()->get_plugin_generator('core_question'); + $cat1 = $generator->create_question_category(['contextid' => $coursecontext->id]); + $question1 = $generator->create_question('multichoice', null, ['category' => $cat1->id]); + $question2 = $generator->create_question('multichoice', null, ['category' => $cat1->id]); + $questiontext = new questiontext(); + $event = \core\event\question_updated::create_from_question_instance($question1, + \context_course::instance($course->id)); + $rs = $questiontext->find_relevant_areas($event); + $this->assertNotNull($rs); + + $count = 0; + foreach ($rs as $rec) { + $count++; + $this->assertEquals($coursecontext->id, $rec->contextid); + $this->assertEquals($course->id, $rec->courseid); + $this->assertEquals($question1->id, $rec->itemid); + } + $rs->close(); + $this->assertEquals(1, $count); + } + + /** + * Test find system areas. + */ + public function test_find_system_areas() { + $this->resetAfterTest(); + $this->setAdminUser(); + + $category = $this->getDataGenerator()->create_category(); + $catcontext = \context_coursecat::instance($category->id); + $systemcontext = \context_system::instance(); + $generator = $this->getDataGenerator()->get_plugin_generator('core_question'); + $cat = $generator->create_question_category(['contextid' => $catcontext->id]); + $cat2 = $generator->create_question_category(['contextid' => $systemcontext->id]); + $question = $generator->create_question('multichoice', null, ['category' => $cat2->id]); + $question2 = $generator->create_question('multichoice', null, ['category' => $cat->id]); + $questiontext = new questiontext(); + $rs = $questiontext->find_system_areas(); + $this->assertNotNull($rs); + + $count = 0; + foreach ($rs as $rec) { + $count++; + if ($count <= 1) { + $this->assertEquals($systemcontext->id, $rec->contextid); + $this->assertEquals(1, $rec->courseid); + $this->assertEquals(0, $rec->categoryid); + $this->assertEquals($question->id, $rec->itemid); + } else { + $this->assertEquals($catcontext->id, $rec->contextid); + $this->assertEquals(1, $rec->courseid); + $this->assertEquals($category->id, $rec->categoryid); + $this->assertEquals($question2->id, $rec->itemid); + } + } + $rs->close(); + $this->assertEquals(2, $count); + } +} diff --git a/admin/tool/brickfield/tests/local/htmlchecker/common/checks/a_links_dont_open_new_window_test.php b/admin/tool/brickfield/tests/local/htmlchecker/common/checks/a_links_dont_open_new_window_test.php new file mode 100644 index 0000000000000..b60a9baaac432 --- /dev/null +++ b/admin/tool/brickfield/tests/local/htmlchecker/common/checks/a_links_dont_open_new_window_test.php @@ -0,0 +1,106 @@ +. + +/** + * tool_brickfield check test. + * + * @package tool_brickfield + * @copyright 2020 onward: Brickfield Education Labs, https://www.brickfield.ie + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace tool_brickfield\local\htmlchecker\common\checks; + +defined('MOODLE_INTERNAL') || die(); + +require_once('all_checks.php'); + +/** + * Class a_links_dont_open_new_window_testcase + */ +class a_links_dont_open_new_window_test extends all_checks { + /** @var string Check type */ + protected $checktype = 'a_links_dont_open_new_window'; + + /** @var string Html fail */ + private $htmlfail = <<This is not a basefont tag
+ + +EOD; + + /** + * Test for basefont tags being used + */ + public function test_check() { + $results = $this->get_checker_results($this->htmlfail); + $this->assertTrue($results[0]->element->tagName == 'basefont'); + + $results = $this->get_checker_results($this->htmlpass); + $this->assertEmpty($results); + } +} diff --git a/admin/tool/brickfield/tests/local/htmlchecker/common/checks/blink_is_not_used_test.php b/admin/tool/brickfield/tests/local/htmlchecker/common/checks/blink_is_not_used_test.php new file mode 100644 index 0000000000000..66a4ce965ac48 --- /dev/null +++ b/admin/tool/brickfield/tests/local/htmlchecker/common/checks/blink_is_not_used_test.php @@ -0,0 +1,74 @@ +. + +/** + * tool_brickfield check test. + * + * @package tool_brickfield + * @copyright 2020 onward: Brickfield Education Labs, https://www.brickfield.ie + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace tool_brickfield\local\htmlchecker\common\checks; + +defined('MOODLE_INTERNAL') || die(); + +require_once('all_checks.php'); + +/** + * Class blink_is_not_used_testcase + */ +class blink_is_not_used_test extends all_checks { + /** @var string Check type */ + protected $checktype = 'blink_is_not_used'; + + /** @var string Html fail */ + private $htmlfail = <<This text does not blink
+ + +EOD; + + /** + * Test for blink tags being used + */ + public function test_check() { + $results = $this->get_checker_results($this->htmlfail); + $this->assertTrue($results[0]->element->tagName == 'blink'); + + $results = $this->get_checker_results($this->htmlpass); + $this->assertEmpty($results); + } +} diff --git a/admin/tool/brickfield/tests/local/htmlchecker/common/checks/bold_is_not_used_test.php b/admin/tool/brickfield/tests/local/htmlchecker/common/checks/bold_is_not_used_test.php new file mode 100644 index 0000000000000..a99978976b5f9 --- /dev/null +++ b/admin/tool/brickfield/tests/local/htmlchecker/common/checks/bold_is_not_used_test.php @@ -0,0 +1,76 @@ +. + +/** + * tool_brickfield check test. + * + * @package tool_brickfield + * @copyright 2020 onward: Brickfield Education Labs, https://www.brickfield.ie + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace tool_brickfield\local\htmlchecker\common\checks; + +defined('MOODLE_INTERNAL') || die(); + +require_once('all_checks.php'); + +/** + * Class bold_is_not_used_testcase + */ +class bold_is_not_used_test extends all_checks { + /** @var string Check type */ + protected $checktype = 'bold_is_not_used'; + + /** @var string Html fail */ + private $htmlfail = <<Lorem ipsum dolor sit amet, consectetur adipiscing elit. Praesent accumsan, ante varius viverra aliquam, dolor risus + scelerisque massa, ut lacinia ipsum felis id est. Nullam convallis odio ante, in commodo elit fermentum sed. Vivamus ullamcorper + tincidunt sagittis. Sed et semper sapien. Quisque malesuada lacus nec libero cursus, aliquam malesuada neque ultricies. Cras sit + amet enim vel orci tristique porttitor a vitae urna. Suspendisse mi leo, hendrerit et eleifend a, mollis at ex. Maecenas eget + magna nec sem dignissim pharetra vel nec ex. Donec in porta lectus. Aenean porttitor euismod lectus, sodales eleifend ex egestas + in. Donec sed metus sodales, lobortis velit quis, dictum arcu. + Praesent mollis urna eget odio cursus, sit amet sollicitudin ante aliquam. Integer nec massa nec ipsum tincidunt laoreet in + vitae metus. Integer massa lacus, elementum quis dui sed, eleifend fringilla turpis. In hac habitasse platea dictumst. Phasellus + efficitur quis felis non eleifend. Sed et mauris vel lorem ultrices porta. Mauris commodo condimentum felis, vel dictum ex + laoreet sit amet. Duis venenatis ut lacus non ultrices. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per + inceptos himenaeos. Nam nunc magna, semper feugiat feugiat a, pellentesque vel nulla. + Sed lacinia nunc lobortis, vestibulum nisi dictum, pulvinar tellus. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed + sodales, mauris vitae vulputate porttitor, urna tellus tempor turpis, sed hendrerit metus turpis at est. Etiam augue purus, + blandit eget elit sit amet, suscipit mollis ligula. Suspendisse rutrum sem ex, eu commodo nisi aliquam sit amet. Fusce ut felis + justo. Sed a quam at lectus consectetur vulputate. Proin elementum dui nisi, in condimentum diam porttitor eget. Donec vehicula + condimentum velit vel semper. Mauris vehicula tortor lectus, quis convallis erat aliquet vel. In dictum nunc ac posuere porta. + Sed vel leo aliquam, volutpat ligula ac, blandit diam. Donec nec ligula lacus. + Mauris ac libero vel ex fringilla fringilla. Ut vehicula justo eu nunc imperdiet ultricies. Sed interdum ligula at nisi rhoncus + auctor. Sed tempus tellus eget risus placerat, et viverra dolor gravida. Sed ultricies neque id ex tempor viverra. Ut imperdiet + pharetra magna sed tristique. Pellentesque blandit elit ac neque lacinia finibus. Lorem ipsum dolor sit amet, consectetur + adipiscing elit. Donec vel auctor dolor. Morbi id elit mollis ante mattis semper eu ac lectus. Integer elit turpis, facilisis + vel metus eget, blandit tempus arcu. Pellentesque eget magna eu ex eleifend tincidunt. Curabitur sit amet congue nisi. + Cras mauris risus, malesuada egestas dapibus et, pharetra in ante. Aenean sit amet augue non ligula tempor scelerisque eget ac + turpis. Aenean tincidunt tristique dui, pretium lacinia felis posuere vel. Donec massa ligula, luctus vitae enim nec, sagittis + hendrerit lorem. In consequat sodales metus vel porttitor. Aenean fringilla fringilla risus, vitae interdum turpis egestas quis. + Aenean volutpat arcu leo, ut dictum purus consectetur id. Cras enim ipsum, tincidunt vitae mi vel, varius convallis ex. Fusce + pretium porttitor tempus. + Morbi laoreet dapibus lectus ut efficitur. Donec at hendrerit nunc. Vivamus venenatis augue non nulla finibus vestibulum. Nam + nunc magna, hendrerit a ipsum nec, pulvinar imperdiet augue. Fusce vel metus maximus, mattis magna at, egestas enim. Suspendisse + et nisl at enim mollis scelerisque. Duis ut ipsum vel turpis eleifend aliquet a a ante. Nam lacinia purus vulputate purus + tincidunt, aliquet sagittis nisi sagittis. Pellentesque efficitur massa non ex sodales pretium. Cras convallis vitae ex et + dignissim. Nunc suscipit bibendum aliquam. Maecenas interdum tellus varius, laoreet velit sed, ornare arcu. Nunc pulvinar + elementum sem eget scelerisque. Duis volutpat tellus ut risus finibus, nec molestie erat fermentum +
+ + +EOD; + + /** @var string Html pass */ + private $htmlpass = <<Nice and short text
+ + +EOD; + + /** + * Test for checking the length of the content + */ + public function test_check() { + $results = $this->get_checker_results($this->htmlfail); + $this->assertTrue($results[0]->message == 'Word Count: 578
'); + + $results = $this->get_checker_results($this->htmlpass); + $this->assertEmpty($results); + } +} diff --git a/admin/tool/brickfield/tests/local/htmlchecker/common/checks/css_text_has_contrast_test.php b/admin/tool/brickfield/tests/local/htmlchecker/common/checks/css_text_has_contrast_test.php new file mode 100644 index 0000000000000..f5c1fa59f2f3e --- /dev/null +++ b/admin/tool/brickfield/tests/local/htmlchecker/common/checks/css_text_has_contrast_test.php @@ -0,0 +1,74 @@ +. + +/** + * tool_brickfield check test. + * + * @package tool_brickfield + * @copyright 2020 onward: Brickfield Education Labs, https://www.brickfield.ie + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace tool_brickfield\local\htmlchecker\common\checks; + +defined('MOODLE_INTERNAL') || die(); + +require_once('all_checks.php'); + +/** + * Class test_css_text_has_contrast_testcase + */ +class css_text_has_contrast_test extends all_checks { + /** @var string Check type */ + protected $checktype = 'css_text_has_contrast'; + + /** @var string Html fail */ + private $htmlfail = <<This is not contrasty enough.
+ + +EOD; + + /** @var string Html pass */ + private $htmlpass = <<This is contrasty enough.
+ + +EOD; + + /** + * Test for the area assign intro + */ + public function test_check() { + $results = $this->get_checker_results($this->htmlfail); + $this->assertTrue($results[0]->element->tagName == 'p'); + + $results = $this->get_checker_results($this->htmlpass); + $this->assertEmpty($results); + } +} diff --git a/admin/tool/brickfield/tests/local/htmlchecker/common/checks/embed_has_associated_no_embed_test.php b/admin/tool/brickfield/tests/local/htmlchecker/common/checks/embed_has_associated_no_embed_test.php new file mode 100644 index 0000000000000..64e283f91d11c --- /dev/null +++ b/admin/tool/brickfield/tests/local/htmlchecker/common/checks/embed_has_associated_no_embed_test.php @@ -0,0 +1,94 @@ +. + +/** + * tool_brickfield check test. + * + * @package tool_brickfield + * @copyright 2020 onward: Brickfield Education Labs, https://www.brickfield.ie + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace tool_brickfield\local\htmlchecker\common\checks; + +defined('MOODLE_INTERNAL') || die(); + +require_once('all_checks.php'); + +/** + * Class embed_has_associated_no_embed_testcase + */ +class embed_has_associated_no_embed_test extends all_checks { + /** @var string Check type */ + protected $checktype = 'embed_has_associated_no_embed'; + + /** @var string Html fail 1 */ + private $htmlfail = <<