Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 3.11.x will end 14 Nov 2022 (12 months plus 6 months extension).
  • Bug fixes for security issues in 3.11.x will end 13 Nov 2023 (18 months plus 12 months extension).
  • PHP version: minimum PHP 7.3.0 Note: minimum PHP version has increased since Moodle 3.10. PHP 7.4.x is supported too.

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  }