Proyectos de Subversion Moodle

Rev

| Ultima modificación | Ver Log |

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