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 310 and 401] [Versions 311 and 401] [Versions 39 and 401] [Versions 400 and 401]

   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   * Upgrade core licenses shipped with Moodle.
 536   */
 537  function upgrade_core_licenses() {
 538      global $CFG, $DB;
 539  
 540      $expectedlicenses = json_decode(file_get_contents($CFG->dirroot . '/lib/licenses.json'))->licenses;
 541      if (!is_array($expectedlicenses)) {
 542          $expectedlicenses = [];
 543      }
 544      $corelicenses = $DB->get_records('license', ['custom' => 0]);
 545  
 546      // Disable core licenses which are no longer current.
 547      $todisable = array_diff(
 548          array_map(fn ($license) => $license->shortname, $corelicenses),
 549          array_map(fn ($license) => $license->shortname, $expectedlicenses),
 550      );
 551  
 552      // Disable any old *core* license that does not exist in the licenses.json file.
 553      if (count($todisable)) {
 554          [$where, $params] = $DB->get_in_or_equal($todisable, SQL_PARAMS_NAMED);
 555          $DB->set_field_select(
 556              'license',
 557              'enabled',
 558              0,
 559              "shortname {$where}",
 560              $params
 561          );
 562      }
 563  
 564      // Add any new licenses.
 565      foreach ($expectedlicenses as $expectedlicense) {
 566          if (!$expectedlicense->enabled) {
 567              // Skip any license which is no longer enabled.
 568              continue;
 569          }
 570          if (!$DB->record_exists('license', ['shortname' => $expectedlicense->shortname])) {
 571              // If the license replaces an older one, check whether this old license was enabled or not.
 572              $isreplacement = false;
 573              foreach (array_reverse($expectedlicense->replaces ?? []) as $item) {
 574                  foreach ($corelicenses as $corelicense) {
 575                      if ($corelicense->shortname === $item) {
 576                          $expectedlicense->enabled = $corelicense->enabled;
 577                          // Also, keep the old sort order.
 578                          $expectedlicense->sortorder = $corelicense->sortorder * 100;
 579                          $isreplacement = true;
 580                          break 2;
 581                      }
 582                  }
 583              }
 584              if (!isset($CFG->upgraderunning) || during_initial_install() || $isreplacement) {
 585                  // Only install missing core licenses if not upgrading or during initial installation.
 586                  $DB->insert_record('license', $expectedlicense);
 587              }
 588          }
 589      }
 590  
 591      // Add/renumber sortorder to all licenses.
 592      $licenses = $DB->get_records('license', null, 'sortorder');
 593      $sortorder = 1;
 594      foreach ($licenses as $license) {
 595          $license->sortorder = $sortorder++;
 596          $DB->update_record('license', $license);
 597      }
 598  
 599      // Set the license config values, used by file repository for rendering licenses at front end.
 600      $activelicenses = $DB->get_records_menu('license', ['enabled' => 1], 'id', 'id, shortname');
 601      set_config('licenses', implode(',', $activelicenses));
 602  
 603      $sitedefaultlicense = get_config('', 'sitedefaultlicense');
 604      if (empty($sitedefaultlicense) || !in_array($sitedefaultlicense, $activelicenses)) {
 605          set_config('sitedefaultlicense', reset($activelicenses));
 606      }
 607  }
 608  
 609  /**
 610   * Detects if the site may need to get the calendar events fixed or no. With optional output.
 611   *
 612   * @param bool $output true if the function must output information, false if not.
 613   * @return bool true if the site needs to run the fixes, false if not.
 614   */
 615  function upgrade_calendar_site_status(bool $output = true): bool {
 616      global $DB;
 617  
 618      // List of upgrade steps where the bug happened.
 619      $badsteps = [
 620          '3.9.5'   => '2020061504.08',
 621          '3.10.2'  => '2020110901.09',
 622          '3.11dev' => '2021022600.02',
 623          '4.0dev'  => '2021052500.65',
 624      ];
 625  
 626      // List of upgrade steps that ran the fixer.
 627      $fixsteps = [
 628          '3.9.6+'  => '2020061506.05',
 629          '3.10.3+' => '2020110903.05',
 630          '3.11dev' => '2021042100.02',
 631          '4.0dev'  => '2021052500.85',
 632      ];
 633  
 634      $targetsteps = array_merge(array_values($badsteps), array_values( $fixsteps));
 635      list($insql, $inparams) = $DB->get_in_or_equal($targetsteps);
 636      $foundsteps = $DB->get_fieldset_sql("
 637          SELECT DISTINCT version
 638            FROM {upgrade_log}
 639           WHERE plugin = 'core'
 640             AND version " . $insql . "
 641        ORDER BY version", $inparams);
 642  
 643      // Analyse the found steps, to decide if the site needs upgrading or no.
 644      $badfound = false;
 645      $fixfound = false;
 646      foreach ($foundsteps as $foundstep) {
 647          $badfound = $badfound ?: array_search($foundstep, $badsteps, true);
 648          $fixfound = $fixfound ?: array_search($foundstep, $fixsteps, true);
 649      }
 650      $needsfix = $badfound && !$fixfound;
 651  
 652      // Let's output some textual information if required to.
 653      if ($output) {
 654          mtrace("");
 655          if ($badfound) {
 656              mtrace("This site has executed the problematic upgrade step {$badsteps[$badfound]} present in {$badfound}.");
 657          } else {
 658              mtrace("Problematic upgrade steps were NOT found, site should be safe.");
 659          }
 660          if ($fixfound) {
 661              mtrace("This site has executed the fix upgrade step {$fixsteps[$fixfound]} present in {$fixfound}.");
 662          } else {
 663              mtrace("Fix upgrade steps were NOT found.");
 664          }
 665          mtrace("");
 666          if ($needsfix) {
 667              mtrace("This site NEEDS to run the calendar events fix!");
 668              mtrace('');
 669              mtrace("You can use this CLI tool or upgrade to a version of Moodle that includes");
 670              mtrace("the fix and will be executed as part of the normal upgrade procedure.");
 671              mtrace("The following versions or up are known candidates to upgrade to:");
 672              foreach ($fixsteps as $key => $value) {
 673                  mtrace("  - {$key}: {$value}");
 674              }
 675              mtrace("");
 676          }
 677      }
 678      return $needsfix;
 679  }
 680  
 681  /**
 682   * Detects the calendar events needing to be fixed. With optional output.
 683   *
 684   * @param bool $output true if the function must output information, false if not.
 685   * @return stdClass[] an array of event types (as keys) with total and bad counters, plus sql to retrieve them.
 686   */
 687  function upgrade_calendar_events_status(bool $output = true): array {
 688      global $DB;
 689  
 690      // Calculate the list of standard (core) activity plugins.
 691      $plugins = core_plugin_manager::standard_plugins_list('mod');
 692      $coremodules = "modulename IN ('" . implode("', '", $plugins) . "')";
 693  
 694      // Some query parts go here.
 695      $brokenevents = "(userid = 0 AND (eventtype <> 'user' OR priority <> 0))"; // From the original bad upgrade step.
 696      $standardevents = "(eventtype IN ('site', 'category', 'course', 'group', 'user') AND subscriptionid IS NULL)";
 697      $subscriptionevents = "(subscriptionid IS NOT NULL)";
 698      $overrideevents = "({$coremodules} AND priority IS NOT NULL)";
 699      $actionevents = "({$coremodules} AND instance > 0 and priority IS NULL)";
 700      $otherevents = "(NOT ({$standardevents} OR {$subscriptionevents} OR {$overrideevents} OR {$actionevents}))";
 701  
 702      // Detailed query template.
 703      $detailstemplate = "
 704          SELECT ##group## AS groupname, COUNT(1) AS count
 705            FROM {event}
 706           WHERE ##groupconditions##
 707        GROUP BY ##group##";
 708  
 709      // Count total and potentially broken events.
 710      $total = $DB->count_records_select('event', '');
 711      $totalbadsql = $brokenevents;
 712      $totalbad = $DB->count_records_select('event', $totalbadsql);
 713  
 714      // Standard events.
 715      $standard = $DB->count_records_select('event', $standardevents);
 716      $standardbadsql = "{$brokenevents} AND {$standardevents}";
 717      $standardbad = $DB->count_records_select('event', $standardbadsql);
 718      $standarddetails = $DB->get_records_sql(
 719          str_replace(
 720              ['##group##', '##groupconditions##'],
 721              ['eventtype', $standardbadsql],
 722              $detailstemplate
 723          )
 724      );
 725      array_walk($standarddetails, function (&$rec) {
 726          $rec = $rec->groupname . ': ' . $rec->count;
 727      });
 728      $standarddetails = $standarddetails ? '(' . implode(', ', $standarddetails) . ')' : '- all good!';
 729  
 730      // Subscription events.
 731      $subscription = $DB->count_records_select('event', $subscriptionevents);
 732      $subscriptionbadsql = "{$brokenevents} AND {$subscriptionevents}";
 733      $subscriptionbad = $DB->count_records_select('event', $subscriptionbadsql);
 734      $subscriptiondetails = $DB->get_records_sql(
 735          str_replace(
 736              ['##group##', '##groupconditions##'],
 737              ['eventtype', $subscriptionbadsql],
 738              $detailstemplate
 739          )
 740      );
 741      array_walk($subscriptiondetails, function (&$rec) {
 742          $rec = $rec->groupname . ': ' . $rec->count;
 743      });
 744      $subscriptiondetails = $subscriptiondetails ? '(' . implode(', ', $subscriptiondetails) . ')' : '- all good!';
 745  
 746      // Override events.
 747      $override = $DB->count_records_select('event', $overrideevents);
 748      $overridebadsql = "{$brokenevents} AND {$overrideevents}";
 749      $overridebad = $DB->count_records_select('event', $overridebadsql);
 750      $overridedetails = $DB->get_records_sql(
 751          str_replace(
 752              ['##group##', '##groupconditions##'],
 753              ['modulename', $overridebadsql],
 754              $detailstemplate
 755          )
 756      );
 757      array_walk($overridedetails, function (&$rec) {
 758          $rec = $rec->groupname . ': ' . $rec->count;
 759      });
 760      $overridedetails = $overridedetails ? '(' . implode(', ', $overridedetails) . ')' : '- all good!';
 761  
 762      // Action events.
 763      $action = $DB->count_records_select('event', $actionevents);
 764      $actionbadsql = "{$brokenevents} AND {$actionevents}";
 765      $actionbad = $DB->count_records_select('event', $actionbadsql);
 766      $actiondetails = $DB->get_records_sql(
 767          str_replace(
 768              ['##group##', '##groupconditions##'],
 769              ['modulename', $actionbadsql],
 770              $detailstemplate
 771          )
 772      );
 773      array_walk($actiondetails, function (&$rec) {
 774          $rec = $rec->groupname . ': ' . $rec->count;
 775      });
 776      $actiondetails = $actiondetails ? '(' . implode(', ', $actiondetails) . ')' : '- all good!';
 777  
 778      // Other events.
 779      $other = $DB->count_records_select('event', $otherevents);
 780      $otherbadsql = "{$brokenevents} AND {$otherevents}";
 781      $otherbad = $DB->count_records_select('event', $otherbadsql);
 782      $otherdetails = $DB->get_records_sql(
 783          str_replace(
 784              ['##group##', '##groupconditions##'],
 785              ['COALESCE(component, modulename)', $otherbadsql],
 786              $detailstemplate
 787          )
 788      );
 789      array_walk($otherdetails, function (&$rec) {
 790          $rec = ($rec->groupname ?: 'unknown') . ': ' . $rec->count;
 791      });
 792      $otherdetails = $otherdetails ? '(' . implode(', ', $otherdetails) . ')' : '- all good!';
 793  
 794      // Let's output some textual information if required to.
 795      if ($output) {
 796          mtrace("");
 797          mtrace("Totals: {$total} / {$totalbad} (total / wrong)");
 798          mtrace("  - standards events: {$standard} / {$standardbad} {$standarddetails}");
 799          mtrace("  - subscription events: {$subscription} / {$subscriptionbad} {$subscriptiondetails}");
 800          mtrace("  - override events: {$override} / {$overridebad} {$overridedetails}");
 801          mtrace("  - action events: {$action} / {$actionbad} {$actiondetails}");
 802          mtrace("  - other events: {$other} / {$otherbad} {$otherdetails}");
 803          mtrace("");
 804      }
 805  
 806      return [
 807          'total' => (object)['count' => $total, 'bad' => $totalbad, 'sql' => $totalbadsql],
 808          'standard' => (object)['count' => $standard, 'bad' => $standardbad, 'sql' => $standardbadsql],
 809          'subscription' => (object)['count' => $subscription, 'bad' => $subscriptionbad, 'sql' => $subscriptionbadsql],
 810          'override' => (object)['count' => $override, 'bad' => $overridebad, 'sql' => $overridebadsql],
 811          'action' => (object)['count' => $action, 'bad' => $actionbad, 'sql' => $actionbadsql],
 812          'other' => (object)['count' => $other, 'bad' => $otherbad, 'sql' => $otherbadsql],
 813      ];
 814  }
 815  
 816  /**
 817   * Detects the calendar events needing to be fixed. With optional output.
 818   *
 819   * @param stdClass[] an array of event types (as keys) with total and bad counters, plus sql to retrieve them.
 820   * @param bool $output true if the function must output information, false if not.
 821   * @param int $maxseconds Number of seconds the function will run as max, with zero meaning no limit.
 822   * @return bool true if the function has not finished fixing everything, false if it has finished.
 823   */
 824  function upgrade_calendar_events_fix_remaining(array $info, bool $output = true, int $maxseconds = 0): bool {
 825      global $DB;
 826  
 827      upgrade_calendar_events_mtrace('', $output);
 828  
 829      // Initial preparations.
 830      $starttime = time();
 831      $endtime = $maxseconds ? ($starttime + $maxseconds) : 0;
 832  
 833      // No bad events, or all bad events are "other" events, finished.
 834      if ($info['total']->bad == 0 || $info['total']->bad == $info['other']->bad) {
 835          return false;
 836      }
 837  
 838      // Let's fix overriden events first (they are the ones performing worse with the missing userid).
 839      if ($info['override']->bad != 0) {
 840          if (upgrade_calendar_override_events_fix($info['override'], $output, $endtime)) {
 841              return true; // Not finished yet.
 842          }
 843      }
 844  
 845      // Let's fix the subscription events (like standard ones, but with the event_subscriptions table).
 846      if ($info['subscription']->bad != 0) {
 847          if (upgrade_calendar_subscription_events_fix($info['subscription'], $output, $endtime)) {
 848              return true; // Not finished yet.
 849          }
 850      }
 851  
 852      // Let's fix the standard events (site, category, course, group).
 853      if ($info['standard']->bad != 0) {
 854          if (upgrade_calendar_standard_events_fix($info['standard'], $output, $endtime)) {
 855              return true; // Not finished yet.
 856          }
 857      }
 858  
 859      // Let's fix the action events (all them are "general" ones, not user-specific in core).
 860      if ($info['action']->bad != 0) {
 861          if (upgrade_calendar_action_events_fix($info['action'], $output, $endtime)) {
 862              return true; // Not finished yet.
 863          }
 864      }
 865  
 866      // Have arrived here, finished!
 867      return false;
 868  }
 869  
 870  /**
 871   * Wrapper over mtrace() to allow a few more things to be specified.
 872   *
 873   * @param string $string string to output.
 874   * @param bool $output true to perform the output, false to avoid it.
 875   */
 876  function upgrade_calendar_events_mtrace(string $string, bool $output): void {
 877      static $cols = 0;
 878  
 879      // No output, do nothing.
 880      if (!$output) {
 881          return;
 882      }
 883  
 884      // Printing dots... let's output them slightly nicer.
 885      if ($string === '.') {
 886          $cols++;
 887          // Up to 60 cols.
 888          if ($cols < 60) {
 889              mtrace($string, '');
 890          } else {
 891              mtrace($string);
 892              $cols = 0;
 893          }
 894          return;
 895      }
 896  
 897      // Reset cols, have ended printing dots.
 898      if ($cols) {
 899          $cols = 0;
 900          mtrace('');
 901      }
 902  
 903      // Normal output.
 904      mtrace($string);
 905  }
 906  
 907  /**
 908   * Get a valid editing teacher for a given courseid
 909   *
 910   * @param int $courseid The course to look for editing teachers.
 911   * @return int A user id of an editing teacher or, if missing, the admin userid.
 912   */
 913  function upgrade_calendar_events_get_teacherid(int $courseid): int {
 914  
 915      if ($context = context_course::instance($courseid, IGNORE_MISSING)) {
 916          if ($havemanage = get_users_by_capability($context, 'moodle/course:manageactivities', 'u.id')) {
 917              return array_keys($havemanage)[0];
 918          }
 919      }
 920      return get_admin()->id; // Could not find a teacher, default to admin.
 921  }
 922  
 923  /**
 924   * Detects the calendar standard events needing to be fixed. With optional output.
 925   *
 926   * @param stdClass $info an object with total and bad counters, plus sql to retrieve them.
 927   * @param bool $output true if the function must output information, false if not.
 928   * @param int $endtime cutoff time when the process must stop (0 means no cutoff).
 929   * @return bool true if the function has not finished fixing everything, false if it has finished.
 930   */
 931  function upgrade_calendar_standard_events_fix(stdClass $info, bool $output = true, int $endtime = 0): bool {
 932      global $DB;
 933  
 934      $return = false; // Let's assume the function is going to finish by default.
 935      $status = "Finished!"; // To decide the message to be presented on return.
 936  
 937      upgrade_calendar_events_mtrace('Processing standard events', $output);
 938  
 939      $rs = $DB->get_recordset_sql("
 940          SELECT DISTINCT eventtype, courseid
 941            FROM {event}
 942           WHERE {$info->sql}");
 943  
 944      foreach ($rs as $record) {
 945          switch ($record->eventtype) {
 946              case 'site':
 947              case 'category':
 948                  // These are created by admin.
 949                  $DB->set_field('event', 'userid', get_admin()->id, ['eventtype' => $record->eventtype]);
 950                  break;
 951              case 'course':
 952              case 'group':
 953                  // These are created by course teacher.
 954                  $DB->set_field('event', 'userid', upgrade_calendar_events_get_teacherid($record->courseid),
 955                      ['eventtype' => $record->eventtype, 'courseid' => $record->courseid]);
 956                  break;
 957          }
 958  
 959          // Cutoff time, let's exit.
 960          if ($endtime && $endtime <= time()) {
 961              $status = 'Remaining standard events pending';
 962              $return = true; // Not finished yet.
 963              break;
 964          }
 965          upgrade_calendar_events_mtrace('.', $output);
 966      }
 967      $rs->close();
 968      upgrade_calendar_events_mtrace($status, $output);
 969      upgrade_calendar_events_mtrace('', $output);
 970      return $return;
 971  }
 972  
 973  /**
 974   * Detects the calendar subscription events needing to be fixed. With optional output.
 975   *
 976   * @param stdClass $info an object with total and bad counters, plus sql to retrieve them.
 977   * @param bool $output true if the function must output information, false if not.
 978   * @param int $endtime cutoff time when the process must stop (0 means no cutoff).
 979   * @return bool true if the function has not finished fixing everything, false if it has finished.
 980   */
 981  function upgrade_calendar_subscription_events_fix(stdClass $info, bool $output = true, int $endtime = 0): bool {
 982      global $DB;
 983  
 984      $return = false; // Let's assume the function is going to finish by default.
 985      $status = "Finished!"; // To decide the message to be presented on return.
 986  
 987      upgrade_calendar_events_mtrace('Processing subscription events', $output);
 988  
 989      $rs = $DB->get_recordset_sql("
 990          SELECT DISTINCT subscriptionid AS id
 991            FROM {event}
 992           WHERE {$info->sql}");
 993  
 994      foreach ($rs as $subscription) {
 995          // Subscriptions can be site or category level, let's put the admin as userid.
 996          // (note that "user" subscription weren't deleted so there is nothing to recover with them.
 997          $DB->set_field('event_subscriptions', 'userid', get_admin()->id, ['id' => $subscription->id]);
 998          $DB->set_field('event', 'userid', get_admin()->id, ['subscriptionid' => $subscription->id]);
 999  
1000          // Cutoff time, let's exit.
1001          if ($endtime && $endtime <= time()) {
1002              $status = 'Remaining subscription events pending';
1003              $return = true; // Not finished yet.
1004              break;
1005          }
1006          upgrade_calendar_events_mtrace('.', $output);
1007      }
1008      $rs->close();
1009      upgrade_calendar_events_mtrace($status, $output);
1010      upgrade_calendar_events_mtrace('', $output);
1011      return $return;
1012  }
1013  
1014  /**
1015   * Detects the calendar action events needing to be fixed. With optional output.
1016   *
1017   * @param stdClass $info an object with total and bad counters, plus sql to retrieve them.
1018   * @param bool $output true if the function must output information, false if not.
1019   * @param int $endtime cutoff time when the process must stop (0 means no cutoff).
1020   * @return bool true if the function has not finished fixing everything, false if it has finished.
1021   */
1022  function upgrade_calendar_action_events_fix(stdClass $info, bool $output = true, int $endtime = 0): bool {
1023      global $DB;
1024  
1025      $return = false; // Let's assume the function is going to finish by default.
1026      $status = "Finished!"; // To decide the message to be presented on return.
1027  
1028      upgrade_calendar_events_mtrace('Processing action events', $output);
1029  
1030      $rs = $DB->get_recordset_sql("
1031          SELECT DISTINCT modulename, instance, courseid
1032            FROM {event}
1033           WHERE {$info->sql}");
1034  
1035      foreach ($rs as $record) {
1036          // These are created by course teacher.
1037          $DB->set_field('event', 'userid', upgrade_calendar_events_get_teacherid($record->courseid),
1038              ['modulename' => $record->modulename, 'instance' => $record->instance, 'courseid' => $record->courseid]);
1039  
1040          // Cutoff time, let's exit.
1041          if ($endtime && $endtime <= time()) {
1042              $status = 'Remaining action events pending';
1043              $return = true; // Not finished yet.
1044              break;
1045          }
1046          upgrade_calendar_events_mtrace('.', $output);
1047      }
1048      $rs->close();
1049      upgrade_calendar_events_mtrace($status, $output);
1050      upgrade_calendar_events_mtrace('', $output);
1051      return $return;
1052  }
1053  
1054  /**
1055   * Detects the calendar override events needing to be fixed. With optional output.
1056   *
1057   * @param stdClass $info an object with total and bad counters, plus sql to retrieve them.
1058   * @param bool $output true if the function must output information, false if not.
1059   * @param int $endtime cutoff time when the process must stop (0 means no cutoff).
1060   * @return bool true if the function has not finished fixing everything, false if it has finished.
1061   */
1062  function upgrade_calendar_override_events_fix(stdClass $info, bool $output = true, int $endtime = 0): bool {
1063      global $CFG, $DB;
1064  
1065      include_once($CFG->dirroot. '/course/lib.php');
1066      include_once($CFG->dirroot. '/mod/assign/lib.php');
1067      include_once($CFG->dirroot. '/mod/assign/locallib.php');
1068      include_once($CFG->dirroot. '/mod/lesson/lib.php');
1069      include_once($CFG->dirroot. '/mod/lesson/locallib.php');
1070      include_once($CFG->dirroot. '/mod/quiz/lib.php');
1071      include_once($CFG->dirroot. '/mod/quiz/locallib.php');
1072  
1073      $return = false; // Let's assume the function is going to finish by default.
1074      $status = "Finished!"; // To decide the message to be presented on return.
1075  
1076      upgrade_calendar_events_mtrace('Processing override events', $output);
1077  
1078      $rs = $DB->get_recordset_sql("
1079          SELECT DISTINCT modulename, instance
1080            FROM {event}
1081           WHERE {$info->sql}");
1082  
1083      foreach ($rs as $module) {
1084          // Remove all the records from the events table for the module.
1085          $DB->delete_records('event', ['modulename' => $module->modulename, 'instance' => $module->instance]);
1086  
1087          // Get the activity record.
1088          if (!$activityrecord = $DB->get_record($module->modulename, ['id' => $module->instance])) {
1089              // Orphaned calendar event (activity doesn't exists), skip.
1090              continue;
1091          }
1092  
1093          // Let's rebuild it by calling to each module API.
1094          switch ($module->modulename) {
1095              case 'assign';
1096                  if (function_exists('assign_prepare_update_events')) {
1097                      assign_prepare_update_events($activityrecord);
1098                  }
1099                  break;
1100              case 'lesson':
1101                  if (function_exists('lesson_update_events')) {
1102                      lesson_update_events($activityrecord);
1103                  }
1104                  break;
1105              case 'quiz':
1106                  if (function_exists('quiz_update_events')) {
1107                      quiz_update_events($activityrecord);
1108                  }
1109                  break;
1110          }
1111  
1112          // Sometimes, some (group) overrides are created without userid, when that happens, they deserve
1113          // some user (teacher or admin). This doesn't affect to groups calendar events behaviour,
1114          // but allows counters to detect already processed group overrides and makes things
1115          // consistent.
1116          $DB->set_field_select('event', 'userid', upgrade_calendar_events_get_teacherid($activityrecord->course),
1117              'modulename = ? AND instance = ? and priority != 0 and userid = 0',
1118              ['modulename' => $module->modulename, 'instance' => $module->instance]);
1119  
1120          // Cutoff time, let's exit.
1121          if ($endtime && $endtime <= time()) {
1122              $status = 'Remaining override events pending';
1123              $return = true; // Not finished yet.
1124              break;
1125          }
1126          upgrade_calendar_events_mtrace('.', $output);
1127      }
1128      $rs->close();
1129      upgrade_calendar_events_mtrace($status, $output);
1130      upgrade_calendar_events_mtrace('', $output);
1131      return $return;
1132  }
1133  
1134  /**
1135   * Add a new item at the end of the usermenu.
1136   *
1137   * @param string $menuitem
1138   */
1139  function upgrade_add_item_to_usermenu(string $menuitem): void {
1140      global $CFG;
1141  
1142      // Get current configuration data.
1143      $currentcustomusermenuitems = str_replace(["\r\n", "\r"], "\n", $CFG->customusermenuitems);
1144      $lines = preg_split('/\n/', $currentcustomusermenuitems, -1, PREG_SPLIT_NO_EMPTY);
1145      $lines = array_map('trim', $lines);
1146  
1147      if (!in_array($menuitem, $lines)) {
1148          // Add the item to the menu.
1149          $lines[] = $menuitem;
1150          set_config('customusermenuitems', implode("\n", $lines));
1151      }
1152  }
1153  
1154  /**
1155   * Update all instances of a block shown on a pagetype to a new default region, adding missing block instances where
1156   * none is found.
1157   *
1158   * Note: This is intended as a helper to add blocks to all instances of the standard my-page. It will only work where
1159   * the subpagepattern is a string representation of an integer. If there are any string values this will not work.
1160   *
1161   * @param string $blockname The block name, without the block_ frankenstyle component
1162   * @param string $pagename The type of my-page to match
1163   * @param string $pagetypepattern The page type pattern to match for the block
1164   * @param string $newdefaultregion The new region to set
1165   */
1166  function upgrade_block_set_defaultregion(
1167      string $blockname,
1168      string $pagename,
1169      string $pagetypepattern,
1170      string $newdefaultregion
1171  ): void {
1172      global $DB;
1173  
1174      // The subpagepattern is a string.
1175      // In all core blocks it contains a string represnetation of an integer, but it is theoretically possible for a
1176      // community block to do something different.
1177      // This function is not suited to those cases.
1178      $subpagepattern = $DB->sql_cast_char2int('bi.subpagepattern');
1179      $subpageempty = $DB->sql_isnotempty('block_instances', 'bi.subpagepattern', true, false);
1180  
1181      // If a subquery returns any NULL then the NOT IN returns no results at all.
1182      // By adding a join in the inner select on my_pages we remove any possible nulls and prevent any need for
1183      // additional casting to filter out the nulls.
1184      $sql = <<<EOF
1185          INSERT INTO {block_instances} (
1186              blockname,
1187              parentcontextid,
1188              showinsubcontexts,
1189              pagetypepattern,
1190              subpagepattern,
1191              defaultregion,
1192              defaultweight,
1193              timecreated,
1194              timemodified
1195          ) SELECT
1196              :selectblockname AS blockname,
1197              c.id AS parentcontextid,
1198              0 AS showinsubcontexts,
1199              :selectpagetypepattern AS pagetypepattern,
1200              mp.id AS subpagepattern,
1201              :selectdefaultregion AS defaultregion,
1202              0 AS defaultweight,
1203              :selecttimecreated AS timecreated,
1204              :selecttimemodified AS timemodified
1205            FROM {my_pages} mp
1206            JOIN {context} c ON c.instanceid = mp.userid AND c.contextlevel = :contextuser
1207           WHERE mp.id NOT IN (
1208              SELECT mpi.id FROM {my_pages} mpi
1209                JOIN {block_instances} bi
1210                      ON bi.blockname = :blockname
1211                     AND bi.subpagepattern IS NOT NULL AND {$subpageempty}
1212                     AND bi.pagetypepattern = :pagetypepattern
1213                     AND {$subpagepattern} = mpi.id
1214           )
1215           AND mp.private = 1
1216           AND mp.name = :pagename
1217      EOF;
1218  
1219      $DB->execute($sql, [
1220          'selectblockname' => $blockname,
1221          'contextuser' => CONTEXT_USER,
1222          'selectpagetypepattern' => $pagetypepattern,
1223          'selectdefaultregion' => $newdefaultregion,
1224          'selecttimecreated' => time(),
1225          'selecttimemodified' => time(),
1226          'pagetypepattern' => $pagetypepattern,
1227          'blockname' => $blockname,
1228          'pagename' => $pagename,
1229      ]);
1230  
1231      // Update the existing instances.
1232      $sql = <<<EOF
1233          UPDATE {block_instances}
1234             SET defaultregion = :newdefaultregion
1235           WHERE id IN (
1236              SELECT * FROM (
1237                  SELECT bi.id
1238                    FROM {my_pages} mp
1239                    JOIN {block_instances} bi
1240                          ON bi.blockname = :blockname
1241                         AND bi.subpagepattern IS NOT NULL AND {$subpageempty}
1242                         AND bi.pagetypepattern = :pagetypepattern
1243                         AND {$subpagepattern} = mp.id
1244                   WHERE mp.private = 1
1245                     AND mp.name = :pagename
1246                     AND bi.defaultregion <> :existingnewdefaultregion
1247              ) bid
1248           )
1249      EOF;
1250  
1251      $DB->execute($sql, [
1252          'newdefaultregion' => $newdefaultregion,
1253          'pagetypepattern' => $pagetypepattern,
1254          'blockname' => $blockname,
1255          'existingnewdefaultregion' => $newdefaultregion,
1256          'pagename' => $pagename,
1257      ]);
1258  
1259      // Note: This can be time consuming!
1260      \context_helper::create_instances(CONTEXT_BLOCK);
1261  }
1262  
1263  /**
1264   * Remove all instances of a block on pages of the specified pagetypepattern.
1265   *
1266   * Note: This is intended as a helper to add blocks to all instances of the standard my-page. It will only work where
1267   * the subpagepattern is a string representation of an integer. If there are any string values this will not work.
1268   *
1269   * @param string $blockname The block name, without the block_ frankenstyle component
1270   * @param string $pagename The type of my-page to match
1271   * @param string $pagetypepattern This is typically used on the 'my-index'
1272   */
1273  function upgrade_block_delete_instances(
1274      string $blockname,
1275      string $pagename,
1276      string $pagetypepattern
1277  ): void {
1278      global $DB;
1279  
1280      $deleteblockinstances = function (string $instanceselect, array $instanceparams) use ($DB) {
1281          $deletesql = <<<EOF
1282              SELECT c.id AS cid
1283                FROM {context} c
1284                JOIN {block_instances} bi ON bi.id = c.instanceid AND c.contextlevel = :contextlevel
1285               WHERE {$instanceselect}
1286          EOF;
1287          $DB->delete_records_subquery('context', 'id', 'cid', $deletesql, array_merge($instanceparams, [
1288              'contextlevel' => CONTEXT_BLOCK,
1289          ]));
1290  
1291          $deletesql = <<<EOF
1292              SELECT bp.id AS bpid
1293                FROM {block_positions} bp
1294                JOIN {block_instances} bi ON bi.id = bp.blockinstanceid
1295               WHERE {$instanceselect}
1296          EOF;
1297          $DB->delete_records_subquery('block_positions', 'id', 'bpid', $deletesql, $instanceparams);
1298  
1299          $blockhidden = $DB->sql_concat("'block'", 'bi.id', "'hidden'");
1300          $blockdocked = $DB->sql_concat("'docked_block_instance_'", 'bi.id');
1301          $deletesql = <<<EOF
1302              SELECT p.id AS pid
1303                FROM {user_preferences} p
1304                JOIN {block_instances} bi ON p.name IN ({$blockhidden}, {$blockdocked})
1305               WHERE {$instanceselect}
1306          EOF;
1307          $DB->delete_records_subquery('user_preferences', 'id', 'pid', $deletesql, $instanceparams);
1308  
1309          $deletesql = <<<EOF
1310              SELECT bi.id AS bid
1311                FROM {block_instances} bi
1312               WHERE {$instanceselect}
1313          EOF;
1314          $DB->delete_records_subquery('block_instances', 'id', 'bid', $deletesql, $instanceparams);
1315      };
1316  
1317      // Delete the default indexsys version of the block.
1318      $subpagepattern = $DB->get_record('my_pages', [
1319          'userid' => null,
1320          'name' => $pagename,
1321          'private' => MY_PAGE_PRIVATE,
1322      ], 'id', IGNORE_MULTIPLE)->id;
1323  
1324      $instanceselect = <<<EOF
1325              blockname = :blockname
1326          AND pagetypepattern = :pagetypepattern
1327          AND subpagepattern = :subpagepattern
1328      EOF;
1329  
1330      $params = [
1331          'blockname' => $blockname,
1332          'pagetypepattern' => $pagetypepattern,
1333          'subpagepattern' => $subpagepattern,
1334      ];
1335      $deleteblockinstances($instanceselect, $params);
1336  
1337      // The subpagepattern is a string.
1338      // In all core blocks it contains a string represnetation of an integer, but it is theoretically possible for a
1339      // community block to do something different.
1340      // This function is not suited to those cases.
1341      $subpagepattern = $DB->sql_cast_char2int('bi.subpagepattern');
1342  
1343      // Look for any and all instances of the block in customised /my pages.
1344      $subpageempty = $DB->sql_isnotempty('block_instances', 'bi.subpagepattern', true, false);
1345      $instanceselect = <<<EOF
1346           bi.id IN (
1347              SELECT * FROM (
1348                  SELECT bi.id
1349                    FROM {my_pages} mp
1350                    JOIN {block_instances} bi
1351                          ON bi.blockname = :blockname
1352                         AND bi.subpagepattern IS NOT NULL AND {$subpageempty}
1353                         AND bi.pagetypepattern = :pagetypepattern
1354                         AND {$subpagepattern} = mp.id
1355                   WHERE mp.private = :private
1356                     AND mp.name = :pagename
1357              ) bid
1358           )
1359      EOF;
1360  
1361      $params = [
1362          'blockname' => $blockname,
1363          'pagetypepattern' => $pagetypepattern,
1364          'pagename' => $pagename,
1365          'private' => MY_PAGE_PRIVATE,
1366      ];
1367  
1368      $deleteblockinstances($instanceselect, $params);
1369  }
1370  
1371  /**
1372   * Update the block instance parentcontext to point to the correct user context id for the specified block on a my page.
1373   *
1374   * @param string $blockname
1375   * @param string $pagename
1376   * @param string $pagetypepattern
1377   */
1378  function upgrade_block_set_my_user_parent_context(
1379      string $blockname,
1380      string $pagename,
1381      string $pagetypepattern
1382  ): void {
1383      global $DB;
1384  
1385      $subpagepattern = $DB->sql_cast_char2int('bi.subpagepattern');
1386      // Look for any and all instances of the block in customised /my pages.
1387      $subpageempty = $DB->sql_isnotempty('block_instances', 'bi.subpagepattern', true, false);
1388  
1389      $dbman = $DB->get_manager();
1390      $temptablename = 'block_instance_context';
1391      $xmldbtable = new \xmldb_table($temptablename);
1392      $xmldbtable->add_field('instanceid', XMLDB_TYPE_INTEGER, 10, null, XMLDB_NOTNULL, null, null);
1393      $xmldbtable->add_field('contextid', XMLDB_TYPE_INTEGER, 10, null, XMLDB_NOTNULL, null, null);
1394      $xmldbtable->add_key('primary', XMLDB_KEY_PRIMARY, ['instanceid']);
1395      $dbman->create_temp_table($xmldbtable);
1396  
1397      $sql = <<<EOF
1398          INSERT INTO {block_instance_context} (
1399              instanceid,
1400              contextid
1401          ) SELECT
1402              bi.id as instanceid,
1403              c.id as contextid
1404             FROM {my_pages} mp
1405             JOIN {context} c ON c.instanceid = mp.userid AND c.contextlevel = :contextuser
1406             JOIN {block_instances} bi
1407                  ON bi.blockname = :blockname
1408                 AND bi.subpagepattern IS NOT NULL AND {$subpageempty}
1409                 AND bi.pagetypepattern = :pagetypepattern
1410                 AND {$subpagepattern} = mp.id
1411            WHERE mp.name = :pagename AND bi.parentcontextid <> c.id
1412      EOF;
1413  
1414      $DB->execute($sql, [
1415          'blockname' => $blockname,
1416          'pagetypepattern' => $pagetypepattern,
1417          'contextuser' => CONTEXT_USER,
1418          'pagename' => $pagename,
1419      ]);
1420  
1421      $dbfamily = $DB->get_dbfamily();
1422      if ($dbfamily === 'mysql') {
1423          // MariaDB and MySQL.
1424          $sql = <<<EOF
1425              UPDATE {block_instances} bi, {block_instance_context} bic
1426                 SET bi.parentcontextid = bic.contextid
1427               WHERE bi.id = bic.instanceid
1428          EOF;
1429      } else if ($dbfamily === 'oracle') {
1430          $sql = <<<EOF
1431              UPDATE {block_instances} bi
1432              SET (bi.parentcontextid) = (
1433                  SELECT bic.contextid
1434                    FROM {block_instance_context} bic
1435                   WHERE bic.instanceid = bi.id
1436              ) WHERE EXISTS (
1437                  SELECT 'x'
1438                    FROM {block_instance_context} bic
1439                   WHERE bic.instanceid = bi.id
1440              )
1441          EOF;
1442      } else {
1443          // Postgres and sqlsrv.
1444          $sql = <<<EOF
1445              UPDATE {block_instances}
1446              SET parentcontextid = bic.contextid
1447              FROM {block_instance_context} bic
1448              WHERE {block_instances}.id = bic.instanceid
1449          EOF;
1450      }
1451  
1452      $DB->execute($sql);
1453  
1454      $dbman->drop_table($xmldbtable);
1455  }
1456  
1457  /**
1458   * Fix the timestamps for files where their timestamps are older
1459   * than the directory listing that they are contained in.
1460   */
1461  function upgrade_fix_file_timestamps() {
1462      global $DB;
1463  
1464      // Due to incompatability in SQL syntax for updates with joins,
1465      // These will be updated in a select + separate update.
1466      $sql = "SELECT f.id, f2.timecreated
1467                FROM {files} f
1468                JOIN {files} f2
1469                      ON f2.contextid = f.contextid
1470                     AND f2.filepath = f.filepath
1471                     AND f2.component = f.component
1472                     AND f2.filearea = f.filearea
1473                     AND f2.itemid = f.itemid
1474                     AND f2.filename = '.'
1475               WHERE f2.timecreated > f.timecreated";
1476  
1477      $recordset = $DB->get_recordset_sql($sql);
1478  
1479      if (!$recordset->valid()) {
1480          $recordset->close();
1481          return;
1482      }
1483  
1484      foreach ($recordset as $record) {
1485          $record->timemodified = $record->timecreated;
1486          $DB->update_record('files', $record);
1487      }
1488  
1489      $recordset->close();
1490  }
1491  
1492  /**
1493   * Upgrade helper to add foreign keys and indexes for MDL-49795
1494   */
1495  function upgrade_add_foreign_key_and_indexes() {
1496      global $DB;
1497  
1498      $dbman = $DB->get_manager();
1499      // Define key originalcourseid (foreign) to be added to course.
1500      $table = new xmldb_table('course');
1501      $key = new xmldb_key('originalcourseid', XMLDB_KEY_FOREIGN, ['originalcourseid'], 'course', ['id']);
1502      // Launch add key originalcourseid.
1503      $dbman->add_key($table, $key);
1504  
1505      // Define key roleid (foreign) to be added to enrol.
1506      $table = new xmldb_table('enrol');
1507      $key = new xmldb_key('roleid', XMLDB_KEY_FOREIGN, ['roleid'], 'role', ['id']);
1508      // Launch add key roleid.
1509      $dbman->add_key($table, $key);
1510  
1511      // Define key userid (foreign) to be added to scale.
1512      $table = new xmldb_table('scale');
1513      $key = new xmldb_key('userid', XMLDB_KEY_FOREIGN, ['userid'], 'user', ['id']);
1514      // Launch add key userid.
1515      $dbman->add_key($table, $key);
1516  
1517      // Define key userid (foreign) to be added to scale_history.
1518      $table = new xmldb_table('scale_history');
1519      $key = new xmldb_key('userid', XMLDB_KEY_FOREIGN, ['userid'], 'user', ['id']);
1520      // Launch add key userid.
1521      $dbman->add_key($table, $key);
1522  
1523      // Define key courseid (foreign) to be added to post.
1524      $table = new xmldb_table('post');
1525      $key = new xmldb_key('courseid', XMLDB_KEY_FOREIGN, ['courseid'], 'course', ['id']);
1526      // Launch add key courseid.
1527      $dbman->add_key($table, $key);
1528  
1529      // Define key coursemoduleid (foreign) to be added to post.
1530      $table = new xmldb_table('post');
1531      $key = new xmldb_key('coursemoduleid', XMLDB_KEY_FOREIGN, ['coursemoduleid'], 'course_modules', ['id']);
1532      // Launch add key coursemoduleid.
1533      $dbman->add_key($table, $key);
1534  
1535      // Define key questionid (foreign) to be added to question_statistics.
1536      $table = new xmldb_table('question_statistics');
1537      $key = new xmldb_key('questionid', XMLDB_KEY_FOREIGN, ['questionid'], 'question', ['id']);
1538      // Launch add key questionid.
1539      $dbman->add_key($table, $key);
1540  
1541      // Define key questionid (foreign) to be added to question_response_analysis.
1542      $table = new xmldb_table('question_response_analysis');
1543      $key = new xmldb_key('questionid', XMLDB_KEY_FOREIGN, ['questionid'], 'question', ['id']);
1544      // Launch add key questionid.
1545      $dbman->add_key($table, $key);
1546  
1547      // Define index last_log_id (not unique) to be added to mnet_host.
1548      $table = new xmldb_table('mnet_host');
1549      $index = new xmldb_index('last_log_id', XMLDB_INDEX_NOTUNIQUE, ['last_log_id']);
1550      // Conditionally launch add index last_log_id.
1551      if (!$dbman->index_exists($table, $index)) {
1552          $dbman->add_index($table, $index);
1553      }
1554  
1555      // Define key userid (foreign) to be added to mnet_session.
1556      $table = new xmldb_table('mnet_session');
1557      $key = new xmldb_key('userid', XMLDB_KEY_FOREIGN, ['userid'], 'user', ['id']);
1558      // Launch add key userid.
1559      $dbman->add_key($table, $key);
1560  
1561      // Define key mnethostid (foreign) to be added to mnet_session.
1562      $table = new xmldb_table('mnet_session');
1563      $key = new xmldb_key('mnethostid', XMLDB_KEY_FOREIGN, ['mnethostid'], 'mnet_host', ['id']);
1564      // Launch add key mnethostid.
1565      $dbman->add_key($table, $key);
1566  
1567      // Define key userid (foreign) to be added to grade_import_values.
1568      $table = new xmldb_table('grade_import_values');
1569      $key = new xmldb_key('userid', XMLDB_KEY_FOREIGN, ['userid'], 'user', ['id']);
1570      // Launch add key userid.
1571      $dbman->add_key($table, $key);
1572  
1573      // Define key tempdataid (foreign) to be added to portfolio_log.
1574      $table = new xmldb_table('portfolio_log');
1575      $key = new xmldb_key('tempdataid', XMLDB_KEY_FOREIGN, ['tempdataid'], 'portfolio_tempdata', ['id']);
1576      // Launch add key tempdataid.
1577      $dbman->add_key($table, $key);
1578  
1579      // Define key usermodified (foreign) to be added to file_conversion.
1580      $table = new xmldb_table('file_conversion');
1581      $key = new xmldb_key('usermodified', XMLDB_KEY_FOREIGN, ['usermodified'], 'user', ['id']);
1582      // Launch add key usermodified.
1583      $dbman->add_key($table, $key);
1584  
1585      // Define key userid (foreign) to be added to repository_instances.
1586      $table = new xmldb_table('repository_instances');
1587      $key = new xmldb_key('userid', XMLDB_KEY_FOREIGN, ['userid'], 'user', ['id']);
1588      // Launch add key userid.
1589      $dbman->add_key($table, $key);
1590  
1591      // Define key contextid (foreign) to be added to repository_instances.
1592      $table = new xmldb_table('repository_instances');
1593      $key = new xmldb_key('contextid', XMLDB_KEY_FOREIGN, ['contextid'], 'context', ['id']);
1594      // Launch add key contextid.
1595      $dbman->add_key($table, $key);
1596  
1597      // Define key scaleid (foreign) to be added to rating.
1598      $table = new xmldb_table('rating');
1599      $key = new xmldb_key('scaleid', XMLDB_KEY_FOREIGN, ['scaleid'], 'scale', ['id']);
1600      // Launch add key scaleid.
1601      $dbman->add_key($table, $key);
1602  
1603      // Define key courseid (foreign) to be added to course_published.
1604      $table = new xmldb_table('course_published');
1605      $key = new xmldb_key('courseid', XMLDB_KEY_FOREIGN, ['courseid'], 'course', ['id']);
1606      // Launch add key courseid.
1607      $dbman->add_key($table, $key);
1608  
1609      // Define index hubcourseid (not unique) to be added to course_published.
1610      $table = new xmldb_table('course_published');
1611      $index = new xmldb_index('hubcourseid', XMLDB_INDEX_NOTUNIQUE, ['hubcourseid']);
1612      // Conditionally launch add index hubcourseid.
1613      if (!$dbman->index_exists($table, $index)) {
1614          $dbman->add_index($table, $index);
1615      }
1616  
1617      // Define key courseid (foreign) to be added to event_subscriptions.
1618      $table = new xmldb_table('event_subscriptions');
1619      $key = new xmldb_key('courseid', XMLDB_KEY_FOREIGN, ['courseid'], 'course', ['id']);
1620      // Launch add key courseid.
1621      $dbman->add_key($table, $key);
1622  
1623      // Define key userid (foreign) to be added to event_subscriptions.
1624      $table = new xmldb_table('event_subscriptions');
1625      $key = new xmldb_key('userid', XMLDB_KEY_FOREIGN, ['userid'], 'user', ['id']);
1626      // Launch add key userid.
1627      $dbman->add_key($table, $key);
1628  
1629      // Define key userid (foreign) to be added to task_log.
1630      $table = new xmldb_table('task_log');
1631      $key = new xmldb_key('userid', XMLDB_KEY_FOREIGN, ['userid'], 'user', ['id']);
1632      // Launch add key userid.
1633      $dbman->add_key($table, $key);
1634  
1635      // Define key scaleid (foreign) to be added to competency.
1636      $table = new xmldb_table('competency');
1637      $key = new xmldb_key('scaleid', XMLDB_KEY_FOREIGN, ['scaleid'], 'scale', ['id']);
1638      // Launch add key scaleid.
1639      $dbman->add_key($table, $key);
1640  
1641      // Define key usermodified (foreign) to be added to competency.
1642      $table = new xmldb_table('competency');
1643      $key = new xmldb_key('usermodified', XMLDB_KEY_FOREIGN, ['usermodified'], 'user', ['id']);
1644      // Launch add key usermodified.
1645      $dbman->add_key($table, $key);
1646  
1647      // Define key usermodified (foreign) to be added to competency_coursecompsetting.
1648      $table = new xmldb_table('competency_coursecompsetting');
1649      $key = new xmldb_key('usermodified', XMLDB_KEY_FOREIGN, ['usermodified'], 'user', ['id']);
1650      // Launch add key usermodified.
1651      $dbman->add_key($table, $key);
1652  
1653      // Define key contextid (foreign) to be added to competency_framework.
1654      $table = new xmldb_table('competency_framework');
1655      $key = new xmldb_key('contextid', XMLDB_KEY_FOREIGN, ['contextid'], 'context', ['id']);
1656      // Launch add key contextid.
1657      $dbman->add_key($table, $key);
1658  
1659      // Define key scaleid (foreign) to be added to competency_framework.
1660      $table = new xmldb_table('competency_framework');
1661      $key = new xmldb_key('scaleid', XMLDB_KEY_FOREIGN, ['scaleid'], 'scale', ['id']);
1662      // Launch add key scaleid.
1663      $dbman->add_key($table, $key);
1664  
1665      // Define key usermodified (foreign) to be added to competency_framework.
1666      $table = new xmldb_table('competency_framework');
1667      $key = new xmldb_key('usermodified', XMLDB_KEY_FOREIGN, ['usermodified'], 'user', ['id']);
1668      // Launch add key usermodified.
1669      $dbman->add_key($table, $key);
1670  
1671      // Define key usermodified (foreign) to be added to competency_coursecomp.
1672      $table = new xmldb_table('competency_coursecomp');
1673      $key = new xmldb_key('usermodified', XMLDB_KEY_FOREIGN, ['usermodified'], 'user', ['id']);
1674      // Launch add key usermodified.
1675      $dbman->add_key($table, $key);
1676  
1677      // Define key actionuserid (foreign) to be added to competency_evidence.
1678      $table = new xmldb_table('competency_evidence');
1679      $key = new xmldb_key('actionuserid', XMLDB_KEY_FOREIGN, ['actionuserid'], 'user', ['id']);
1680      // Launch add key actionuserid.
1681      $dbman->add_key($table, $key);
1682  
1683      // Define key contextid (foreign) to be added to competency_evidence.
1684      $table = new xmldb_table('competency_evidence');
1685      $key = new xmldb_key('contextid', XMLDB_KEY_FOREIGN, ['contextid'], 'context', ['id']);
1686      // Launch add key contextid.
1687      $dbman->add_key($table, $key);
1688  
1689      // Define key usermodified (foreign) to be added to competency_evidence.
1690      $table = new xmldb_table('competency_evidence');
1691      $key = new xmldb_key('usermodified', XMLDB_KEY_FOREIGN, ['usermodified'], 'user', ['id']);
1692      // Launch add key usermodified.
1693      $dbman->add_key($table, $key);
1694  
1695      // Define key usermodified (foreign) to be added to competency_userevidence.
1696      $table = new xmldb_table('competency_userevidence');
1697      $key = new xmldb_key('usermodified', XMLDB_KEY_FOREIGN, ['usermodified'], 'user', ['id']);
1698      // Launch add key usermodified.
1699      $dbman->add_key($table, $key);
1700  
1701      // Define key usermodified (foreign) to be added to competency_plan.
1702      $table = new xmldb_table('competency_plan');
1703      $key = new xmldb_key('usermodified', XMLDB_KEY_FOREIGN, ['usermodified'], 'user', ['id']);
1704      // Launch add key usermodified.
1705      $dbman->add_key($table, $key);
1706  
1707      // Define key usermodified (foreign) to be added to competency_template.
1708      $table = new xmldb_table('competency_template');
1709      $key = new xmldb_key('usermodified', XMLDB_KEY_FOREIGN, ['usermodified'], 'user', ['id']);
1710      // Launch add key usermodified.
1711      $dbman->add_key($table, $key);
1712  
1713      // Define key contextid (foreign) to be added to competency_template.
1714      $table = new xmldb_table('competency_template');
1715      $key = new xmldb_key('contextid', XMLDB_KEY_FOREIGN, ['contextid'], 'context', ['id']);
1716      // Launch add key contextid.
1717      $dbman->add_key($table, $key);
1718  
1719      // Define key usermodified (foreign) to be added to competency_templatecomp.
1720      $table = new xmldb_table('competency_templatecomp');
1721      $key = new xmldb_key('usermodified', XMLDB_KEY_FOREIGN, ['usermodified'], 'user', ['id']);
1722      // Launch add key usermodified.
1723      $dbman->add_key($table, $key);
1724  
1725      // Define key usermodified (foreign) to be added to competency_templatecohort.
1726      $table = new xmldb_table('competency_templatecohort');
1727      $key = new xmldb_key('usermodified', XMLDB_KEY_FOREIGN, ['usermodified'], 'user', ['id']);
1728      // Launch add key usermodified.
1729      $dbman->add_key($table, $key);
1730  
1731      // Define key competencyid (foreign) to be added to competency_relatedcomp.
1732      $table = new xmldb_table('competency_relatedcomp');
1733      $key = new xmldb_key('competencyid', XMLDB_KEY_FOREIGN, ['competencyid'], 'competency', ['id']);
1734      // Launch add key competencyid.
1735      $dbman->add_key($table, $key);
1736  
1737      // Define key relatedcompetencyid (foreign) to be added to competency_relatedcomp.
1738      $table = new xmldb_table('competency_relatedcomp');
1739      $key = new xmldb_key('relatedcompetencyid', XMLDB_KEY_FOREIGN, ['relatedcompetencyid'], 'competency', ['id']);
1740      // Launch add key relatedcompetencyid.
1741      $dbman->add_key($table, $key);
1742  
1743      // Define key usermodified (foreign) to be added to competency_relatedcomp.
1744      $table = new xmldb_table('competency_relatedcomp');
1745      $key = new xmldb_key('usermodified', XMLDB_KEY_FOREIGN, ['usermodified'], 'user', ['id']);
1746      // Launch add key usermodified.
1747      $dbman->add_key($table, $key);
1748  
1749      // Define key usermodified (foreign) to be added to competency_usercomp.
1750      $table = new xmldb_table('competency_usercomp');
1751      $key = new xmldb_key('usermodified', XMLDB_KEY_FOREIGN, ['usermodified'], 'user', ['id']);
1752      // Launch add key usermodified.
1753      $dbman->add_key($table, $key);
1754  
1755      // Define key usermodified (foreign) to be added to competency_usercompcourse.
1756      $table = new xmldb_table('competency_usercompcourse');
1757      $key = new xmldb_key('usermodified', XMLDB_KEY_FOREIGN, ['usermodified'], 'user', ['id']);
1758      // Launch add key usermodified.
1759      $dbman->add_key($table, $key);
1760  
1761      // Define key usermodified (foreign) to be added to competency_usercompplan.
1762      $table = new xmldb_table('competency_usercompplan');
1763      $key = new xmldb_key('usermodified', XMLDB_KEY_FOREIGN, ['usermodified'], 'user', ['id']);
1764      // Launch add key usermodified.
1765      $dbman->add_key($table, $key);
1766  
1767      // Define key usermodified (foreign) to be added to competency_plancomp.
1768      $table = new xmldb_table('competency_plancomp');
1769      $key = new xmldb_key('usermodified', XMLDB_KEY_FOREIGN, ['usermodified'], 'user', ['id']);
1770      // Launch add key usermodified.
1771      $dbman->add_key($table, $key);
1772  
1773      // Define key usermodified (foreign) to be added to competency_userevidencecomp.
1774      $table = new xmldb_table('competency_userevidencecomp');
1775      $key = new xmldb_key('usermodified', XMLDB_KEY_FOREIGN, ['usermodified'], 'user', ['id']);
1776      // Launch add key usermodified.
1777      $dbman->add_key($table, $key);
1778  
1779      // Define key usermodified (foreign) to be added to competency_modulecomp.
1780      $table = new xmldb_table('competency_modulecomp');
1781      $key = new xmldb_key('usermodified', XMLDB_KEY_FOREIGN, ['usermodified'], 'user', ['id']);
1782      // Launch add key usermodified.
1783      $dbman->add_key($table, $key);
1784  
1785      // Define key usermodified (foreign) to be added to oauth2_endpoint.
1786      $table = new xmldb_table('oauth2_endpoint');
1787      $key = new xmldb_key('usermodified', XMLDB_KEY_FOREIGN, ['usermodified'], 'user', ['id']);
1788      // Launch add key usermodified.
1789      $dbman->add_key($table, $key);
1790  
1791      // Define key usermodified (foreign) to be added to oauth2_system_account.
1792      $table = new xmldb_table('oauth2_system_account');
1793      $key = new xmldb_key('usermodified', XMLDB_KEY_FOREIGN, ['usermodified'], 'user', ['id']);
1794      // Launch add key usermodified.
1795      $dbman->add_key($table, $key);
1796  
1797      // Define key usermodified (foreign) to be added to oauth2_user_field_mapping.
1798      $table = new xmldb_table('oauth2_user_field_mapping');
1799      $key = new xmldb_key('usermodified', XMLDB_KEY_FOREIGN, ['usermodified'], 'user', ['id']);
1800      // Launch add key usermodified.
1801      $dbman->add_key($table, $key);
1802  
1803      // Define key usermodified (foreign) to be added to analytics_models.
1804      $table = new xmldb_table('analytics_models');
1805      $key = new xmldb_key('usermodified', XMLDB_KEY_FOREIGN, ['usermodified'], 'user', ['id']);
1806      // Launch add key usermodified.
1807      $dbman->add_key($table, $key);
1808  
1809      // Define key usermodified (foreign) to be added to analytics_models_log.
1810      $table = new xmldb_table('analytics_models_log');
1811      $key = new xmldb_key('usermodified', XMLDB_KEY_FOREIGN, ['usermodified'], 'user', ['id']);
1812      // Launch add key usermodified.
1813      $dbman->add_key($table, $key);
1814  
1815      // Define key usermodified (foreign) to be added to oauth2_access_token.
1816      $table = new xmldb_table('oauth2_access_token');
1817      $key = new xmldb_key('usermodified', XMLDB_KEY_FOREIGN, ['usermodified'], 'user', ['id']);
1818      // Launch add key usermodified.
1819      $dbman->add_key($table, $key);
1820  
1821      // Define key contextid (foreign) to be added to payment_accounts.
1822      $table = new xmldb_table('payment_accounts');
1823      $key = new xmldb_key('contextid', XMLDB_KEY_FOREIGN, ['contextid'], 'context', ['id']);
1824      // Launch add key contextid.
1825      $dbman->add_key($table, $key);
1826  }