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  /**
  18   * Upgrade helper functions
  19   *
  20   * This file is used for special upgrade functions - for example groups and gradebook.
  21   * These functions must use SQL and database related functions only- no other Moodle API,
  22   * because it might depend on db structures that are not yet present during upgrade.
  23   * (Do not use functions from accesslib.php, grades classes or group functions at all!)
  24   *
  25   * @package   core_install
  26   * @category  upgrade
  27   * @copyright 2007 Petr Skoda (http://skodak.org)
  28   * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  29   */
  30  
  31  defined('MOODLE_INTERNAL') || die();
  32  
  33  /**
  34   * Returns all non-view and non-temp tables with sane names.
  35   * Prints list of non-supported tables using $OUTPUT->notification()
  36   *
  37   * @return array
  38   */
  39  function upgrade_mysql_get_supported_tables() {
  40      global $OUTPUT, $DB;
  41  
  42      $tables = array();
  43      $patprefix = str_replace('_', '\\_', $DB->get_prefix());
  44      $pregprefix = preg_quote($DB->get_prefix(), '/');
  45  
  46      $sql = "SHOW FULL TABLES LIKE '$patprefix%'";
  47      $rs = $DB->get_recordset_sql($sql);
  48      foreach ($rs as $record) {
  49          $record = array_change_key_case((array)$record, CASE_LOWER);
  50          $type = $record['table_type'];
  51          unset($record['table_type']);
  52          $fullname = array_shift($record);
  53  
  54          if ($pregprefix === '') {
  55              $name = $fullname;
  56          } else {
  57              $count = null;
  58              $name = preg_replace("/^$pregprefix/", '', $fullname, -1, $count);
  59              if ($count !== 1) {
  60                  continue;
  61              }
  62          }
  63  
  64          if (!preg_match("/^[a-z][a-z0-9_]*$/", $name)) {
  65              echo $OUTPUT->notification("Database table with invalid name '$fullname' detected, skipping.", 'notifyproblem');
  66              continue;
  67          }
  68          if ($type === 'VIEW') {
  69              echo $OUTPUT->notification("Unsupported database table view '$fullname' detected, skipping.", 'notifyproblem');
  70              continue;
  71          }
  72          $tables[$name] = $name;
  73      }
  74      $rs->close();
  75  
  76      return $tables;
  77  }
  78  
  79  /**
  80   * Using data for a single course-module that has groupmembersonly enabled,
  81   * returns the new availability value that incorporates the correct
  82   * groupmembersonly option.
  83   *
  84   * Included as a function so that it can be shared between upgrade and restore,
  85   * and unit-tested.
  86   *
  87   * @param int $groupingid Grouping id for the course-module (0 if none)
  88   * @param string $availability Availability JSON data for the module (null if none)
  89   * @return string New value for availability for the module
  90   */
  91  function upgrade_group_members_only($groupingid, $availability) {
  92      // Work out the new JSON object representing this option.
  93      if ($groupingid) {
  94          // Require specific grouping.
  95          $condition = (object)array('type' => 'grouping', 'id' => (int)$groupingid);
  96      } else {
  97          // No grouping specified, so require membership of any group.
  98          $condition = (object)array('type' => 'group');
  99      }
 100  
 101      if (is_null($availability)) {
 102          // If there are no conditions using the new API then just set it.
 103          $tree = (object)array('op' => '&', 'c' => array($condition), 'showc' => array(false));
 104      } else {
 105          // There are existing conditions.
 106          $tree = json_decode($availability);
 107          switch ($tree->op) {
 108              case '&' :
 109                  // For & conditions we can just add this one.
 110                  $tree->c[] = $condition;
 111                  $tree->showc[] = false;
 112                  break;
 113              case '!|' :
 114                  // For 'not or' conditions we can add this one
 115                  // but negated.
 116                  $tree->c[] = (object)array('op' => '!&', 'c' => array($condition));
 117                  $tree->showc[] = false;
 118                  break;
 119              default:
 120                  // For the other two (OR and NOT AND) we have to add
 121                  // an extra level to the tree.
 122                  $tree = (object)array('op' => '&', 'c' => array($tree, $condition),
 123                          'showc' => array($tree->show, false));
 124                  // Inner trees do not have a show option, so remove it.
 125                  unset($tree->c[0]->show);
 126                  break;
 127          }
 128      }
 129  
 130      return json_encode($tree);
 131  }
 132  
 133  /**
 134   * Marks all courses with changes in extra credit weight calculation
 135   *
 136   * Used during upgrade and in course restore process
 137   *
 138   * This upgrade script is needed because we changed the algorithm for calculating the automatic weights of extra
 139   * credit items and want to prevent changes in the existing student grades.
 140   *
 141   * @param int $onlycourseid
 142   */
 143  function upgrade_extra_credit_weightoverride($onlycourseid = 0) {
 144      global $DB;
 145  
 146      // Find all courses that have categories in Natural aggregation method where there is at least one extra credit
 147      // item and at least one item with overridden weight.
 148      $courses = $DB->get_fieldset_sql(
 149          "SELECT DISTINCT gc.courseid
 150            FROM {grade_categories} gc
 151            INNER JOIN {grade_items} gi ON gc.id = gi.categoryid AND gi.weightoverride = :weightoverriden
 152            INNER JOIN {grade_items} gie ON gc.id = gie.categoryid AND gie.aggregationcoef = :extracredit
 153            WHERE gc.aggregation = :naturalaggmethod" . ($onlycourseid ? " AND gc.courseid = :onlycourseid" : ''),
 154          array('naturalaggmethod' => 13,
 155              'weightoverriden' => 1,
 156              'extracredit' => 1,
 157              'onlycourseid' => $onlycourseid,
 158          )
 159      );
 160      foreach ($courses as $courseid) {
 161          $gradebookfreeze = get_config('core', 'gradebook_calculations_freeze_' . $courseid);
 162          if (!$gradebookfreeze) {
 163              set_config('gradebook_calculations_freeze_' . $courseid, 20150619);
 164          }
 165      }
 166  }
 167  
 168  /**
 169   * Marks all courses that require calculated grade items be updated.
 170   *
 171   * Used during upgrade and in course restore process.
 172   *
 173   * This upgrade script is needed because the calculated grade items were stuck with a maximum of 100 and could be changed.
 174   * This flags the courses that are affected and the grade book is frozen to retain grade integrity.
 175   *
 176   * @param int $courseid Specify a course ID to run this script on just one course.
 177   */
 178  function upgrade_calculated_grade_items($courseid = null) {
 179      global $DB, $CFG;
 180  
 181      $affectedcourses = array();
 182      $possiblecourseids = array();
 183      $params = array();
 184      $singlecoursesql = '';
 185      if (isset($courseid)) {
 186          $singlecoursesql = "AND ns.id = :courseid";
 187          $params['courseid'] = $courseid;
 188      }
 189      $siteminmaxtouse = 1;
 190      if (isset($CFG->grade_minmaxtouse)) {
 191          $siteminmaxtouse = $CFG->grade_minmaxtouse;
 192      }
 193      $courseidsql = "SELECT ns.id
 194                        FROM (
 195                          SELECT c.id, coalesce(" . $DB->sql_compare_text('gs.value') . ", :siteminmax) AS gradevalue
 196                            FROM {course} c
 197                            LEFT JOIN {grade_settings} gs
 198                              ON c.id = gs.courseid
 199                             AND ((gs.name = 'minmaxtouse' AND " . $DB->sql_compare_text('gs.value') . " = '2'))
 200                          ) ns
 201                      WHERE " . $DB->sql_compare_text('ns.gradevalue') . " = '2' $singlecoursesql";
 202      $params['siteminmax'] = $siteminmaxtouse;
 203      $courses = $DB->get_records_sql($courseidsql, $params);
 204      foreach ($courses as $course) {
 205          $possiblecourseids[$course->id] = $course->id;
 206      }
 207  
 208      if (!empty($possiblecourseids)) {
 209          list($sql, $params) = $DB->get_in_or_equal($possiblecourseids);
 210          // A calculated grade item grade min != 0 and grade max != 100 and the course setting is set to
 211          // "Initial min and max grades".
 212          $coursesql = "SELECT DISTINCT courseid
 213                          FROM {grade_items}
 214                         WHERE calculation IS NOT NULL
 215                           AND itemtype = 'manual'
 216                           AND (grademax <> 100 OR grademin <> 0)
 217                           AND courseid $sql";
 218          $affectedcourses = $DB->get_records_sql($coursesql, $params);
 219      }
 220  
 221      // Check for second type of affected courses.
 222      // If we already have the courseid parameter set in the affectedcourses then there is no need to run through this section.
 223      if (!isset($courseid) || !in_array($courseid, $affectedcourses)) {
 224          $singlecoursesql = '';
 225          $params = array();
 226          if (isset($courseid)) {
 227              $singlecoursesql = "AND courseid = :courseid";
 228              $params['courseid'] = $courseid;
 229          }
 230          $nestedsql = "SELECT id
 231                          FROM {grade_items}
 232                         WHERE itemtype = 'category'
 233                           AND calculation IS NOT NULL $singlecoursesql";
 234          $calculatedgradecategories = $DB->get_records_sql($nestedsql, $params);
 235          $categoryids = array();
 236          foreach ($calculatedgradecategories as $key => $gradecategory) {
 237              $categoryids[$key] = $gradecategory->id;
 238          }
 239  
 240          if (!empty($categoryids)) {
 241              list($sql, $params) = $DB->get_in_or_equal($categoryids);
 242              // A category with a calculation where the raw grade min and the raw grade max don't match the grade min and grade max
 243              // for the category.
 244              $coursesql = "SELECT DISTINCT gi.courseid
 245                              FROM {grade_grades} gg, {grade_items} gi
 246                             WHERE gi.id = gg.itemid
 247                               AND (gg.rawgrademax <> gi.grademax OR gg.rawgrademin <> gi.grademin)
 248                               AND gi.id $sql";
 249              $additionalcourses = $DB->get_records_sql($coursesql, $params);
 250              foreach ($additionalcourses as $key => $additionalcourse) {
 251                  if (!array_key_exists($key, $affectedcourses)) {
 252                      $affectedcourses[$key] = $additionalcourse;
 253                  }
 254              }
 255          }
 256      }
 257  
 258      foreach ($affectedcourses as $affectedcourseid) {
 259          if (isset($CFG->upgrade_calculatedgradeitemsonlyregrade) && !($courseid)) {
 260              $DB->set_field('grade_items', 'needsupdate', 1, array('courseid' => $affectedcourseid->courseid));
 261          } else {
 262              // Check to see if the gradebook freeze is already in affect.
 263              $gradebookfreeze = get_config('core', 'gradebook_calculations_freeze_' . $affectedcourseid->courseid);
 264              if (!$gradebookfreeze) {
 265                  set_config('gradebook_calculations_freeze_' . $affectedcourseid->courseid, 20150627);
 266              }
 267          }
 268      }
 269  }
 270  
 271  /**
 272   * This function creates a default separated/connected scale
 273   * so there's something in the database.  The locations of
 274   * strings and files is a bit odd, but this is because we
 275   * need to maintain backward compatibility with many different
 276   * existing language translations and older sites.
 277   *
 278   * @global object
 279   * @return void
 280   */
 281  function make_default_scale() {
 282      global $DB;
 283  
 284      $defaultscale = new stdClass();
 285      $defaultscale->courseid = 0;
 286      $defaultscale->userid = 0;
 287      $defaultscale->name  = get_string('separateandconnected');
 288      $defaultscale->description = get_string('separateandconnectedinfo');
 289      $defaultscale->scale = get_string('postrating1', 'forum').','.
 290                             get_string('postrating2', 'forum').','.
 291                             get_string('postrating3', 'forum');
 292      $defaultscale->timemodified = time();
 293  
 294      $defaultscale->id = $DB->insert_record('scale', $defaultscale);
 295      return $defaultscale;
 296  }
 297  
 298  
 299  /**
 300   * Create another default scale.
 301   *
 302   * @param int $oldversion
 303   * @return bool always true
 304   */
 305  function make_competence_scale() {
 306      global $DB;
 307  
 308      $defaultscale = new stdClass();
 309      $defaultscale->courseid = 0;
 310      $defaultscale->userid = 0;
 311      $defaultscale->name  = get_string('defaultcompetencescale');
 312      $defaultscale->description = get_string('defaultcompetencescaledesc');
 313      $defaultscale->scale = get_string('defaultcompetencescalenotproficient').','.
 314                             get_string('defaultcompetencescaleproficient');
 315      $defaultscale->timemodified = time();
 316  
 317      $defaultscale->id = $DB->insert_record('scale', $defaultscale);
 318      return $defaultscale;
 319  }
 320  
 321  /**
 322   * Marks all courses that require rounded grade items be updated.
 323   *
 324   * Used during upgrade and in course restore process.
 325   *
 326   * This upgrade script is needed because it has been decided that if a grade is rounded up, and it will changed a letter
 327   * grade or satisfy a course completion grade criteria, then it should be set as so, and the letter will be awarded and or
 328   * the course completion grade will be awarded.
 329   *
 330   * @param int $courseid Specify a course ID to run this script on just one course.
 331   */
 332  function upgrade_course_letter_boundary($courseid = null) {
 333      global $DB, $CFG;
 334  
 335      $coursesql = '';
 336      $params = array('contextlevel' => CONTEXT_COURSE);
 337      if (!empty($courseid)) {
 338          $coursesql = 'AND c.id = :courseid';
 339          $params['courseid'] = $courseid;
 340      }
 341  
 342      // Check to see if the system letter boundaries are borked.
 343      $systemcontext = context_system::instance();
 344      $systemneedsfreeze = upgrade_letter_boundary_needs_freeze($systemcontext);
 345  
 346      // Check the setting for showing the letter grade in a column (default is false).
 347      $usergradelettercolumnsetting = 0;
 348      if (isset($CFG->grade_report_user_showlettergrade)) {
 349          $usergradelettercolumnsetting = (int)$CFG->grade_report_user_showlettergrade;
 350      }
 351      $lettercolumnsql = '';
 352      if ($usergradelettercolumnsetting) {
 353          // The system default is to show a column with letters (and the course uses the defaults).
 354          $lettercolumnsql = '(gss.value is NULL OR ' . $DB->sql_compare_text('gss.value') .  ' <> \'0\')';
 355      } else {
 356          // The course displays a column with letters.
 357          $lettercolumnsql = $DB->sql_compare_text('gss.value') .  ' = \'1\'';
 358      }
 359  
 360      // 3, 13, 23, 31, and 32 are the grade display types that incorporate showing letters. See lib/grade/constants/php.
 361      $systemusesletters = (int) (isset($CFG->grade_displaytype) && in_array($CFG->grade_displaytype, array(3, 13, 23, 31, 32)));
 362      $systemletters = $systemusesletters || $usergradelettercolumnsetting;
 363  
 364      $contextselect = context_helper::get_preload_record_columns_sql('ctx');
 365  
 366      if ($systemletters && $systemneedsfreeze) {
 367          // Select courses with no grade setting for display and a grade item that is using the default display,
 368          // but have not altered the course letter boundary configuration. These courses are definitely affected.
 369  
 370          $sql = "SELECT DISTINCT c.id AS courseid
 371                    FROM {course} c
 372                    JOIN {grade_items} gi ON c.id = gi.courseid
 373                    JOIN {context} ctx ON ctx.instanceid = c.id AND ctx.contextlevel = :contextlevel
 374               LEFT JOIN {grade_settings} gs ON gs.courseid = c.id AND gs.name = 'displaytype'
 375               LEFT JOIN {grade_settings} gss ON gss.courseid = c.id AND gss.name = 'report_user_showlettergrade'
 376               LEFT JOIN {grade_letters} gl ON gl.contextid = ctx.id
 377                   WHERE gi.display = 0
 378                   AND ((gs.value is NULL)
 379                        AND ($lettercolumnsql))
 380                   AND gl.id is NULL $coursesql";
 381          $affectedcourseids = $DB->get_recordset_sql($sql, $params);
 382          foreach ($affectedcourseids as $courseid) {
 383              set_config('gradebook_calculations_freeze_' . $courseid->courseid, 20160518);
 384          }
 385          $affectedcourseids->close();
 386      }
 387  
 388      // If the system letter boundary is okay proceed to check grade item and course grade display settings.
 389      $sql = "SELECT DISTINCT c.id AS courseid, $contextselect
 390                FROM {course} c
 391                JOIN {context} ctx ON ctx.instanceid = c.id AND ctx.contextlevel = :contextlevel
 392                JOIN {grade_items} gi ON c.id = gi.courseid
 393           LEFT JOIN {grade_settings} gs ON c.id = gs.courseid AND gs.name = 'displaytype'
 394           LEFT JOIN {grade_settings} gss ON gss.courseid = c.id AND gss.name = 'report_user_showlettergrade'
 395               WHERE
 396                  (
 397                      -- A grade item is using letters
 398                      (gi.display IN (3, 13, 23, 31, 32))
 399                      -- OR the course is using letters
 400                      OR (" . $DB->sql_compare_text('gs.value') . " IN ('3', '13', '23', '31', '32')
 401                          -- OR the course using the system default which is letters
 402                          OR (gs.value IS NULL AND $systemusesletters = 1)
 403                      )
 404                      OR ($lettercolumnsql)
 405                  )
 406                  -- AND the course matches
 407                  $coursesql";
 408  
 409      $potentialcourses = $DB->get_recordset_sql($sql, $params);
 410  
 411      foreach ($potentialcourses as $value) {
 412          $gradebookfreeze = 'gradebook_calculations_freeze_' . $value->courseid;
 413  
 414          // Check also if this course id has already been frozen.
 415          // If we already have this course ID then move on to the next record.
 416          if (!property_exists($CFG, $gradebookfreeze)) {
 417              // Check for 57 letter grade issue.
 418              context_helper::preload_from_record($value);
 419              $coursecontext = context_course::instance($value->courseid);
 420              if (upgrade_letter_boundary_needs_freeze($coursecontext)) {
 421                  // We have a course with a possible score standardisation problem. Flag for freeze.
 422                  // Flag this course as being frozen.
 423                  set_config('gradebook_calculations_freeze_' . $value->courseid, 20160518);
 424              }
 425          }
 426      }
 427      $potentialcourses->close();
 428  }
 429  
 430  /**
 431   * Checks the letter boundary of the provided context to see if it needs freezing.
 432   * Each letter boundary is tested to see if receiving that boundary number will
 433   * result in achieving the cosponsoring letter.
 434   *
 435   * @param object $context Context object
 436   * @return bool if the letter boundary for this context should be frozen.
 437   */
 438  function upgrade_letter_boundary_needs_freeze($context) {
 439      global $DB;
 440  
 441      $contexts = $context->get_parent_context_ids();
 442      array_unshift($contexts, $context->id);
 443  
 444      foreach ($contexts as $ctxid) {
 445  
 446          $letters = $DB->get_records_menu('grade_letters', array('contextid' => $ctxid), 'lowerboundary DESC',
 447                  'lowerboundary, letter');
 448  
 449          if (!empty($letters)) {
 450              foreach ($letters as $boundary => $notused) {
 451                  $standardisedboundary = upgrade_standardise_score($boundary, 0, 100, 0, 100);
 452                  if ($standardisedboundary < $boundary) {
 453                      return true;
 454                  }
 455              }
 456              // We found letters but we have no boundary problem.
 457              return false;
 458          }
 459      }
 460      return false;
 461  }
 462  
 463  /**
 464   * Given a float value situated between a source minimum and a source maximum, converts it to the
 465   * corresponding value situated between a target minimum and a target maximum. Thanks to Darlene
 466   * for the formula :-)
 467   *
 468   * @param float $rawgrade
 469   * @param float $sourcemin
 470   * @param float $sourcemax
 471   * @param float $targetmin
 472   * @param float $targetmax
 473   * @return float Converted value
 474   */
 475  function upgrade_standardise_score($rawgrade, $sourcemin, $sourcemax, $targetmin, $targetmax) {
 476      if (is_null($rawgrade)) {
 477          return null;
 478      }
 479  
 480      if ($sourcemax == $sourcemin or $targetmin == $targetmax) {
 481          // Prevent division by 0.
 482          return $targetmax;
 483      }
 484  
 485      $factor = ($rawgrade - $sourcemin) / ($sourcemax - $sourcemin);
 486      $diff = $targetmax - $targetmin;
 487      $standardisedvalue = $factor * $diff + $targetmin;
 488      return $standardisedvalue;
 489  }
 490  
 491  /**
 492   * Provides a way to check and update a serialized string that uses the deprecated object class.
 493   *
 494   * @param  string $serializeddata Serialized string which may contain the now deprecated object.
 495   * @return array Returns an array where the first variable is a bool with a status of whether the initial data was changed
 496   * or not. The second variable is the said data.
 497   */
 498  function upgrade_fix_serialized_objects($serializeddata) {
 499      $updated = false;
 500      if (strpos($serializeddata, ":6:\"object") !== false) {
 501          $serializeddata = str_replace(":6:\"object", ":8:\"stdClass", $serializeddata);
 502          $updated = true;
 503      }
 504      return [$updated, $serializeddata];
 505  }
 506  
 507  /**
 508   * Deletes file records which have their repository deleted.
 509   *
 510   */
 511  function upgrade_delete_orphaned_file_records() {
 512      global $DB;
 513  
 514      $sql = "SELECT f.id, f.contextid, f.component, f.filearea, f.itemid, fr.id AS referencefileid
 515                FROM {files} f
 516                JOIN {files_reference} fr ON f.referencefileid = fr.id
 517           LEFT JOIN {repository_instances} ri ON fr.repositoryid = ri.id
 518               WHERE ri.id IS NULL";
 519  
 520      $deletedfiles = $DB->get_recordset_sql($sql);
 521  
 522      $deletedfileids = array();
 523  
 524      $fs = get_file_storage();
 525      foreach ($deletedfiles as $deletedfile) {
 526          $fs->delete_area_files($deletedfile->contextid, $deletedfile->component, $deletedfile->filearea, $deletedfile->itemid);
 527          $deletedfileids[] = $deletedfile->referencefileid;
 528      }
 529      $deletedfiles->close();
 530  
 531      $DB->delete_records_list('files_reference', 'id', $deletedfileids);
 532  }
 533  
 534  /**
 535   * Updates the existing prediction actions in the database according to the new suggested actions.
 536   * @return null
 537   */
 538  function upgrade_rename_prediction_actions_useful_incorrectly_flagged() {
 539      global $DB;
 540  
 541      // The update depends on the analyser class used by each model so we need to iterate through the models in the system.
 542      $modelids = $DB->get_records_sql("SELECT DISTINCT am.id, am.target
 543                                          FROM {analytics_models} am
 544                                          JOIN {analytics_predictions} ap ON ap.modelid = am.id
 545                                          JOIN {analytics_prediction_actions} apa ON ap.id = apa.predictionid");
 546      foreach ($modelids as $model) {
 547          $targetname = $model->target;
 548          if (!class_exists($targetname)) {
 549              // The plugin may not be available.
 550              continue;
 551          }
 552          $target = new $targetname();
 553  
 554          $analyserclass = $target->get_analyser_class();
 555          if (!class_exists($analyserclass)) {
 556              // The plugin may not be available.
 557              continue;
 558          }
 559  
 560          if ($analyserclass::one_sample_per_analysable()) {
 561              // From 'fixed' to 'useful'.
 562              $params = ['oldaction' => 'fixed', 'newaction' => 'useful'];
 563          } else {
 564              // From 'notuseful' to 'incorrectlyflagged'.
 565              $params = ['oldaction' => 'notuseful', 'newaction' => 'incorrectlyflagged'];
 566          }
 567  
 568          $subsql = "SELECT id FROM {analytics_predictions} WHERE modelid = :modelid";
 569          $updatesql = "UPDATE {analytics_prediction_actions}
 570                           SET actionname = :newaction
 571                         WHERE predictionid IN ($subsql) AND actionname = :oldaction";
 572  
 573          $DB->execute($updatesql, $params + ['modelid' => $model->id]);
 574      }
 575  }
 576  
 577  /**
 578   * Convert the site settings for the 'hub' component in the config_plugins table.
 579   *
 580   * @param stdClass $hubconfig Settings loaded for the 'hub' component.
 581   * @param string $huburl The URL of the hub to use as the valid one in case of conflict.
 582   * @return stdClass List of new settings to be applied (including null values to be unset).
 583   */
 584  function upgrade_convert_hub_config_site_param_names(stdClass $hubconfig, string $huburl): stdClass {
 585  
 586      $cleanhuburl = clean_param($huburl, PARAM_ALPHANUMEXT);
 587      $converted = [];
 588  
 589      foreach ($hubconfig as $oldname => $value) {
 590          if (preg_match('/^site_([a-z]+)([A-Za-z0-9_-]*)/', $oldname, $matches)) {
 591              $newname = 'site_'.$matches[1];
 592  
 593              if ($oldname === $newname) {
 594                  // There is an existing value with the new naming convention already.
 595                  $converted[$newname] = $value;
 596  
 597              } else if (!array_key_exists($newname, $converted)) {
 598                  // Add the value under a new name and mark the original to be unset.
 599                  $converted[$newname] = $value;
 600                  $converted[$oldname] = null;
 601  
 602              } else if ($matches[2] === '_'.$cleanhuburl) {
 603                  // The new name already exists, overwrite only if coming from the valid hub.
 604                  $converted[$newname] = $value;
 605                  $converted[$oldname] = null;
 606  
 607              } else {
 608                  // Just unset the old value.
 609                  $converted[$oldname] = null;
 610              }
 611  
 612          } else {
 613              // Not a hub-specific site setting, just keep it.
 614              $converted[$oldname] = $value;
 615          }
 616      }
 617  
 618      return (object) $converted;
 619  }
 620  
 621  /**
 622   * Fix the incorrect default values inserted into analytics contextids field.
 623   */
 624  function upgrade_analytics_fix_contextids_defaults() {
 625      global $DB;
 626  
 627      $select = $DB->sql_compare_text('contextids') . ' = :zero OR ' . $DB->sql_compare_text('contextids') . ' = :null';
 628      $params = ['zero' => '0', 'null' => 'null'];
 629      $DB->execute("UPDATE {analytics_models} set contextids = null WHERE " . $select, $params);
 630  }
 631  
 632  /**
 633   * Upgrade core licenses shipped with Moodle.
 634   */
 635  function upgrade_core_licenses() {
 636      global $CFG, $DB;
 637  
 638      $corelicenses = [];
 639  
 640      $license = new stdClass();
 641      $license->shortname = 'unknown';
 642      $license->fullname = 'Licence not specified';
 643      $license->source = '';
 644      $license->enabled = 1;
 645      $license->version = '2010033100';
 646      $license->custom = 0;
 647      $corelicenses[] = $license;
 648  
 649      $license = new stdClass();
 650      $license->shortname = 'allrightsreserved';
 651      $license->fullname = 'All rights reserved';
 652      $license->source = 'https://en.wikipedia.org/wiki/All_rights_reserved';
 653      $license->enabled = 1;
 654      $license->version = '2010033100';
 655      $license->custom = 0;
 656      $corelicenses[] = $license;
 657  
 658      $license = new stdClass();
 659      $license->shortname = 'public';
 660      $license->fullname = 'Public domain';
 661      $license->source = 'https://en.wikipedia.org/wiki/Public_domain';
 662      $license->enabled = 1;
 663      $license->version = '2010033100';
 664      $license->custom = 0;
 665      $corelicenses[] = $license;
 666  
 667      $license = new stdClass();
 668      $license->shortname = 'cc';
 669      $license->fullname = 'Creative Commons';
 670      $license->source = 'https://creativecommons.org/licenses/by/3.0/';
 671      $license->enabled = 1;
 672      $license->version = '2010033100';
 673      $license->custom = 0;
 674      $corelicenses[] = $license;
 675  
 676      $license = new stdClass();
 677      $license->shortname = 'cc-nd';
 678      $license->fullname = 'Creative Commons - NoDerivs';
 679      $license->source = 'https://creativecommons.org/licenses/by-nd/3.0/';
 680      $license->enabled = 1;
 681      $license->version = '2010033100';
 682      $license->custom = 0;
 683      $corelicenses[] = $license;
 684  
 685      $license = new stdClass();
 686      $license->shortname = 'cc-nc-nd';
 687      $license->fullname = 'Creative Commons - No Commercial NoDerivs';
 688      $license->source = 'https://creativecommons.org/licenses/by-nc-nd/3.0/';
 689      $license->enabled = 1;
 690      $license->version = '2010033100';
 691      $license->custom = 0;
 692      $corelicenses[] = $license;
 693  
 694      $license = new stdClass();
 695      $license->shortname = 'cc-nc';
 696      $license->fullname = 'Creative Commons - No Commercial';
 697      $license->source = 'https://creativecommons.org/licenses/by-nc/3.0/';
 698      $license->enabled = 1;
 699      $license->version = '2010033100';
 700      $license->custom = 0;
 701      $corelicenses[] = $license;
 702  
 703      $license = new stdClass();
 704      $license->shortname = 'cc-nc-sa';
 705      $license->fullname = 'Creative Commons - No Commercial ShareAlike';
 706      $license->source = 'https://creativecommons.org/licenses/by-nc-sa/3.0/';
 707      $license->enabled = 1;
 708      $license->version = '2010033100';
 709      $license->custom = 0;
 710      $corelicenses[] = $license;
 711  
 712      $license = new stdClass();
 713      $license->shortname = 'cc-sa';
 714      $license->fullname = 'Creative Commons - ShareAlike';
 715      $license->source = 'https://creativecommons.org/licenses/by-sa/3.0/';
 716      $license->enabled = 1;
 717      $license->version = '2010033100';
 718      $license->custom = 0;
 719      $corelicenses[] = $license;
 720  
 721      foreach ($corelicenses as $corelicense) {
 722          // Check for current license to maintain idempotence.
 723          $currentlicense = $DB->get_record('license', ['shortname' => $corelicense->shortname]);
 724          if (!empty($currentlicense)) {
 725              $corelicense->id = $currentlicense->id;
 726              // Remember if the license was enabled before upgrade.
 727              $corelicense->enabled = $currentlicense->enabled;
 728              $DB->update_record('license', $corelicense);
 729          } else if (!isset($CFG->upgraderunning) || during_initial_install()) {
 730              // Only install missing core licenses if not upgrading or during initial install.
 731              $DB->insert_record('license', $corelicense);
 732          }
 733      }
 734  
 735      // Add sortorder to all licenses.
 736      $licenses = $DB->get_records('license');
 737      $sortorder = 1;
 738      foreach ($licenses as $license) {
 739          $license->sortorder = $sortorder++;
 740          $DB->update_record('license', $license);
 741      }
 742  
 743      // Set the license config values, used by file repository for rendering licenses at front end.
 744      $activelicenses = $DB->get_records_menu('license', ['enabled' => 1], 'id', 'id, shortname');
 745      set_config('licenses', implode(',', $activelicenses));
 746  
 747      $sitedefaultlicense = get_config('', 'sitedefaultlicense');
 748      if (empty($sitedefaultlicense) || !in_array($sitedefaultlicense, $activelicenses)) {
 749          set_config('sitedefaultlicense', reset($activelicenses));
 750      }
 751  }
 752  
 753  /**
 754   * Detects if the site may need to get the calendar events fixed or no. With optional output.
 755   *
 756   * @param bool $output true if the function must output information, false if not.
 757   * @return bool true if the site needs to run the fixes, false if not.
 758   */
 759  function upgrade_calendar_site_status(bool $output = true): bool {
 760      global $DB;
 761  
 762      // List of upgrade steps where the bug happened.
 763      $badsteps = [
 764          '3.9.5'   => '2020061504.08',
 765          '3.10.2'  => '2020110901.09',
 766          '3.11dev' => '2021022600.02',
 767          '4.0dev'  => '2021052500.65',
 768      ];
 769  
 770      // List of upgrade steps that ran the fixer.
 771      $fixsteps = [
 772          '3.9.6+'  => '2020061506.05',
 773          '3.10.3+' => '2020110903.05',
 774          '3.11dev' => '2021042100.02',
 775          '4.0dev'  => '2021052500.85',
 776      ];
 777  
 778      $targetsteps = array_merge(array_values($badsteps), array_values( $fixsteps));
 779      list($insql, $inparams) = $DB->get_in_or_equal($targetsteps);
 780      $foundsteps = $DB->get_fieldset_sql("
 781          SELECT DISTINCT version
 782            FROM {upgrade_log}
 783           WHERE plugin = 'core'
 784             AND version " . $insql . "
 785        ORDER BY version", $inparams);
 786  
 787      // Analyse the found steps, to decide if the site needs upgrading or no.
 788      $badfound = false;
 789      $fixfound = false;
 790      foreach ($foundsteps as $foundstep) {
 791          $badfound = $badfound ?: array_search($foundstep, $badsteps, true);
 792          $fixfound = $fixfound ?: array_search($foundstep, $fixsteps, true);
 793      }
 794      $needsfix = $badfound && !$fixfound;
 795  
 796      // Let's output some textual information if required to.
 797      if ($output) {
 798          mtrace("");
 799          if ($badfound) {
 800              mtrace("This site has executed the problematic upgrade step {$badsteps[$badfound]} present in {$badfound}.");
 801          } else {
 802              mtrace("Problematic upgrade steps were NOT found, site should be safe.");
 803          }
 804          if ($fixfound) {
 805              mtrace("This site has executed the fix upgrade step {$fixsteps[$fixfound]} present in {$fixfound}.");
 806          } else {
 807              mtrace("Fix upgrade steps were NOT found.");
 808          }
 809          mtrace("");
 810          if ($needsfix) {
 811              mtrace("This site NEEDS to run the calendar events fix!");
 812              mtrace('');
 813              mtrace("You can use this CLI tool or upgrade to a version of Moodle that includes");
 814              mtrace("the fix and will be executed as part of the normal upgrade procedure.");
 815              mtrace("The following versions or up are known candidates to upgrade to:");
 816              foreach ($fixsteps as $key => $value) {
 817                  mtrace("  - {$key}: {$value}");
 818              }
 819              mtrace("");
 820          }
 821      }
 822      return $needsfix;
 823  }
 824  
 825  /**
 826   * Detects the calendar events needing to be fixed. With optional output.
 827   *
 828   * @param bool $output true if the function must output information, false if not.
 829   * @return stdClass[] an array of event types (as keys) with total and bad counters, plus sql to retrieve them.
 830   */
 831  function upgrade_calendar_events_status(bool $output = true): array {
 832      global $DB;
 833  
 834      // Calculate the list of standard (core) activity plugins.
 835      $plugins = core_plugin_manager::standard_plugins_list('mod');
 836      $coremodules = "modulename IN ('" . implode("', '", $plugins) . "')";
 837  
 838      // Some query parts go here.
 839      $brokenevents = "(userid = 0 AND (eventtype <> 'user' OR priority <> 0))"; // From the original bad upgrade step.
 840      $standardevents = "(eventtype IN ('site', 'category', 'course', 'group', 'user') AND subscriptionid IS NULL)";
 841      $subscriptionevents = "(subscriptionid IS NOT NULL)";
 842      $overrideevents = "({$coremodules} AND priority IS NOT NULL)";
 843      $actionevents = "({$coremodules} AND instance > 0 and priority IS NULL)";
 844      $otherevents = "(NOT ({$standardevents} OR {$subscriptionevents} OR {$overrideevents} OR {$actionevents}))";
 845  
 846      // Detailed query template.
 847      $detailstemplate = "
 848          SELECT ##group## AS groupname, COUNT(1) AS count
 849            FROM {event}
 850           WHERE ##groupconditions##
 851        GROUP BY ##group##";
 852  
 853      // Count total and potentially broken events.
 854      $total = $DB->count_records_select('event', '');
 855      $totalbadsql = $brokenevents;
 856      $totalbad = $DB->count_records_select('event', $totalbadsql);
 857  
 858      // Standard events.
 859      $standard = $DB->count_records_select('event', $standardevents);
 860      $standardbadsql = "{$brokenevents} AND {$standardevents}";
 861      $standardbad = $DB->count_records_select('event', $standardbadsql);
 862      $standarddetails = $DB->get_records_sql(
 863          str_replace(
 864              ['##group##', '##groupconditions##'],
 865              ['eventtype', $standardbadsql],
 866              $detailstemplate
 867          )
 868      );
 869      array_walk($standarddetails, function (&$rec) {
 870          $rec = $rec->groupname . ': ' . $rec->count;
 871      });
 872      $standarddetails = $standarddetails ? '(' . implode(', ', $standarddetails) . ')' : '- all good!';
 873  
 874      // Subscription events.
 875      $subscription = $DB->count_records_select('event', $subscriptionevents);
 876      $subscriptionbadsql = "{$brokenevents} AND {$subscriptionevents}";
 877      $subscriptionbad = $DB->count_records_select('event', $subscriptionbadsql);
 878      $subscriptiondetails = $DB->get_records_sql(
 879          str_replace(
 880              ['##group##', '##groupconditions##'],
 881              ['eventtype', $subscriptionbadsql],
 882              $detailstemplate
 883          )
 884      );
 885      array_walk($subscriptiondetails, function (&$rec) {
 886          $rec = $rec->groupname . ': ' . $rec->count;
 887      });
 888      $subscriptiondetails = $subscriptiondetails ? '(' . implode(', ', $subscriptiondetails) . ')' : '- all good!';
 889  
 890      // Override events.
 891      $override = $DB->count_records_select('event', $overrideevents);
 892      $overridebadsql = "{$brokenevents} AND {$overrideevents}";
 893      $overridebad = $DB->count_records_select('event', $overridebadsql);
 894      $overridedetails = $DB->get_records_sql(
 895          str_replace(
 896              ['##group##', '##groupconditions##'],
 897              ['modulename', $overridebadsql],
 898              $detailstemplate
 899          )
 900      );
 901      array_walk($overridedetails, function (&$rec) {
 902          $rec = $rec->groupname . ': ' . $rec->count;
 903      });
 904      $overridedetails = $overridedetails ? '(' . implode(', ', $overridedetails) . ')' : '- all good!';
 905  
 906      // Action events.
 907      $action = $DB->count_records_select('event', $actionevents);
 908      $actionbadsql = "{$brokenevents} AND {$actionevents}";
 909      $actionbad = $DB->count_records_select('event', $actionbadsql);
 910      $actiondetails = $DB->get_records_sql(
 911          str_replace(
 912              ['##group##', '##groupconditions##'],
 913              ['modulename', $actionbadsql],
 914              $detailstemplate
 915          )
 916      );
 917      array_walk($actiondetails, function (&$rec) {
 918          $rec = $rec->groupname . ': ' . $rec->count;
 919      });
 920      $actiondetails = $actiondetails ? '(' . implode(', ', $actiondetails) . ')' : '- all good!';
 921  
 922      // Other events.
 923      $other = $DB->count_records_select('event', $otherevents);
 924      $otherbadsql = "{$brokenevents} AND {$otherevents}";
 925      $otherbad = $DB->count_records_select('event', $otherbadsql);
 926      $otherdetails = $DB->get_records_sql(
 927          str_replace(
 928              ['##group##', '##groupconditions##'],
 929              ['COALESCE(component, modulename)', $otherbadsql],
 930              $detailstemplate
 931          )
 932      );
 933      array_walk($otherdetails, function (&$rec) {
 934          $rec = ($rec->groupname ?: 'unknown') . ': ' . $rec->count;
 935      });
 936      $otherdetails = $otherdetails ? '(' . implode(', ', $otherdetails) . ')' : '- all good!';
 937  
 938      // Let's output some textual information if required to.
 939      if ($output) {
 940          mtrace("");
 941          mtrace("Totals: {$total} / {$totalbad} (total / wrong)");
 942          mtrace("  - standards events: {$standard} / {$standardbad} {$standarddetails}");
 943          mtrace("  - subscription events: {$subscription} / {$subscriptionbad} {$subscriptiondetails}");
 944          mtrace("  - override events: {$override} / {$overridebad} {$overridedetails}");
 945          mtrace("  - action events: {$action} / {$actionbad} {$actiondetails}");
 946          mtrace("  - other events: {$other} / {$otherbad} {$otherdetails}");
 947          mtrace("");
 948      }
 949  
 950      return [
 951          'total' => (object)['count' => $total, 'bad' => $totalbad, 'sql' => $totalbadsql],
 952          'standard' => (object)['count' => $standard, 'bad' => $standardbad, 'sql' => $standardbadsql],
 953          'subscription' => (object)['count' => $subscription, 'bad' => $subscriptionbad, 'sql' => $subscriptionbadsql],
 954          'override' => (object)['count' => $override, 'bad' => $overridebad, 'sql' => $overridebadsql],
 955          'action' => (object)['count' => $action, 'bad' => $actionbad, 'sql' => $actionbadsql],
 956          'other' => (object)['count' => $other, 'bad' => $otherbad, 'sql' => $otherbadsql],
 957      ];
 958  }
 959  
 960  /**
 961   * Detects the calendar events needing to be fixed. With optional output.
 962   *
 963   * @param stdClass[] an array of event types (as keys) with total and bad counters, plus sql to retrieve them.
 964   * @param bool $output true if the function must output information, false if not.
 965   * @param int $maxseconds Number of seconds the function will run as max, with zero meaning no limit.
 966   * @return bool true if the function has not finished fixing everything, false if it has finished.
 967   */
 968  function upgrade_calendar_events_fix_remaining(array $info, bool $output = true, int $maxseconds = 0): bool {
 969      global $DB;
 970  
 971      upgrade_calendar_events_mtrace('', $output);
 972  
 973      // Initial preparations.
 974      $starttime = time();
 975      $endtime = $maxseconds ? ($starttime + $maxseconds) : 0;
 976  
 977      // No bad events, or all bad events are "other" events, finished.
 978      if ($info['total']->bad == 0 || $info['total']->bad == $info['other']->bad) {
 979          return false;
 980      }
 981  
 982      // Let's fix overriden events first (they are the ones performing worse with the missing userid).
 983      if ($info['override']->bad != 0) {
 984          if (upgrade_calendar_override_events_fix($info['override'], $output, $endtime)) {
 985              return true; // Not finished yet.
 986          }
 987      }
 988  
 989      // Let's fix the subscription events (like standard ones, but with the event_subscriptions table).
 990      if ($info['subscription']->bad != 0) {
 991          if (upgrade_calendar_subscription_events_fix($info['subscription'], $output, $endtime)) {
 992              return true; // Not finished yet.
 993          }
 994      }
 995  
 996      // Let's fix the standard events (site, category, course, group).
 997      if ($info['standard']->bad != 0) {
 998          if (upgrade_calendar_standard_events_fix($info['standard'], $output, $endtime)) {
 999              return true; // Not finished yet.
1000          }
1001      }
1002  
1003      // Let's fix the action events (all them are "general" ones, not user-specific in core).
1004      if ($info['action']->bad != 0) {
1005          if (upgrade_calendar_action_events_fix($info['action'], $output, $endtime)) {
1006              return true; // Not finished yet.
1007          }
1008      }
1009  
1010      // Have arrived here, finished!
1011      return false;
1012  }
1013  
1014  /**
1015   * Wrapper over mtrace() to allow a few more things to be specified.
1016   *
1017   * @param string $string string to output.
1018   * @param bool $output true to perform the output, false to avoid it.
1019   */
1020  function upgrade_calendar_events_mtrace(string $string, bool $output): void {
1021      static $cols = 0;
1022  
1023      // No output, do nothing.
1024      if (!$output) {
1025          return;
1026      }
1027  
1028      // Printing dots... let's output them slightly nicer.
1029      if ($string === '.') {
1030          $cols++;
1031          // Up to 60 cols.
1032          if ($cols < 60) {
1033              mtrace($string, '');
1034          } else {
1035              mtrace($string);
1036              $cols = 0;
1037          }
1038          return;
1039      }
1040  
1041      // Reset cols, have ended printing dots.
1042      if ($cols) {
1043          $cols = 0;
1044          mtrace('');
1045      }
1046  
1047      // Normal output.
1048      mtrace($string);
1049  }
1050  
1051  /**
1052   * Get a valid editing teacher for a given courseid
1053   *
1054   * @param int $courseid The course to look for editing teachers.
1055   * @return int A user id of an editing teacher or, if missing, the admin userid.
1056   */
1057  function upgrade_calendar_events_get_teacherid(int $courseid): int {
1058  
1059      if ($context = context_course::instance($courseid, IGNORE_MISSING)) {
1060          if ($havemanage = get_users_by_capability($context, 'moodle/course:manageactivities', 'u.id')) {
1061              return array_keys($havemanage)[0];
1062          }
1063      }
1064      return get_admin()->id; // Could not find a teacher, default to admin.
1065  }
1066  
1067  /**
1068   * Detects the calendar standard events needing to be fixed. With optional output.
1069   *
1070   * @param stdClass $info an object with total and bad counters, plus sql to retrieve them.
1071   * @param bool $output true if the function must output information, false if not.
1072   * @param int $endtime cutoff time when the process must stop (0 means no cutoff).
1073   * @return bool true if the function has not finished fixing everything, false if it has finished.
1074   */
1075  function upgrade_calendar_standard_events_fix(stdClass $info, bool $output = true, int $endtime = 0): bool {
1076      global $DB;
1077  
1078      $return = false; // Let's assume the function is going to finish by default.
1079      $status = "Finished!"; // To decide the message to be presented on return.
1080  
1081      upgrade_calendar_events_mtrace('Processing standard events', $output);
1082  
1083      $rs = $DB->get_recordset_sql("
1084          SELECT DISTINCT eventtype, courseid
1085            FROM {event}
1086           WHERE {$info->sql}");
1087  
1088      foreach ($rs as $record) {
1089          switch ($record->eventtype) {
1090              case 'site':
1091              case 'category':
1092                  // These are created by admin.
1093                  $DB->set_field('event', 'userid', get_admin()->id, ['eventtype' => $record->eventtype]);
1094                  break;
1095              case 'course':
1096              case 'group':
1097                  // These are created by course teacher.
1098                  $DB->set_field('event', 'userid', upgrade_calendar_events_get_teacherid($record->courseid),
1099                      ['eventtype' => $record->eventtype, 'courseid' => $record->courseid]);
1100                  break;
1101          }
1102  
1103          // Cutoff time, let's exit.
1104          if ($endtime && $endtime <= time()) {
1105              $status = 'Remaining standard events pending';
1106              $return = true; // Not finished yet.
1107              break;
1108          }
1109          upgrade_calendar_events_mtrace('.', $output);
1110      }
1111      $rs->close();
1112      upgrade_calendar_events_mtrace($status, $output);
1113      upgrade_calendar_events_mtrace('', $output);
1114      return $return;
1115  }
1116  
1117  /**
1118   * Detects the calendar subscription events needing to be fixed. With optional output.
1119   *
1120   * @param stdClass $info an object with total and bad counters, plus sql to retrieve them.
1121   * @param bool $output true if the function must output information, false if not.
1122   * @param int $endtime cutoff time when the process must stop (0 means no cutoff).
1123   * @return bool true if the function has not finished fixing everything, false if it has finished.
1124   */
1125  function upgrade_calendar_subscription_events_fix(stdClass $info, bool $output = true, int $endtime = 0): bool {
1126      global $DB;
1127  
1128      $return = false; // Let's assume the function is going to finish by default.
1129      $status = "Finished!"; // To decide the message to be presented on return.
1130  
1131      upgrade_calendar_events_mtrace('Processing subscription events', $output);
1132  
1133      $rs = $DB->get_recordset_sql("
1134          SELECT DISTINCT subscriptionid AS id
1135            FROM {event}
1136           WHERE {$info->sql}");
1137  
1138      foreach ($rs as $subscription) {
1139          // Subscriptions can be site or category level, let's put the admin as userid.
1140          // (note that "user" subscription weren't deleted so there is nothing to recover with them.
1141          $DB->set_field('event_subscriptions', 'userid', get_admin()->id, ['id' => $subscription->id]);
1142          $DB->set_field('event', 'userid', get_admin()->id, ['subscriptionid' => $subscription->id]);
1143  
1144          // Cutoff time, let's exit.
1145          if ($endtime && $endtime <= time()) {
1146              $status = 'Remaining subscription events pending';
1147              $return = true; // Not finished yet.
1148              break;
1149          }
1150          upgrade_calendar_events_mtrace('.', $output);
1151      }
1152      $rs->close();
1153      upgrade_calendar_events_mtrace($status, $output);
1154      upgrade_calendar_events_mtrace('', $output);
1155      return $return;
1156  }
1157  
1158  /**
1159   * Detects the calendar action events needing to be fixed. With optional output.
1160   *
1161   * @param stdClass $info an object with total and bad counters, plus sql to retrieve them.
1162   * @param bool $output true if the function must output information, false if not.
1163   * @param int $endtime cutoff time when the process must stop (0 means no cutoff).
1164   * @return bool true if the function has not finished fixing everything, false if it has finished.
1165   */
1166  function upgrade_calendar_action_events_fix(stdClass $info, bool $output = true, int $endtime = 0): bool {
1167      global $DB;
1168  
1169      $return = false; // Let's assume the function is going to finish by default.
1170      $status = "Finished!"; // To decide the message to be presented on return.
1171  
1172      upgrade_calendar_events_mtrace('Processing action events', $output);
1173  
1174      $rs = $DB->get_recordset_sql("
1175          SELECT DISTINCT modulename, instance, courseid
1176            FROM {event}
1177           WHERE {$info->sql}");
1178  
1179      foreach ($rs as $record) {
1180          // These are created by course teacher.
1181          $DB->set_field('event', 'userid', upgrade_calendar_events_get_teacherid($record->courseid),
1182              ['modulename' => $record->modulename, 'instance' => $record->instance, 'courseid' => $record->courseid]);
1183  
1184          // Cutoff time, let's exit.
1185          if ($endtime && $endtime <= time()) {
1186              $status = 'Remaining action events pending';
1187              $return = true; // Not finished yet.
1188              break;
1189          }
1190          upgrade_calendar_events_mtrace('.', $output);
1191      }
1192      $rs->close();
1193      upgrade_calendar_events_mtrace($status, $output);
1194      upgrade_calendar_events_mtrace('', $output);
1195      return $return;
1196  }
1197  
1198  /**
1199   * Detects the calendar override events needing to be fixed. With optional output.
1200   *
1201   * @param stdClass $info an object with total and bad counters, plus sql to retrieve them.
1202   * @param bool $output true if the function must output information, false if not.
1203   * @param int $endtime cutoff time when the process must stop (0 means no cutoff).
1204   * @return bool true if the function has not finished fixing everything, false if it has finished.
1205   */
1206  function upgrade_calendar_override_events_fix(stdClass $info, bool $output = true, int $endtime = 0): bool {
1207      global $CFG, $DB;
1208  
1209      include_once($CFG->dirroot. '/course/lib.php');
1210      include_once($CFG->dirroot. '/mod/assign/lib.php');
1211      include_once($CFG->dirroot. '/mod/assign/locallib.php');
1212      include_once($CFG->dirroot. '/mod/lesson/lib.php');
1213      include_once($CFG->dirroot. '/mod/lesson/locallib.php');
1214      include_once($CFG->dirroot. '/mod/quiz/lib.php');
1215      include_once($CFG->dirroot. '/mod/quiz/locallib.php');
1216  
1217      $return = false; // Let's assume the function is going to finish by default.
1218      $status = "Finished!"; // To decide the message to be presented on return.
1219  
1220      upgrade_calendar_events_mtrace('Processing override events', $output);
1221  
1222      $rs = $DB->get_recordset_sql("
1223          SELECT DISTINCT modulename, instance
1224            FROM {event}
1225           WHERE {$info->sql}");
1226  
1227      foreach ($rs as $module) {
1228          // Remove all the records from the events table for the module.
1229          $DB->delete_records('event', ['modulename' => $module->modulename, 'instance' => $module->instance]);
1230  
1231          // Get the activity record.
1232          if (!$activityrecord = $DB->get_record($module->modulename, ['id' => $module->instance])) {
1233              // Orphaned calendar event (activity doesn't exists), skip.
1234              continue;
1235          }
1236  
1237          // Let's rebuild it by calling to each module API.
1238          switch ($module->modulename) {
1239              case 'assign';
1240                  if (function_exists('assign_prepare_update_events')) {
1241                      assign_prepare_update_events($activityrecord);
1242                  }
1243                  break;
1244              case 'lesson':
1245                  if (function_exists('lesson_update_events')) {
1246                      lesson_update_events($activityrecord);
1247                  }
1248                  break;
1249              case 'quiz':
1250                  if (function_exists('quiz_update_events')) {
1251                      quiz_update_events($activityrecord);
1252                  }
1253                  break;
1254          }
1255  
1256          // Sometimes, some (group) overrides are created without userid, when that happens, they deserve
1257          // some user (teacher or admin). This doesn't affect to groups calendar events behaviour,
1258          // but allows counters to detect already processed group overrides and makes things
1259          // consistent.
1260          $DB->set_field_select('event', 'userid', upgrade_calendar_events_get_teacherid($activityrecord->course),
1261              'modulename = ? AND instance = ? and priority != 0 and userid = 0',
1262              ['modulename' => $module->modulename, 'instance' => $module->instance]);
1263  
1264          // Cutoff time, let's exit.
1265          if ($endtime && $endtime <= time()) {
1266              $status = 'Remaining override events pending';
1267              $return = true; // Not finished yet.
1268              break;
1269          }
1270          upgrade_calendar_events_mtrace('.', $output);
1271      }
1272      $rs->close();
1273      upgrade_calendar_events_mtrace($status, $output);
1274      upgrade_calendar_events_mtrace('', $output);
1275      return $return;
1276  }