AutorÃa | Ultima modificación | Ver Log |
<?php// 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, see <http://www.gnu.org/licenses/>.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'];/** @var array */public array $path = [];/** @var array To store additional CSS files to load. */public array $css_files = [];/*** 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;}}