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;use context_system;use moodle_exception;use moodle_url;use stdClass;use tool_brickfield\local\tool\filter;/*** Provides the Brickfield Accessibility toolkit API.** @package tool_brickfield* @copyright 2020 onward Brickfield Education Labs Ltd, https://www.brickfield.ie* @author Mike Churchward (mike@brickfieldlabs.ie)* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later*/class accessibility {/** @var string The component sub path */private static $pluginpath = 'tool/brickfield';/** @var string Supported format of topics */const TOOL_BRICKFIELD_FORMAT_TOPIC = 'topics';/** @var string Supported format of weeks */const TOOL_BRICKFIELD_FORMAT_WEEKLY = 'weeks';/*** Return the state of the site enable condition.* @return bool*/public static function is_accessibility_enabled(): bool {global $CFG;return !empty($CFG->enableaccessibilitytools);}/*** Throw an error if the toolkit is not enabled.* @return bool* @throws moodle_exception*/public static function require_accessibility_enabled(): bool {if (!static::is_accessibility_enabled()) {throw new moodle_exception('accessibilitydisabled', manager::PLUGINNAME);}return true;}/*** Get a URL for a page within the plugin.** This takes into account the value of the admin config value.** @param string $url The URL within the plugin* @return moodle_url*/public static function get_plugin_url(string $url = ''): moodle_url {$url = ($url == '') ? 'index.php' : $url;$pluginpath = self::$pluginpath;return new moodle_url("/admin/{$pluginpath}/{$url}");}/*** Get a file path for a file within the plugin.** This takes into account the value of the admin config value.** @param string $path The path within the plugin* @return string*/public static function get_file_path(string $path): string {global $CFG;return implode(DIRECTORY_SEPARATOR, [$CFG->dirroot, $CFG->admin, self::$pluginpath, $path, ]);}/*** Get the canonicalised name of a capability.** @param string $capability* @return string*/public static function get_capability_name(string $capability): string {return self::$pluginpath . ':' . $capability;}/*** Get the relevant title.* @param filter $filter* @param int $countdata* @return string* @throws \coding_exception* @throws \dml_exception* @throws \moodle_exception*/public static function get_title(filter $filter, int $countdata): string {global $DB;$tmp = new \stdClass();$tmp->count = $countdata;$langstr = 'title' . $filter->tab . 'partial';if ($filter->courseid != 0) {$thiscourse = get_fast_modinfo($filter->courseid)->get_course();$tmp->name = $thiscourse->fullname;} else {$langstr = 'title' . $filter->tab . 'all';}return get_string($langstr, manager::PLUGINNAME, $tmp);}/*** Function to be run periodically according to the scheduled task.* Return true if a process was completed. False if no process executed.* Finds all unprocessed courses for bulk batch processing and completes them.* @param int $batch* @return bool* @throws \ReflectionException* @throws \coding_exception* @throws \ddl_exception* @throws \ddl_table_missing_exception* @throws \dml_exception*/public static function bulk_process_courses_cron(int $batch = 0): bool {global $PAGE;// Run a registration check.if (!(new registration())->validate()) {return false;}if (analysis::is_enabled()) {$PAGE->set_context(context_system::instance());mtrace("Starting cron for bulk_process_courses");// Do regular processing. True if full deployment type isn't selected as well.static::bulk_processing($batch);mtrace("Ending cron for bulk_process_courses");return true;} else {mtrace('Content analysis is currently disabled in settings.');return false;}}/*** Bulk processing.* @param int $batch* @return bool*/protected static function bulk_processing(int $batch = 0): bool {manager::check_course_updates();mtrace("check_course_updates completed at " . time());$recordsprocessed = manager::check_scheduled_areas($batch);mtrace("check_scheduled_areas completed at " . time());manager::check_scheduled_deletions();mtrace("check_scheduled_deletions completed at " . time());manager::delete_historical_data();mtrace("delete_historical_data completed at " . time());return $recordsprocessed;}/*** Function to be run periodically according to the scheduled task.* Finds all unprocessed courses for cache processing and completes them.*/public static function bulk_process_caches_cron() {global $DB;// Run a registration check.if (!(new registration())->validate()) {return;}if (analysis::is_enabled()) {mtrace("Starting cron for bulk_process_caches");// Monitor ongoing caching requests.$fields = 'DISTINCT courseid';$reruns = $DB->get_records(manager::DB_PROCESS, ['item' => 'cache'], '', $fields);foreach ($reruns as $rerun) {mtrace("Running rerun caching for Courseid " . $rerun->courseid);manager::store_result_summary($rerun->courseid);mtrace("rerun cache completed at " . time());$DB->delete_records(manager::DB_PROCESS, ['courseid' => $rerun->courseid, 'item' => 'cache']);}mtrace("Ending cron for bulk_process_caches at " . time());} else {mtrace('Content analysis is currently disabled in settings.');}}/*** This function runs the checks on the html item** @param string $html The html string to be analysed; might be NULL.* @param int $contentid The content area ID* @param int $processingtime* @param int $resultstime*/public static function run_check(string $html, int $contentid, int &$processingtime, int &$resultstime) {global $DB;// Change the limit if 10,000 is not appropriate.$bulkrecordlimit = manager::BULKRECORDLIMIT;$bulkrecordcount = 0;$checkids = static::checkids();$checknameids = array_flip($checkids);$testname = 'brickfield';$stime = time();// Swapping in new library.$htmlchecker = new local\htmlchecker\brickfield_accessibility($html, $testname, 'string');$htmlchecker->run_check();$tests = $htmlchecker->guideline->get_tests();$report = $htmlchecker->get_report();$processingtime += (time() - $stime);$records = [];foreach ($tests as $test) {$records[$test]['count'] = 0;$records[$test]['errors'] = [];}foreach ($report['report'] as $a) {if (!isset($a['type'])) {continue;}$type = $a['type'];$records[$type]['errors'][] = $a;if (!isset($records[$type]['count'])) {$records[$type]['count'] = 0;}$records[$type]['count']++;}$stime = time();$returnchecks = [];$errors = [];// Build up records for inserting.foreach ($records as $key => $rec) {$recordres = new stdClass();// Handling if checkid is unknown.$checkid = (isset($checknameids[$key])) ? $checknameids[$key] : 0;$recordres->contentid = $contentid;$recordres->checkid = $checkid;$recordres->errorcount = $rec['count'];// Build error inserts if needed.if ($rec['count'] > 0) {foreach ($rec['errors'] as $tmp) {$error = new stdClass();$error->resultid = 0;$error->linenumber = $tmp['lineNo'];$error->htmlcode = $tmp['html'];$error->errordescription = $tmp['title'];// Add contentid and checkid so that we can query for the results record id later.$error->contentid = $contentid;$error->checkid = $checkid;$errors[] = $error;}}$returnchecks[] = $recordres;$bulkrecordcount++;// If we've hit the bulk limit, write the results records and reset.if ($bulkrecordcount > $bulkrecordlimit) {$DB->insert_records(manager::DB_RESULTS, $returnchecks);$bulkrecordcount = 0;$returnchecks = [];// Get the results id value for each error record and write the errors.foreach ($errors as $key2 => $error) {$errors[$key2]->resultid = $DB->get_field(manager::DB_RESULTS, 'id',['contentid' => $error->contentid, 'checkid' => $error->checkid]);unset($errors[$key2]->contentid);unset($errors[$key2]->checkid);}$DB->insert_records(manager::DB_ERRORS, $errors);$errors = [];}}// Write any leftover records.if ($bulkrecordcount > 0) {$DB->insert_records(manager::DB_RESULTS, $returnchecks);// Get the results id value for each error record and write the errors.foreach ($errors as $key => $error) {$errors[$key]->resultid = $DB->get_field(manager::DB_RESULTS, 'id',['contentid' => $error->contentid, 'checkid' => $error->checkid]);unset($errors[$key]->contentid);unset($errors[$key]->checkid);}$DB->insert_records(manager::DB_ERRORS, $errors);}$resultstime += (time() - $stime);}/*** This function runs one specified check on the html item** @param string|null $html The html string to be analysed; might be NULL.* @param int $contentid The content area ID* @param int $errid The error ID* @param string $check The check name to run* @param int $processingtime* @param int $resultstime* @throws \coding_exception* @throws \dml_exception*/public static function run_one_check(?string $html,int $contentid,int $errid,string $check,int &$processingtime,int &$resultstime) {global $DB;$stime = time();$checkdata = $DB->get_record(manager::DB_CHECKS, ['shortname' => $check], 'id,shortname,severity');$testname = 'brickfield';// Swapping in new library.$htmlchecker = new local\htmlchecker\brickfield_accessibility($html, $testname, 'string');$htmlchecker->run_check();$report = $htmlchecker->get_test($check);$processingtime += (time() - $stime);$record = [];$record['count'] = 0;$record['errors'] = [];foreach ($report as $a) {$a->html = $a->get_html();$record['errors'][] = $a;$record['count']++;}// Build up record for inserting.$recordres = new stdClass();// Handling if checkid is unknown.$checkid = (isset($checkdata->id)) ? $checkdata->id : 0;$recordres->contentid = $contentid;$recordres->checkid = $checkid;$recordres->errorcount = $record['count'];if ($exists = $DB->get_record(manager::DB_RESULTS, ['contentid' => $contentid, 'checkid' => $checkid])) {$resultid = $exists->id;$DB->set_field(manager::DB_RESULTS, 'errorcount', $record['count'], ['id' => $resultid]);// Remove old error records for specific resultid, if existing.$DB->delete_records(manager::DB_ERRORS, ['id' => $errid]);} else {$resultid = $DB->insert_record(manager::DB_RESULTS, $recordres);}$errors = [];// Build error inserts if needed.if ($record['count'] > 0) {// Reporting all found errors for this check, so need to ignore existing other error records.foreach ($record['errors'] as $tmp) {// Confirm if error is reported separately.if ($DB->record_exists_select(manager::DB_ERRORS,'resultid = ? AND ' . $DB->sql_compare_text('htmlcode', 255) . ' = ' . $DB->sql_compare_text('?', 255),[$resultid, html_entity_decode($tmp->html, ENT_COMPAT)])) {continue;}$error = new stdClass();$error->resultid = $resultid;$error->linenumber = $tmp->line;$error->htmlcode = html_entity_decode($tmp->html, ENT_COMPAT);$errors[] = $error;}$DB->insert_records(manager::DB_ERRORS, $errors);}$resultstime += (time() - $stime);}/*** Returns all of the id's and shortnames of all of the checks.* @param int $status* @return array* @throws \dml_exception*/public static function checkids(int $status = 1): array {global $DB;$checks = $DB->get_records_menu(manager::DB_CHECKS, ['status' => $status], 'id ASC', 'id,shortname');return $checks;}/*** Returns an array of translations from htmlchecker of all of the checks, and their descriptions.* @return array* @throws \dml_exception*/public static function get_translations(): array {global $DB;$htmlchecker = new local\htmlchecker\brickfield_accessibility('test', 'brickfield', 'string');$htmlchecker->run_check();ksort($htmlchecker->guideline->translations);// Need to limit to active checks.$activechecks = $DB->get_fieldset_select(manager::DB_CHECKS, 'shortname', 'status = :status', ['status' => 1]);$translations = [];foreach ($htmlchecker->guideline->translations as $key => $trans) {if (in_array($key, $activechecks)) {$translations[$key] = $trans;}}return $translations;}/*** Returns an array of all of the course id's for a given category.* @param int $categoryid* @return array|null* @throws \dml_exception*/public static function get_category_courseids(int $categoryid): ?array {global $DB;if (!$DB->record_exists('course_categories', ['id' => $categoryid])) {return null;}$sql = "SELECT {course}.idFROM {course}, {course_categories}WHERE {course}.category = {course_categories}.idAND (" . $DB->sql_like('path', ':categoryid1') . "OR " . $DB->sql_like('path', ':categoryid2') . ")";$params = ['categoryid1' => "%/$categoryid/%", 'categoryid2' => "%/$categoryid"];$courseids = $DB->get_fieldset_sql($sql, $params);return $courseids;}/*** Get summary data for this site.* @param int $id* @return \stdClass* @throws \dml_exception*/public static function get_summary_data(int $id): \stdClass {global $CFG, $DB;$summarydata = new \stdClass();$summarydata->siteurl = (substr($CFG->wwwroot, -1) !== '/') ? $CFG->wwwroot . '/' : $CFG->wwwroot;$summarydata->moodlerelease = (preg_match('/^(\d+\.\d.*?)[. ]/', $CFG->release, $matches)) ? $matches[1] : $CFG->release;$summarydata->numcourses = $DB->count_records('course') - 1;$summarydata->numusers = $DB->count_records('user', array('deleted' => 0));$summarydata->numfiles = $DB->count_records('files');$summarydata->numfactivities = $DB->count_records('course_modules');$summarydata->mobileservice = (int)$CFG->enablemobilewebservice === 1 ? true : false;$summarydata->usersmobileregistered = $DB->count_records('user_devices');$summarydata->contenttyperesults = static::get_contenttyperesults($id);$summarydata->contenttypeerrors = static::get_contenttypeerrors();$summarydata->percheckerrors = static::get_percheckerrors();return $summarydata;}/*** Get content type results.* @param int $id* @return \stdClass*/private static function get_contenttyperesults(int $id): \stdClass {global $DB;$sql = 'SELECT component, COUNT(id) AS countFROM {' . manager::DB_AREAS . '}GROUP BY component';$components = $DB->get_recordset_sql($sql);$contenttyperesults = new \stdClass();$contenttyperesults->id = $id;$contenttyperesults->contenttype = new \stdClass();foreach ($components as $component) {$componentname = $component->component;$contenttyperesults->contenttype->$componentname = $component->count;}$components->close();$contenttyperesults->summarydatastorage = static::get_summary_data_storage();$contenttyperesults->datachecked = time();return $contenttyperesults;}/*** Get per check errors.* @return stdClass* @throws dml_exception*/private static function get_percheckerrors(): stdClass {global $DB;$sql = 'SELECT ' . $DB->sql_concat_join("'_'", ['courseid', 'checkid']) . ' as tmpid,ca.courseid, ca.status, ca.checkid, ch.shortname, ca.checkcount, ca.errorcountFROM {' . manager::DB_CACHECHECK . '} caINNER JOIN {' . manager::DB_CHECKS . '} ch on ch.id = ca.checkidORDER BY courseid, checkid ASC';$combo = $DB->get_records_sql($sql);return (object) ['percheckerrors' => $combo,];}/*** Get content type errors.* @return stdClass* @throws dml_exception*/private static function get_contenttypeerrors(): stdClass {global $DB;$fields = 'courseid, status, activities, activitiespassed, activitiesfailed,errorschecktype1, errorschecktype2, errorschecktype3, errorschecktype4,errorschecktype5, errorschecktype6, errorschecktype7,failedchecktype1, failedchecktype2, failedchecktype3, failedchecktype4,failedchecktype5, failedchecktype6, failedchecktype7,percentchecktype1, percentchecktype2, percentchecktype3, percentchecktype4,percentchecktype5, percentchecktype6, percentchecktype7';$combo = $DB->get_records(manager::DB_SUMMARY, null, 'courseid ASC', $fields);return (object) ['typeerrors' => $combo,];}/*** Get summary data storage.* @return array* @throws dml_exception*/private static function get_summary_data_storage(): array {global $DB;$fields = $DB->sql_concat_join("''", ['component', 'courseid']) . ' as tmpid,courseid, component, errorcount, totalactivities, failedactivities, passedactivities';$combo = $DB->get_records(manager::DB_CACHEACTS, null, 'courseid, component ASC', $fields);return $combo;}}