Search moodle.org's
Developer Documentation

See Release Notes
Long Term Support Release

  • Bug fixes for general core bugs in 4.1.x will end 13 November 2023 (12 months).
  • Bug fixes for security issues in 4.1.x will end 10 November 2025 (36 months).
  • PHP version: minimum PHP 7.4.0 Note: minimum PHP version has increased since Moodle 4.0. PHP 8.0.x is supported too.

Differences Between: [Versions 311 and 401] [Versions 400 and 401] [Versions 401 and 402] [Versions 401 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  /**
  20   * Class manager
  21   * @package tool_brickfield
  22   * @copyright  2021 Brickfield Education Labs https://www.brickfield.ie
  23   * @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  24   */
  25  class manager {
  26  
  27      /**
  28       * Defines the waiting for analysis status.
  29       */
  30      const STATUS_WAITING = 0;
  31  
  32      /**
  33       * Defined the analysis in progress status.
  34       */
  35      const STATUS_INPROGRESS = -1;
  36  
  37      /**
  38       * Defines the analysis has completed status.
  39       */
  40      const STATUS_CHECKED = 1;
  41  
  42      /**
  43       * Defines summary error value.
  44       */
  45      const SUMMARY_ERROR = 0;
  46  
  47      /**
  48       * Defines summary failed value.
  49       */
  50      const SUMMARY_FAILED = 1;
  51  
  52      /**
  53       * Defines summary percent value.
  54       */
  55      const SUMMARY_PERCENT = 2;
  56  
  57      /**
  58       * Default bulk record limit.
  59       */
  60      const BULKRECORDLIMIT = 1000;
  61  
  62      /**
  63       * Name of this plugin.
  64       */
  65      const PLUGINNAME = 'tool_brickfield';
  66  
  67      /**
  68       * Areas table name.
  69       */
  70      const DB_AREAS = self::PLUGINNAME . '_areas';
  71  
  72      /**
  73       * Cacheacts table name.
  74       */
  75      const DB_CACHEACTS = self::PLUGINNAME . '_cache_acts';
  76  
  77      /**
  78       * Cachecheck table name.
  79       */
  80      const DB_CACHECHECK = self::PLUGINNAME . '_cache_check';
  81  
  82      /**
  83       * Checks table name.
  84       */
  85      const DB_CHECKS = self::PLUGINNAME . '_checks';
  86  
  87      /**
  88       * Content table name.
  89       */
  90      const DB_CONTENT = self::PLUGINNAME . '_content';
  91  
  92      /**
  93       * Errors table name.
  94       */
  95      const DB_ERRORS = self::PLUGINNAME . '_errors';
  96  
  97      /**
  98       * Process table name.
  99       */
 100      const DB_PROCESS = self::PLUGINNAME . '_process';
 101  
 102      /**
 103       * Results table name.
 104       */
 105      const DB_RESULTS = self::PLUGINNAME . '_results';
 106  
 107      /**
 108       * Schedule table name.
 109       */
 110      const DB_SCHEDULE = self::PLUGINNAME . '_schedule';
 111  
 112      /**
 113       * Summary table name.
 114       */
 115      const DB_SUMMARY = self::PLUGINNAME . '_summary';
 116  
 117      /** @var string The URL to find help at. */
 118      private static $helpurl = 'https://www.brickfield.ie/moodle-help-311';
 119  
 120  
 121      /** @var  array Statically stores the database checks records. */
 122      static protected $checks;
 123  
 124      /**
 125       * Returns the URL used for registration.
 126       *
 127       * @return \moodle_url
 128       */
 129      public static function registration_url(): \moodle_url {
 130          return accessibility::get_plugin_url('registration.php');
 131      }
 132  
 133      /**
 134       * Returns an appropriate message about the current registration state.
 135       *
 136       * @return string
 137       * @throws \coding_exception
 138       * @throws \dml_exception
 139       * @throws \moodle_exception
 140       */
 141      public static function registration_message(): string {
 142          $firstline = get_string('notregistered', self::PLUGINNAME);
 143          if (has_capability('moodle/site:config', \context_system::instance())) {
 144              $secondline = \html_writer::link(self::registration_url(), get_string('registernow', self::PLUGINNAME));
 145          } else {
 146              $secondline = get_string('contactadmin', self::PLUGINNAME);
 147          }
 148          return $firstline . '<br />' . $secondline;
 149      }
 150  
 151      /**
 152       * Get the help page URL.
 153       * @return string
 154       * @throws dml_exception
 155       */
 156      public static function get_helpurl(): string {
 157          return self::$helpurl;
 158      }
 159  
 160      /**
 161       * Return an array of system checks available, and store them statically.
 162       *
 163       * @return array
 164       * @throws \dml_exception
 165       */
 166      public static function get_checks(): array {
 167          global $DB;
 168          if (self::$checks === null) {
 169              self::$checks = $DB->get_records(self::DB_CHECKS, [] , 'id');
 170          }
 171          return self::$checks;
 172      }
 173  
 174      /**
 175       * Find all available areas.
 176       *
 177       * @return area_base[]
 178       * @throws \ReflectionException
 179       */
 180      public static function get_all_areas(): array {
 181          return array_filter(
 182              array_map(
 183                  function($classname) {
 184                      $reflectionclass = new \ReflectionClass($classname);
 185                      if ($reflectionclass->isAbstract()) {
 186                          return false;
 187                      }
 188                      $instance = new $classname();
 189  
 190                      if ($instance->is_available()) {
 191                          return $instance;
 192                      } else {
 193                          return null;
 194                      }
 195                  },
 196                  array_keys(\core_component::get_component_classes_in_namespace('tool_brickfield', 'local\areas'))
 197              )
 198          );
 199      }
 200  
 201      /**
 202       * Calculate contenthash of a given content string
 203       *
 204       * @param string|null $content
 205       * @return string
 206       */
 207      public static function get_contenthash(?string $content = null): string {
 208          return sha1($content ?? '');
 209      }
 210  
 211      /**
 212       * Does the current area content need to be scheduled for check?
 213       *
 214       * It does not need to be scheduled if:
 215       * - it is the current content
 216       * OR
 217       * - there is already schedule
 218       *
 219       * @param int $areaid
 220       * @param string $contenthash
 221       * @return bool
 222       * @throws \dml_exception
 223       */
 224      protected static function content_needs_scheduling(int $areaid, string $contenthash): bool {
 225          global $DB;
 226          return ! $DB->get_field_sql('SELECT 1 FROM {' . self::DB_CONTENT . '} '.
 227              'WHERE areaid = ?
 228              AND (status = 0 OR (iscurrent = 1 AND contenthash = ?))',
 229              [$areaid, $contenthash], IGNORE_MULTIPLE);
 230      }
 231  
 232      /**
 233       * Schedule an area for analysis if there has been changes.
 234       *
 235       * @param \stdClass $arearecord record with the fields from the {tool_brickfield_areas} table
 236       *    as returned by area_base::find_relevant_areas().
 237       *    It also contains the 'content' property with the current area content
 238       * @throws \dml_exception
 239       */
 240      protected static function schedule_area_if_necessary(\stdClass $arearecord) {
 241          global $DB;
 242  
 243          $contenthash = static::get_contenthash($arearecord->content);
 244          $searchparams = array_diff_key((array)$arearecord, ['content' => 1, 'reftable' => 1, 'refid' => 1]);
 245          if ($dbrecord = $DB->get_record(self::DB_AREAS, $searchparams)) {
 246              if ( ! static::content_needs_scheduling($dbrecord->id, $contenthash)) {
 247                  // This is already the latest content record or there is already scheduled record, nothing to do.
 248                  return;
 249              }
 250          } else {
 251              $dbrecord = (object)array_diff_key((array)$arearecord, ['content' => 1]);
 252              $dbrecord->id = $DB->insert_record(self::DB_AREAS, $dbrecord);
 253          }
 254          // Schedule the area for the check. Note that we do not record the contenthash, we will calculate it again
 255          // during the actual check.
 256          $DB->insert_record(self::DB_CONTENT,
 257              (object)['areaid' => $dbrecord->id, 'contenthash' => '', 'timecreated' => time(),
 258                  'status' => self::STATUS_WAITING]);
 259      }
 260  
 261      /**
 262       * Asks all area providers if they have any areas that might have changed as a result of an event and schedules them
 263       *
 264       * @param \core\event\base $event
 265       * @throws \ReflectionException
 266       * @throws \dml_exception
 267       */
 268      public static function find_new_or_updated_areas(\core\event\base $event) {
 269          foreach (static::get_all_areas() as $area) {
 270              if ($records = $area->find_relevant_areas($event)) {
 271                  foreach ($records as $record) {
 272                      static::schedule_area_if_necessary($record);
 273                  }
 274                  $records->close();
 275              }
 276          }
 277      }
 278  
 279      /**
 280       * Returns the current content of the area.
 281       *
 282       * @param \stdClass $arearecord record from the tool_brickfield_areas table
 283       * @return array|null array where the first element is the value of the field and the second element
 284       *    is the 'format' for this field if it is present. If the record was not found null is returned.
 285       * @throws \ddl_exception
 286       * @throws \ddl_table_missing_exception
 287       * @throws \dml_exception
 288       */
 289      protected static function get_area_content(\stdClass $arearecord): array {
 290          global $DB;
 291          if ($arearecord->type == area_base::TYPE_FIELD) {
 292              $tablename = $arearecord->tablename;
 293              $fieldname = $arearecord->fieldorarea;
 294              $itemid = $arearecord->itemid;
 295  
 296              if (!$DB->get_manager()->table_exists($tablename)) {
 297                  return [];
 298              }
 299              if (!$DB->get_manager()->field_exists($tablename, $fieldname)) {
 300                  return [];
 301              }
 302              $fields = $fieldname;
 303              if ($DB->get_manager()->field_exists($tablename, $fieldname . 'format')) {
 304                  $fields .= ',' . $fieldname . 'format';
 305              }
 306              if ($record = $DB->get_record($tablename, ['id' => $itemid], $fields)) {
 307                  return array_values((array)$record);
 308              }
 309          }
 310          return [];
 311      }
 312  
 313      /**
 314       * Asks all area providers if they have any areas that might have changed per courseid and schedules them.
 315       *
 316       * @param int $courseid
 317       * @throws \ReflectionException
 318       * @throws \coding_exception
 319       * @throws \ddl_exception
 320       * @throws \ddl_table_missing_exception
 321       * @throws \dml_exception
 322       */
 323      public static function find_new_or_updated_areas_per_course(int $courseid) {
 324          $totalcount = 0;
 325          foreach (static::get_all_areas() as $area) {
 326              if ($records = $area->find_course_areas($courseid)) {
 327                  foreach ($records as $record) {
 328                      $totalcount++;
 329                      static::schedule_area_if_necessary($record);
 330                  }
 331                  $records->close();
 332              }
 333              // For a site course request, also process the site level areas.
 334              if (($courseid == SITEID) && ($records = $area->find_system_areas())) {
 335                  foreach ($records as $record) {
 336                      $totalcount++;
 337                      // Currently, the courseid in the area table is null if there is a category id.
 338                      if (!empty($record->categoryid)) {
 339                          $record->courseid = null;
 340                      }
 341                      static::schedule_area_if_necessary($record);
 342                  }
 343                  $records->close();
 344              }
 345          }
 346          // Need to run for total count of areas.
 347          static::check_scheduled_areas($totalcount);
 348      }
 349  
 350      /**
 351       * Finds all areas that are waiting to be checked, performs checks. Returns true if there were records processed, false if not.
 352       * To be called from scheduled task
 353       *
 354       * @param int $batch
 355       * @return bool
 356       * @throws \coding_exception
 357       * @throws \ddl_exception
 358       * @throws \ddl_table_missing_exception
 359       * @throws \dml_exception
 360       */
 361      public static function check_scheduled_areas(int $batch = 0): bool {
 362          global $DB;
 363  
 364          $processingtime = 0;
 365          $resultstime = 0;
 366  
 367          $config = get_config(self::PLUGINNAME);
 368          if ($batch == 0) {
 369              $batch = $config->batch;
 370          }
 371          // Creating insert array for courseid cache reruns.
 372          $recordsfound = false;
 373          $batchinserts = [];
 374          echo("Batch amount is ".$batch.", starttime ".time()."\n");
 375          $rs = $DB->get_recordset_sql('SELECT a.*, ch.id AS contentid
 376              FROM {' . self::DB_AREAS. '} a
 377              JOIN {' . self::DB_CONTENT . '} ch ON ch.areaid = a.id
 378              WHERE ch.status = ?
 379              ORDER BY a.id, ch.timecreated, ch.id',
 380              [self::STATUS_WAITING], 0, $batch);
 381  
 382          foreach ($rs as $arearecord) {
 383              $recordsfound = true;
 384              $DB->set_field(self::DB_CONTENT, 'status', self::STATUS_INPROGRESS, ['id' => $arearecord->contentid]);
 385              $content = static::get_area_content($arearecord);
 386              if ($content[0] == null) {
 387                  $content[0] = '';
 388              }
 389              accessibility::run_check($content[0], $arearecord->contentid, $processingtime, $resultstime);
 390  
 391              // Set all content 'iscurrent' fields for this areaid to 0.
 392              $DB->set_field(self::DB_CONTENT, 'iscurrent', 0, ['areaid' => $arearecord->id]);
 393              // Update this content record to be the current record.
 394              $DB->update_record(self::DB_CONTENT,
 395                  (object)['id' => $arearecord->contentid, 'status' => self::STATUS_CHECKED, 'timechecked' => time(),
 396                      'contenthash' => static::get_contenthash($content[0]), 'iscurrent' => 1]);
 397  
 398              // If full caching has been run, then queue, if not in queue already.
 399              if (($arearecord->courseid != null) && static::is_okay_to_cache() &&
 400                  !isset($batchinserts[$arearecord->courseid])) {
 401                  $batchinserts[$arearecord->courseid] = ['courseid' => $arearecord->courseid, 'item' => 'cache'];
 402              }
 403          }
 404  
 405          if (count($batchinserts) > 0) {
 406              $DB->insert_records(self::DB_PROCESS, $batchinserts);
 407          }
 408  
 409          mtrace('Total time in htmlchecker: ' . $processingtime . ' secs.');
 410          mtrace('Total time in results: ' . $resultstime . ' secs.');
 411          return $recordsfound;
 412      }
 413  
 414      /**
 415       * Return true if analysis hasn't been disabled.
 416       * @return bool
 417       * @throws \dml_exception
 418       */
 419      public static function is_okay_to_cache(): bool {
 420          return (analysis::type_is_byrequest());
 421      }
 422  
 423      /**
 424       * Finds all areas that are waiting to be deleted, performs deletions.
 425       *
 426       * @param int $batch limit, can be called from runcli.php
 427       * To be called from scheduled task
 428       * @throws \coding_exception
 429       * @throws \dml_exception
 430       */
 431      public static function check_scheduled_deletions(int $batch = 0) {
 432          global $DB;
 433  
 434          $config = get_config(self::PLUGINNAME);
 435          if ($batch == 0) {
 436              $batch = $config->batch;
 437          }
 438  
 439          // Creating insert array for courseid cache reruns.
 440          $batchinserts = [];
 441  
 442          $rs = $DB->get_recordset(self::DB_PROCESS, ['contextid' => -1, 'timecompleted' => 0], '', '*', 0, $batch);
 443  
 444          foreach ($rs as $record) {
 445  
 446              if ($record->item == "core_course") {
 447                  $tidyparams = ['courseid' => $record->courseid];
 448                  static::delete_summary_data($record->courseid); // Delete cache too.
 449              } else if ($record->item == "course_categories") {
 450                  $tidyparams = ['component' => 'core_course', 'categoryid' => $record->innercontextid];
 451              } else if ($record->item == "course_sections") {
 452                  // Locate course sections, using innercontextid, contextid set to -1 for delete.
 453                  $tidyparams = ['courseid' => $record->courseid, 'component' => 'core_course',
 454                      'tablename' => $record->item, 'itemid' => $record->innercontextid];
 455              } else if ($record->item == "lesson_pages") {
 456                  // Locate lesson pages, using innercontextid, contextid set to -1 for delete.
 457                  $tidyparams = ['courseid' => $record->courseid, 'component' => 'mod_lesson',
 458                      'tablename' => $record->item, 'itemid' => $record->innercontextid];
 459              } else if ($record->item == "book_chapters") {
 460                  // Locate book chapters, using innercontextid, contextid set to -1 for delete.
 461                  $tidyparams = ['courseid' => $record->courseid, 'component' => 'mod_book',
 462                      'tablename' => $record->item, 'itemid' => $record->innercontextid];
 463              } else if ($record->item == "question") {
 464                  // Locate question areas, using innercontextid, contextid set to -1 for delete.
 465                  $tidyparams = [
 466                      'courseid' => $record->courseid, 'component' => 'core_question',
 467                      'tablename' => $record->item, 'itemid' => $record->innercontextid
 468                  ];
 469              } else {
 470                  // Locate specific module instance, using innercontextid, contextid set to -1 for delete.
 471                  $tidyparams = ['courseid' => $record->courseid, 'component' => $record->item,
 472                      'itemid' => $record->innercontextid];
 473              }
 474  
 475              $areas = $DB->get_records(self::DB_AREAS, $tidyparams);
 476              foreach ($areas as $area) {
 477                  static::delete_area_tree($area);
 478              }
 479  
 480              $DB->delete_records(self::DB_PROCESS, ['id' => $record->id]);
 481  
 482              // If full caching has been run, then queue, if not in queue already.
 483              if ($record->courseid != null && static::is_okay_to_cache() && !isset($batchinserts[$record->courseid])) {
 484                  $batchinserts[$record->courseid] = ['courseid' => $record->courseid, 'item' => 'cache'];
 485              }
 486          }
 487          $rs->close();
 488  
 489          if (count($batchinserts) > 0) {
 490              $DB->insert_records(self::DB_PROCESS, $batchinserts);
 491          }
 492      }
 493  
 494      /**
 495       * Checks all queued course updates, and finds all relevant areas.
 496       *
 497       * @param int $batch limit
 498       * To be called from scheduled task
 499       * @throws \ReflectionException
 500       * @throws \dml_exception
 501       */
 502      public static function check_course_updates(int $batch = 0) {
 503          global $DB;
 504  
 505          if ($batch == 0) {
 506              $config = get_config(self::PLUGINNAME);
 507              $batch = $config->batch;
 508          }
 509  
 510          $recs = $DB->get_records(self::DB_PROCESS, ['item' => 'coursererun'], '', 'DISTINCT courseid', 0, $batch);
 511  
 512          foreach ($recs as $record) {
 513              static::find_new_or_updated_areas_per_course($record->courseid);
 514              $DB->delete_records(self::DB_PROCESS, ['courseid' => $record->courseid, 'item' => 'coursererun']);
 515              static::store_result_summary($record->courseid);
 516          }
 517      }
 518  
 519      /**
 520       * Finds all records for a given content area and performs deletions.
 521       *
 522       * To be called from scheduled task
 523       * @param \stdClass $area
 524       * @throws \dml_exception
 525       */
 526      public static function delete_area_tree(\stdClass $area) {
 527          global $DB;
 528  
 529          $contents = $DB->get_records(self::DB_CONTENT, ['areaid' => $area->id]);
 530          foreach ($contents as $content) {
 531              $results = $DB->get_records(self::DB_RESULTS, ['contentid' => $content->id]);
 532              foreach ($results as $result) {
 533                  $DB->delete_records(self::DB_ERRORS, ['resultid' => $result->id]);
 534              }
 535              $DB->delete_records(self::DB_RESULTS, ['contentid' => $content->id]);
 536          }
 537  
 538          // Also, delete all child areas, if existing.
 539          $childparams = ['type' => $area->type, 'reftable' => $area->tablename,
 540              'refid' => $area->itemid];
 541          $childareas = $DB->get_records(self::DB_AREAS, $childparams);
 542          foreach ($childareas as $childarea) {
 543              static::delete_area_tree($childarea);
 544          }
 545  
 546          $DB->delete_records(self::DB_CONTENT, ['areaid' => $area->id]);
 547          $DB->delete_records(self::DB_AREAS, ['id' => $area->id]);
 548      }
 549  
 550      /**
 551       * Finds all records which are no longer current and performs deletions.
 552       *
 553       * To be called from scheduled task.
 554       */
 555      public static function delete_historical_data() {
 556          global $DB;
 557  
 558          $config = get_config(self::PLUGINNAME);
 559  
 560          if ($config->deletehistoricaldata) {
 561              $contents = $DB->get_records(self::DB_CONTENT, ['iscurrent' => 0, 'status' => self::STATUS_CHECKED]);
 562              foreach ($contents as $content) {
 563                  $results = $DB->get_records(self::DB_RESULTS, ['contentid' => $content->id]);
 564                  foreach ($results as $result) {
 565                      $DB->delete_records(self::DB_ERRORS, ['resultid' => $result->id]);
 566                  }
 567                  $DB->delete_records(self::DB_RESULTS, ['contentid' => $content->id]);
 568                  $DB->delete_records(self::DB_CONTENT, ['id' => $content->id]);
 569              }
 570          }
 571      }
 572  
 573      /**
 574       * Finds all summary cache records for a given courseid and performs deletions.
 575       * To be called from scheduled task.
 576       *
 577       * @param int $courseid
 578       * @throws \dml_exception
 579       */
 580      public static function delete_summary_data(int $courseid) {
 581          global $DB;
 582  
 583          if ($courseid == null) {
 584              mtrace('Attempting to run delete_summary_data with no courseid, returning');
 585              return;
 586          }
 587  
 588          $DB->delete_records(self::DB_SUMMARY, ['courseid' => $courseid]);
 589          $DB->delete_records(self::DB_CACHECHECK, ['courseid' => $courseid]);
 590          $DB->delete_records(self::DB_CACHEACTS, ['courseid' => $courseid]);
 591      }
 592  
 593      /**
 594       * Finds all results required to display accessibility report and stores them in the database.
 595       *
 596       * To be called from scheduled task.
 597       * @param int|null $courseid
 598       * @throws \coding_exception
 599       * @throws \dml_exception
 600       */
 601      public static function store_result_summary(int $courseid = null) {
 602          global $DB;
 603  
 604          if (static::is_okay_to_cache() && ($courseid == null)) {
 605              mtrace('Attempting to run update cache with no courseid, returning');
 606              return;
 607          }
 608  
 609          $extrasql = !$courseid ? "" : "AND courseid = ?";
 610          $coursesqlval = !$courseid ? [] : [$courseid];
 611  
 612          // Count of failed activities and count of errors by check.
 613          $errorsql = "SELECT areas.courseid, chx.checkgroup,
 614                  COUNT(DISTINCT (".$DB->sql_concat('areas.contextid', 'areas.component').")) AS failed,
 615                  SUM(res.errorcount) AS errors
 616                  FROM {" . self::DB_AREAS . "} areas
 617                  INNER JOIN {" . self::DB_CONTENT . "} ch ON ch.areaid = areas.id
 618                  INNER JOIN {" . self::DB_RESULTS . "} res ON res.contentid = ch.id
 619                  INNER JOIN {" . self::DB_CHECKS . "} chx ON chx.id = res.checkid
 620                  WHERE res.errorcount > ? AND ch.iscurrent = ? ". $extrasql ." GROUP BY courseid, chx.checkgroup";
 621  
 622          $recordserrored = $DB->get_recordset_sql($errorsql,  array_merge([0, 1], $coursesqlval));
 623  
 624          // Count of failed activities by course.
 625          $failsql = "SELECT areas.courseid,
 626                  COUNT(DISTINCT (".$DB->sql_concat('areas.contextid', 'areas.component').")) AS failed,
 627                  SUM(res.errorcount) AS errors
 628                  FROM {" . self::DB_AREAS . "} areas
 629                  INNER JOIN {" . self::DB_CONTENT . "} ch ON ch.areaid = areas.id
 630                  INNER JOIN {" . self::DB_RESULTS . "} res ON res.contentid = ch.id
 631                  WHERE res.errorcount > ? AND ch.iscurrent = ? ". $extrasql ." GROUP BY courseid";
 632  
 633          $recordsfailed = $DB->get_recordset_sql($failsql, array_merge([0, 1], $coursesqlval));
 634  
 635          $extrasql = !$courseid ? "" : "WHERE courseid = ?";
 636          // Count of activities per course.
 637          $countsql = "SELECT courseid, COUNT(DISTINCT (".$DB->sql_concat('areas.contextid', 'areas.component').")) AS activities
 638                  FROM {" . self::DB_AREAS . "} areas ". $extrasql ." GROUP BY areas.courseid";
 639  
 640          $recordscount = $DB->get_records_sql($countsql, $coursesqlval);
 641  
 642          $final = [];
 643          $values = [];
 644  
 645          foreach ($recordscount as $countrecord) {
 646              $final[$countrecord->courseid] = array_pad(array(), 8,
 647                      [self::SUMMARY_ERROR => 0, self::SUMMARY_FAILED => 0, self::SUMMARY_PERCENT => 100]
 648                  ) + [
 649                      "activitiespassed" => $countrecord->activities,
 650                      "activitiesfailed" => 0,
 651                      "activities" => $countrecord->activities
 652                  ];
 653          }
 654  
 655          foreach ($recordsfailed as $failedrecord) {
 656              $final[$failedrecord->courseid]["activitiespassed"] -= $failedrecord->failed;
 657              $final[$failedrecord->courseid]["activitiesfailed"] += $failedrecord->failed;
 658          }
 659  
 660          foreach ($recordserrored as $errorrecord) {
 661              $final[$errorrecord->courseid][$errorrecord->checkgroup][self::SUMMARY_ERROR] = $errorrecord->errors;
 662              $final[$errorrecord->courseid][$errorrecord->checkgroup][self::SUMMARY_FAILED] = $errorrecord->failed;
 663              $final[$errorrecord->courseid][$errorrecord->checkgroup][self::SUMMARY_PERCENT] = round(100 * (1 -
 664                      ($final[$errorrecord->courseid][$errorrecord->checkgroup][self::SUMMARY_FAILED]
 665                          / $final[$errorrecord->courseid]["activities"])));
 666          }
 667  
 668          foreach ($recordscount as $course) {
 669              if (!$course->courseid) {
 670                  continue;
 671              }
 672              $element = [
 673                  'courseid' => $course->courseid,
 674                  'status' => self::STATUS_CHECKED,
 675                  'activities' => $final[$course->courseid]["activities"],
 676                  'activitiespassed' => $final[$course->courseid]["activitiespassed"],
 677                  'activitiesfailed' => $final[$course->courseid]["activitiesfailed"],
 678                  'errorschecktype1' => $final[$course->courseid][area_base::CHECKGROUP_FORM][self::SUMMARY_ERROR],
 679                  'errorschecktype2' => $final[$course->courseid][area_base::CHECKGROUP_IMAGE][self::SUMMARY_ERROR],
 680                  'errorschecktype3' => $final[$course->courseid][area_base::CHECKGROUP_LAYOUT][self::SUMMARY_ERROR],
 681                  'errorschecktype4' => $final[$course->courseid][area_base::CHECKGROUP_LINK][self::SUMMARY_ERROR],
 682                  'errorschecktype5' => $final[$course->courseid][area_base::CHECKGROUP_MEDIA][self::SUMMARY_ERROR],
 683                  'errorschecktype6' => $final[$course->courseid][area_base::CHECKGROUP_TABLE][self::SUMMARY_ERROR],
 684                  'errorschecktype7' => $final[$course->courseid][area_base::CHECKGROUP_TEXT][self::SUMMARY_ERROR],
 685                  'failedchecktype1' => $final[$course->courseid][area_base::CHECKGROUP_FORM][self::SUMMARY_FAILED],
 686                  'failedchecktype2' => $final[$course->courseid][area_base::CHECKGROUP_IMAGE][self::SUMMARY_FAILED],
 687                  'failedchecktype3' => $final[$course->courseid][area_base::CHECKGROUP_LAYOUT][self::SUMMARY_FAILED],
 688                  'failedchecktype4' => $final[$course->courseid][area_base::CHECKGROUP_LINK][self::SUMMARY_FAILED],
 689                  'failedchecktype5' => $final[$course->courseid][area_base::CHECKGROUP_MEDIA][self::SUMMARY_FAILED],
 690                  'failedchecktype6' => $final[$course->courseid][area_base::CHECKGROUP_TABLE][self::SUMMARY_FAILED],
 691                  'failedchecktype7' => $final[$course->courseid][area_base::CHECKGROUP_TEXT][self::SUMMARY_FAILED],
 692                  'percentchecktype1' => $final[$course->courseid][area_base::CHECKGROUP_FORM][self::SUMMARY_PERCENT],
 693                  'percentchecktype2' => $final[$course->courseid][area_base::CHECKGROUP_IMAGE][self::SUMMARY_PERCENT],
 694                  'percentchecktype3' => $final[$course->courseid][area_base::CHECKGROUP_LAYOUT][self::SUMMARY_PERCENT],
 695                  'percentchecktype4' => $final[$course->courseid][area_base::CHECKGROUP_LINK][self::SUMMARY_PERCENT],
 696                  'percentchecktype5' => $final[$course->courseid][area_base::CHECKGROUP_MEDIA][self::SUMMARY_PERCENT],
 697                  'percentchecktype6' => $final[$course->courseid][area_base::CHECKGROUP_TABLE][self::SUMMARY_PERCENT],
 698                  'percentchecktype7' => $final[$course->courseid][area_base::CHECKGROUP_TEXT][self::SUMMARY_PERCENT]
 699              ];
 700              $resultid = $DB->get_field(self::DB_SUMMARY, 'id', ['courseid' => $course->courseid]);
 701              if ($resultid) {
 702                  $element['id'] = $resultid;
 703                  $DB->update_record(self::DB_SUMMARY, (object)$element);
 704                  continue;
 705              }
 706              array_push($values, $element);
 707          }
 708  
 709          $DB->insert_records(self::DB_SUMMARY, $values);
 710  
 711          $extrasql = !$courseid ? "WHERE courseid != ?" : "WHERE courseid = ?";
 712          $coursesqlval = !$courseid ? [0] : [$courseid];
 713          // Count of failed errors per check.
 714          $checkssql = "SELECT area.courseid, ".self::STATUS_CHECKED." AS status, res.checkid,
 715                  COUNT(res.errorcount) as checkcount, SUM(res.errorcount) AS errorcount
 716                  FROM {" . self::DB_AREAS . "} area
 717                  INNER JOIN {" . self::DB_CONTENT . "} ch ON ch.areaid = area.id AND ch.iscurrent = 1
 718                  INNER JOIN {" . self::DB_RESULTS . "} res ON res.contentid = ch.id
 719                  ".$extrasql." GROUP BY area.courseid, res.checkid";
 720  
 721          $checksresult = $DB->get_recordset_sql($checkssql, $coursesqlval);
 722  
 723          $checkvalues = [];
 724          foreach ($checksresult as $check) {
 725              if ($result = $DB->get_record(self::DB_CACHECHECK, ['courseid' => $check->courseid, 'checkid' => $check->checkid])) {
 726                  $check->id = $result->id;
 727                  $DB->update_record(self::DB_CACHECHECK, $check);
 728              } else {
 729                  array_push($checkvalues, (array)$check);
 730              }
 731          }
 732          $DB->insert_records(self::DB_CACHECHECK, $checkvalues);
 733  
 734          // Count of failed or passed rate per activity.
 735          $activitysql = "SELECT courseid, ".self::STATUS_CHECKED." AS status, area.component,
 736                  COUNT(DISTINCT area.contextid) AS totalactivities, 0 AS failedactivities,
 737                  COUNT(DISTINCT area.contextid) AS passedactivities, 0 AS errorcount
 738                  FROM {" . self::DB_AREAS . "} area
 739                  ".$extrasql."
 740                  GROUP BY area.courseid, area.component";
 741  
 742          $activityresults = $DB->get_recordset_sql($activitysql, $coursesqlval);
 743  
 744          $activityvalues = [];
 745  
 746          // Count of failed errors per courseid per activity.
 747          $activityfailedsql = "SELECT area.courseid, area.component, area.contextid, SUM(res.errorcount) AS errorcount
 748                  FROM {" . self::DB_AREAS . "} area
 749                  INNER JOIN {" . self::DB_CONTENT . "} ch ON ch.areaid = area.id AND ch.iscurrent = 1
 750                  INNER JOIN {" . self::DB_RESULTS . "} res ON res.contentid = ch.id
 751                  ".$extrasql." AND res.errorcount != 0
 752                  GROUP BY area.courseid, area.component, area.contextid";
 753  
 754          $activityfailedresults = $DB->get_recordset_sql($activityfailedsql, $coursesqlval);
 755  
 756          foreach ($activityresults as $activity) {
 757              $tmpkey = $activity->courseid.$activity->component;
 758              $activityvalues[$tmpkey] = $activity;
 759          }
 760  
 761          foreach ($activityfailedresults as $failed) {
 762              $tmpkey = $failed->courseid.$failed->component;
 763              $activityvalues[$tmpkey]->failedactivities ++;
 764              $activityvalues[$tmpkey]->passedactivities --;
 765              $activityvalues[$tmpkey]->errorcount += $failed->errorcount;
 766          }
 767  
 768          $activityvaluespush = [];
 769          foreach ($activityvalues as $value) {
 770              if ($result = $DB->get_record(self::DB_CACHEACTS, ['courseid' => $value->courseid, 'component' => $value->component])) {
 771                  $value->id = $result->id;
 772                  $DB->update_record(self::DB_CACHEACTS, $value);
 773              } else {
 774                  array_push($activityvaluespush, (array)$value);
 775              }
 776          }
 777  
 778          $DB->insert_records(self::DB_CACHEACTS, $activityvaluespush);
 779  
 780          $recordserrored->close();
 781          $recordsfailed->close();
 782          $checksresult->close();
 783          $activityresults->close();
 784          $activityfailedresults->close();
 785      }
 786  
 787      /**
 788       * Get course module summary information for a course.
 789       *
 790       * @param   int $courseid
 791       * @return  stdClass[]
 792       */
 793      public static function get_cm_summary_for_course(int $courseid): array {
 794          global $DB;
 795  
 796          $sql = "
 797          SELECT
 798              area.cmid,
 799              sum(errorcount) as numerrors,
 800              count(errorcount) as numchecks
 801           FROM {" . self::DB_AREAS . "} area
 802           JOIN {" . self::DB_CONTENT . "} ch ON ch.areaid = area.id AND ch.iscurrent = 1
 803           JOIN {" . self::DB_RESULTS . "} res ON res.contentid = ch.id
 804          WHERE area.courseid = :courseid AND component != :component
 805       GROUP BY cmid";
 806  
 807          $params = [
 808              'courseid' => $courseid,
 809              'component' => 'core_course',
 810          ];
 811  
 812          return $DB->get_records_sql($sql, $params);
 813      }
 814  
 815      /**
 816       * Get section summary information for a course.
 817       *
 818       * @param   int $courseid
 819       * @return  stdClass[]
 820       */
 821      public static function get_section_summary_for_course(int $courseid): array {
 822          global $DB;
 823  
 824          $sql = "
 825          SELECT
 826              sec.section,
 827              sum(errorcount) AS numerrors,
 828              count(errorcount) as numchecks
 829           FROM {" . self::DB_AREAS . "} area
 830           JOIN {" . self::DB_CONTENT . "} ch ON ch.areaid = area.id AND ch.iscurrent = 1
 831           JOIN {" . self::DB_RESULTS . "} res ON res.contentid = ch.id
 832           JOIN {course_sections} sec ON area.itemid = sec.id
 833          WHERE area.tablename = :tablename AND area.courseid = :courseid
 834       GROUP BY sec.section";
 835  
 836          $params = [
 837              'courseid' => $courseid,
 838              'tablename' => 'course_sections'
 839          ];
 840  
 841          return $DB->get_records_sql($sql, $params);
 842      }
 843  }