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