Differences Between: [Versions 311 and 400] [Versions 311 and 401] [Versions 311 and 402] [Versions 311 and 403]
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; 18 19 use context_system; 20 use moodle_exception; 21 use moodle_url; 22 use stdClass; 23 use tool_brickfield\local\tool\filter; 24 25 /** 26 * Provides the Brickfield Accessibility toolkit API. 27 * 28 * @package tool_brickfield 29 * @copyright 2020 onward Brickfield Education Labs Ltd, https://www.brickfield.ie 30 * @author Mike Churchward (mike@brickfieldlabs.ie) 31 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 32 */ 33 class accessibility { 34 35 /** @var string The component sub path */ 36 private static $pluginpath = 'tool/brickfield'; 37 38 /** @var string Supported format of topics */ 39 const TOOL_BRICKFIELD_FORMAT_TOPIC = 'topics'; 40 41 /** @var string Supported format of weeks */ 42 const TOOL_BRICKFIELD_FORMAT_WEEKLY = 'weeks'; 43 44 /** 45 * Return the state of the site enable condition. 46 * @return bool 47 */ 48 public static function is_accessibility_enabled(): bool { 49 global $CFG; 50 51 if (isset($CFG->enableaccessibilitytools)) { 52 return $CFG->enableaccessibilitytools; 53 } 54 55 // Enabled by default. 56 return true; 57 } 58 59 /** 60 * Throw an error if the toolkit is not enabled. 61 * @return bool 62 * @throws moodle_exception 63 */ 64 public static function require_accessibility_enabled(): bool { 65 if (!static::is_accessibility_enabled()) { 66 throw new moodle_exception('accessibilitydisabled', manager::PLUGINNAME); 67 } 68 return true; 69 } 70 71 /** 72 * Get a URL for a page within the plugin. 73 * 74 * This takes into account the value of the admin config value. 75 * 76 * @param string $url The URL within the plugin 77 * @return moodle_url 78 */ 79 public static function get_plugin_url(string $url = ''): moodle_url { 80 $url = ($url == '') ? 'index.php' : $url; 81 $pluginpath = self::$pluginpath; 82 return new moodle_url("/admin/{$pluginpath}/{$url}"); 83 } 84 85 /** 86 * Get a file path for a file within the plugin. 87 * 88 * This takes into account the value of the admin config value. 89 * 90 * @param string $path The path within the plugin 91 * @return string 92 */ 93 public static function get_file_path(string $path): string { 94 global $CFG; 95 96 return implode(DIRECTORY_SEPARATOR, [$CFG->dirroot, $CFG->admin, self::$pluginpath, $path, ]); 97 } 98 99 /** 100 * Get the canonicalised name of a capability. 101 * 102 * @param string $capability 103 * @return string 104 */ 105 public static function get_capability_name(string $capability): string { 106 return self::$pluginpath . ':' . $capability; 107 } 108 109 /** 110 * Get the relevant title. 111 * @param filter $filter 112 * @param int $countdata 113 * @return string 114 * @throws \coding_exception 115 * @throws \dml_exception 116 * @throws \moodle_exception 117 */ 118 public static function get_title(filter $filter, int $countdata): string { 119 global $DB; 120 121 $tmp = new \stdClass(); 122 $tmp->count = $countdata; 123 $langstr = 'title' . $filter->tab . 'partial'; 124 125 if ($filter->courseid != 0) { 126 $thiscourse = get_fast_modinfo($filter->courseid)->get_course(); 127 $tmp->name = $thiscourse->fullname; 128 } else { 129 $langstr = 'title' . $filter->tab . 'all'; 130 } 131 return get_string($langstr, manager::PLUGINNAME, $tmp); 132 } 133 134 /** 135 * Function to be run periodically according to the scheduled task. 136 * Return true if a process was completed. False if no process executed. 137 * Finds all unprocessed courses for bulk batch processing and completes them. 138 * @param int $batch 139 * @return bool 140 * @throws \ReflectionException 141 * @throws \coding_exception 142 * @throws \ddl_exception 143 * @throws \ddl_table_missing_exception 144 * @throws \dml_exception 145 */ 146 public static function bulk_process_courses_cron(int $batch = 0): bool { 147 global $PAGE; 148 149 // Run a registration check. 150 if (!(new registration())->validate()) { 151 return false; 152 } 153 154 if (analysis::is_enabled()) { 155 $PAGE->set_context(context_system::instance()); 156 mtrace("Starting cron for bulk_process_courses"); 157 // Do regular processing. True if full deployment type isn't selected as well. 158 static::bulk_processing($batch); 159 mtrace("Ending cron for bulk_process_courses"); 160 return true; 161 } else { 162 mtrace('Content analysis is currently disabled in settings.'); 163 return false; 164 } 165 } 166 167 /** 168 * Bulk processing. 169 * @param int $batch 170 * @return bool 171 */ 172 protected static function bulk_processing(int $batch = 0): bool { 173 manager::check_course_updates(); 174 mtrace("check_course_updates completed at " . time()); 175 $recordsprocessed = manager::check_scheduled_areas($batch); 176 mtrace("check_scheduled_areas completed at " . time()); 177 manager::check_scheduled_deletions(); 178 mtrace("check_scheduled_deletions completed at " . time()); 179 manager::delete_historical_data(); 180 mtrace("delete_historical_data completed at " . time()); 181 return $recordsprocessed; 182 } 183 184 /** 185 * Function to be run periodically according to the scheduled task. 186 * Finds all unprocessed courses for cache processing and completes them. 187 */ 188 public static function bulk_process_caches_cron() { 189 global $DB; 190 191 // Run a registration check. 192 if (!(new registration())->validate()) { 193 return; 194 } 195 196 if (analysis::is_enabled()) { 197 mtrace("Starting cron for bulk_process_caches"); 198 // Monitor ongoing caching requests. 199 $fields = 'DISTINCT courseid'; 200 $reruns = $DB->get_records(manager::DB_PROCESS, ['item' => 'cache'], '', $fields); 201 foreach ($reruns as $rerun) { 202 mtrace("Running rerun caching for Courseid " . $rerun->courseid); 203 manager::store_result_summary($rerun->courseid); 204 mtrace("rerun cache completed at " . time()); 205 $DB->delete_records(manager::DB_PROCESS, ['courseid' => $rerun->courseid, 'item' => 'cache']); 206 } 207 mtrace("Ending cron for bulk_process_caches at " . time()); 208 } else { 209 mtrace('Content analysis is currently disabled in settings.'); 210 } 211 } 212 213 /** 214 * This function runs the checks on the html item 215 * 216 * @param string $html The html string to be analysed; might be NULL. 217 * @param int $contentid The content area ID 218 * @param int $processingtime 219 * @param int $resultstime 220 */ 221 public static function run_check(string $html, int $contentid, int &$processingtime, int &$resultstime) { 222 global $DB; 223 224 // Change the limit if 10,000 is not appropriate. 225 $bulkrecordlimit = manager::BULKRECORDLIMIT; 226 $bulkrecordcount = 0; 227 228 $checkids = static::checkids(); 229 $checknameids = array_flip($checkids); 230 231 $testname = 'brickfield'; 232 233 $stime = time(); 234 235 // Swapping in new library. 236 $htmlchecker = new local\htmlchecker\brickfield_accessibility($html, $testname, 'string'); 237 $htmlchecker->run_check(); 238 $tests = $htmlchecker->guideline->get_tests(); 239 $report = $htmlchecker->get_report(); 240 $processingtime += (time() - $stime); 241 242 $records = []; 243 foreach ($tests as $test) { 244 $records[$test]['count'] = 0; 245 $records[$test]['errors'] = []; 246 } 247 248 foreach ($report['report'] as $a) { 249 if (!isset($a['type'])) { 250 continue; 251 } 252 $type = $a['type']; 253 $records[$type]['errors'][] = $a; 254 if (!isset($records[$type]['count'])) { 255 $records[$type]['count'] = 0; 256 } 257 $records[$type]['count']++; 258 } 259 260 $stime = time(); 261 $returnchecks = []; 262 $errors = []; 263 264 // Build up records for inserting. 265 foreach ($records as $key => $rec) { 266 $recordres = new stdClass(); 267 // Handling if checkid is unknown. 268 $checkid = (isset($checknameids[$key])) ? $checknameids[$key] : 0; 269 $recordres->contentid = $contentid; 270 $recordres->checkid = $checkid; 271 $recordres->errorcount = $rec['count']; 272 273 // Build error inserts if needed. 274 if ($rec['count'] > 0) { 275 foreach ($rec['errors'] as $tmp) { 276 $error = new stdClass(); 277 $error->resultid = 0; 278 $error->linenumber = $tmp['lineNo']; 279 $error->htmlcode = $tmp['html']; 280 $error->errordescription = $tmp['title']; 281 // Add contentid and checkid so that we can query for the results record id later. 282 $error->contentid = $contentid; 283 $error->checkid = $checkid; 284 $errors[] = $error; 285 } 286 } 287 $returnchecks[] = $recordres; 288 $bulkrecordcount++; 289 290 // If we've hit the bulk limit, write the results records and reset. 291 if ($bulkrecordcount > $bulkrecordlimit) { 292 $DB->insert_records(manager::DB_RESULTS, $returnchecks); 293 $bulkrecordcount = 0; 294 $returnchecks = []; 295 // Get the results id value for each error record and write the errors. 296 foreach ($errors as $key2 => $error) { 297 $errors[$key2]->resultid = $DB->get_field(manager::DB_RESULTS, 'id', 298 ['contentid' => $error->contentid, 'checkid' => $error->checkid]); 299 unset($errors[$key2]->contentid); 300 unset($errors[$key2]->checkid); 301 } 302 $DB->insert_records(manager::DB_ERRORS, $errors); 303 $errors = []; 304 } 305 } 306 307 // Write any leftover records. 308 if ($bulkrecordcount > 0) { 309 $DB->insert_records(manager::DB_RESULTS, $returnchecks); 310 // Get the results id value for each error record and write the errors. 311 foreach ($errors as $key => $error) { 312 $errors[$key]->resultid = $DB->get_field(manager::DB_RESULTS, 'id', 313 ['contentid' => $error->contentid, 'checkid' => $error->checkid]); 314 unset($errors[$key]->contentid); 315 unset($errors[$key]->checkid); 316 } 317 $DB->insert_records(manager::DB_ERRORS, $errors); 318 } 319 320 $resultstime += (time() - $stime); 321 } 322 323 /** 324 * This function runs one specified check on the html item 325 * 326 * @param string|null $html The html string to be analysed; might be NULL. 327 * @param int $contentid The content area ID 328 * @param int $errid The error ID 329 * @param string $check The check name to run 330 * @param int $processingtime 331 * @param int $resultstime 332 * @throws \coding_exception 333 * @throws \dml_exception 334 */ 335 public static function run_one_check( 336 ?string $html, 337 int $contentid, 338 int $errid, 339 string $check, 340 int &$processingtime, 341 int &$resultstime 342 ) { 343 global $DB; 344 345 $stime = time(); 346 347 $checkdata = $DB->get_record(manager::DB_CHECKS, ['shortname' => $check], 'id,shortname,severity'); 348 349 $testname = 'brickfield'; 350 351 // Swapping in new library. 352 $htmlchecker = new local\htmlchecker\brickfield_accessibility($html, $testname, 'string'); 353 $htmlchecker->run_check(); 354 $report = $htmlchecker->get_test($check); 355 $processingtime += (time() - $stime); 356 357 $record = []; 358 $record['count'] = 0; 359 $record['errors'] = []; 360 361 foreach ($report as $a) { 362 $a->html = $a->get_html(); 363 $record['errors'][] = $a; 364 $record['count']++; 365 } 366 367 // Build up record for inserting. 368 $recordres = new stdClass(); 369 // Handling if checkid is unknown. 370 $checkid = (isset($checkdata->id)) ? $checkdata->id : 0; 371 $recordres->contentid = $contentid; 372 $recordres->checkid = $checkid; 373 $recordres->errorcount = $record['count']; 374 if ($exists = $DB->get_record(manager::DB_RESULTS, ['contentid' => $contentid, 'checkid' => $checkid])) { 375 $resultid = $exists->id; 376 $DB->set_field(manager::DB_RESULTS, 'errorcount', $record['count'], ['id' => $resultid]); 377 // Remove old error records for specific resultid, if existing. 378 $DB->delete_records(manager::DB_ERRORS, ['id' => $errid]); 379 } else { 380 $resultid = $DB->insert_record(manager::DB_RESULTS, $recordres); 381 } 382 $errors = []; 383 384 // Build error inserts if needed. 385 if ($record['count'] > 0) { 386 // Reporting all found errors for this check, so need to ignore existing other error records. 387 foreach ($record['errors'] as $tmp) { 388 // Confirm if error is reported separately. 389 if ($DB->record_exists_select(manager::DB_ERRORS, 390 'resultid = ? AND ' . $DB->sql_compare_text('htmlcode', 255) . ' = ' . $DB->sql_compare_text('?', 255), 391 [$resultid, html_entity_decode($tmp->html)])) { 392 continue; 393 } 394 $error = new stdClass(); 395 $error->resultid = $resultid; 396 $error->linenumber = $tmp->line; 397 $error->htmlcode = html_entity_decode($tmp->html); 398 $errors[] = $error; 399 } 400 401 $DB->insert_records(manager::DB_ERRORS, $errors); 402 } 403 404 $resultstime += (time() - $stime); 405 } 406 407 /** 408 * Returns all of the id's and shortnames of all of the checks. 409 * @param int $status 410 * @return array 411 * @throws \dml_exception 412 */ 413 public static function checkids(int $status = 1): array { 414 global $DB; 415 416 $checks = $DB->get_records_menu(manager::DB_CHECKS, ['status' => $status], 'id ASC', 'id,shortname'); 417 return $checks; 418 } 419 420 /** 421 * Returns an array of translations from htmlchecker of all of the checks, and their descriptions. 422 * @return array 423 * @throws \dml_exception 424 */ 425 public static function get_translations(): array { 426 global $DB; 427 428 $htmlchecker = new local\htmlchecker\brickfield_accessibility('test', 'brickfield', 'string'); 429 $htmlchecker->run_check(); 430 ksort($htmlchecker->guideline->translations); 431 432 // Need to limit to active checks. 433 $activechecks = $DB->get_fieldset_select(manager::DB_CHECKS, 'shortname', 'status = :status', ['status' => 1]); 434 435 $translations = []; 436 foreach ($htmlchecker->guideline->translations as $key => $trans) { 437 if (in_array($key, $activechecks)) { 438 $translations[$key] = $trans; 439 } 440 } 441 442 return $translations; 443 } 444 445 /** 446 * Returns an array of all of the course id's for a given category. 447 * @param int $categoryid 448 * @return array|null 449 * @throws \dml_exception 450 */ 451 public static function get_category_courseids(int $categoryid): ?array { 452 global $DB; 453 454 if (!$DB->record_exists('course_categories', ['id' => $categoryid])) { 455 return null; 456 } 457 458 $sql = "SELECT {course}.id 459 FROM {course}, {course_categories} 460 WHERE {course}.category = {course_categories}.id 461 AND ( 462 " . $DB->sql_like('path', ':categoryid1') . " 463 OR " . $DB->sql_like('path', ':categoryid2') . " 464 )"; 465 $params = ['categoryid1' => "%/$categoryid/%", 'categoryid2' => "%/$categoryid"]; 466 $courseids = $DB->get_fieldset_sql($sql, $params); 467 468 return $courseids; 469 } 470 471 /** 472 * Get summary data for this site. 473 * @param int $id 474 * @return \stdClass 475 * @throws \dml_exception 476 */ 477 public static function get_summary_data(int $id): \stdClass { 478 global $CFG, $DB; 479 480 $summarydata = new \stdClass(); 481 $summarydata->siteurl = (substr($CFG->wwwroot, -1) !== '/') ? $CFG->wwwroot . '/' : $CFG->wwwroot; 482 $summarydata->moodlerelease = (preg_match('/^(\d+\.\d.*?)[. ]/', $CFG->release, $matches)) ? $matches[1] : $CFG->release; 483 $summarydata->numcourses = $DB->count_records('course') - 1; 484 $summarydata->numusers = $DB->count_records('user', array('deleted' => 0)); 485 $summarydata->numfiles = $DB->count_records('files'); 486 $summarydata->numfactivities = $DB->count_records('course_modules'); 487 $summarydata->mobileservice = (int)$CFG->enablemobilewebservice === 1 ? true : false; 488 $summarydata->usersmobileregistered = $DB->count_records('user_devices'); 489 $summarydata->contenttyperesults = static::get_contenttyperesults($id); 490 $summarydata->contenttypeerrors = static::get_contenttypeerrors(); 491 $summarydata->percheckerrors = static::get_percheckerrors(); 492 return $summarydata; 493 } 494 495 /** 496 * Get content type results. 497 * @param int $id 498 * @return \stdClass 499 */ 500 private static function get_contenttyperesults(int $id): \stdClass { 501 global $DB; 502 $sql = 'SELECT component, COUNT(id) AS count 503 FROM {' . manager::DB_AREAS . '} 504 GROUP BY component'; 505 $components = $DB->get_recordset_sql($sql); 506 $contenttyperesults = new \stdClass(); 507 $contenttyperesults->id = $id; 508 $contenttyperesults->contenttype = new \stdClass(); 509 foreach ($components as $component) { 510 $componentname = $component->component; 511 $contenttyperesults->contenttype->$componentname = $component->count; 512 } 513 $components->close(); 514 $contenttyperesults->summarydatastorage = static::get_summary_data_storage(); 515 $contenttyperesults->datachecked = time(); 516 return $contenttyperesults; 517 } 518 519 520 /** 521 * Get per check errors. 522 * @return stdClass 523 * @throws dml_exception 524 */ 525 private static function get_percheckerrors(): stdClass { 526 global $DB; 527 528 $sql = 'SELECT ' . $DB->sql_concat_join("'_'", ['courseid', 'checkid']) . ' as tmpid, 529 ca.courseid, ca.status, ca.checkid, ch.shortname, ca.checkcount, ca.errorcount 530 FROM {' . manager::DB_CACHECHECK . '} ca 531 INNER JOIN {' . manager::DB_CHECKS . '} ch on ch.id = ca.checkid 532 ORDER BY courseid, checkid ASC'; 533 534 $combo = $DB->get_records_sql($sql); 535 536 return (object) [ 537 'percheckerrors' => $combo, 538 ]; 539 } 540 541 /** 542 * Get content type errors. 543 * @return stdClass 544 * @throws dml_exception 545 */ 546 private static function get_contenttypeerrors(): stdClass { 547 global $DB; 548 549 $fields = 'courseid, status, activities, activitiespassed, activitiesfailed, 550 errorschecktype1, errorschecktype2, errorschecktype3, errorschecktype4, 551 errorschecktype5, errorschecktype6, errorschecktype7, 552 failedchecktype1, failedchecktype2, failedchecktype3, failedchecktype4, 553 failedchecktype5, failedchecktype6, failedchecktype7, 554 percentchecktype1, percentchecktype2, percentchecktype3, percentchecktype4, 555 percentchecktype5, percentchecktype6, percentchecktype7'; 556 $combo = $DB->get_records(manager::DB_SUMMARY, null, 'courseid ASC', $fields); 557 558 return (object) [ 559 'typeerrors' => $combo, 560 ]; 561 } 562 563 /** 564 * Get summary data storage. 565 * @return array 566 * @throws dml_exception 567 */ 568 private static function get_summary_data_storage(): array { 569 global $DB; 570 571 $fields = $DB->sql_concat_join("''", ['component', 'courseid']) . ' as tmpid, 572 courseid, component, errorcount, totalactivities, failedactivities, passedactivities'; 573 $combo = $DB->get_records(manager::DB_CACHEACTS, null, 'courseid, component ASC', $fields); 574 return $combo; 575 } 576 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body