Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 4.0.x will end 8 May 2023 (12 months).
  • Bug fixes for security issues in 4.0.x will end 13 November 2023 (18 months).
  • PHP version: minimum PHP 7.3.0 Note: the minimum PHP version has increased since Moodle 3.10. PHP 7.4.x is also supported.

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  }