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.
/course/ -> lib.php (source)

Differences Between: [Versions 310 and 401] [Versions 311 and 401] [Versions 39 and 401] [Versions 400 and 401] [Versions 401 and 402] [Versions 401 and 403]

   1  <?php
   2  // This file is part of Moodle - http://moodle.org/
   3  //
   4  // Moodle is free software: you can redistribute it and/or modify
   5  // it under the terms of the GNU General Public License as published by
   6  // the Free Software Foundation, either version 3 of the License, or
   7  // (at your option) any later version.
   8  //
   9  // Moodle is distributed in the hope that it will be useful,
  10  // but WITHOUT ANY WARRANTY; without even the implied warranty of
  11  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  12  // GNU General Public License for more details.
  13  //
  14  // You should have received a copy of the GNU General Public License
  15  // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
  16  
  17  /**
  18   * Library of useful functions
  19   *
  20   * @copyright 1999 Martin Dougiamas  http://dougiamas.com
  21   * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  22   * @package core_course
  23   */
  24  
  25  defined('MOODLE_INTERNAL') || die;
  26  
  27  use core_courseformat\base as course_format;
  28  
  29  require_once($CFG->libdir.'/completionlib.php');
  30  require_once($CFG->libdir.'/filelib.php');
  31  require_once($CFG->libdir.'/datalib.php');
  32  require_once($CFG->dirroot.'/course/format/lib.php');
  33  
  34  define('COURSE_MAX_LOGS_PER_PAGE', 1000);       // Records.
  35  define('COURSE_MAX_RECENT_PERIOD', 172800);     // Two days, in seconds.
  36  
  37  /**
  38   * Number of courses to display when summaries are included.
  39   * @var int
  40   * @deprecated since 2.4, use $CFG->courseswithsummarieslimit instead.
  41   */
  42  define('COURSE_MAX_SUMMARIES_PER_PAGE', 10);
  43  
  44  // Max courses in log dropdown before switching to optional.
  45  define('COURSE_MAX_COURSES_PER_DROPDOWN', 1000);
  46  // Max users in log dropdown before switching to optional.
  47  define('COURSE_MAX_USERS_PER_DROPDOWN', 1000);
  48  define('FRONTPAGENEWS', '0');
  49  define('FRONTPAGECATEGORYNAMES', '2');
  50  define('FRONTPAGECATEGORYCOMBO', '4');
  51  define('FRONTPAGEENROLLEDCOURSELIST', '5');
  52  define('FRONTPAGEALLCOURSELIST', '6');
  53  define('FRONTPAGECOURSESEARCH', '7');
  54  // Important! Replaced with $CFG->frontpagecourselimit - maximum number of courses displayed on the frontpage.
  55  define('EXCELROWS', 65535);
  56  define('FIRSTUSEDEXCELROW', 3);
  57  
  58  define('MOD_CLASS_ACTIVITY', 0);
  59  define('MOD_CLASS_RESOURCE', 1);
  60  
  61  define('COURSE_TIMELINE_ALLINCLUDINGHIDDEN', 'allincludinghidden');
  62  define('COURSE_TIMELINE_ALL', 'all');
  63  define('COURSE_TIMELINE_PAST', 'past');
  64  define('COURSE_TIMELINE_INPROGRESS', 'inprogress');
  65  define('COURSE_TIMELINE_FUTURE', 'future');
  66  define('COURSE_TIMELINE_SEARCH', 'search');
  67  define('COURSE_FAVOURITES', 'favourites');
  68  define('COURSE_TIMELINE_HIDDEN', 'hidden');
  69  define('COURSE_CUSTOMFIELD', 'customfield');
  70  define('COURSE_DB_QUERY_LIMIT', 1000);
  71  /** Searching for all courses that have no value for the specified custom field. */
  72  define('COURSE_CUSTOMFIELD_EMPTY', -1);
  73  
  74  // Course activity chooser footer default display option.
  75  define('COURSE_CHOOSER_FOOTER_NONE', 'hidden');
  76  
  77  // Download course content options.
  78  define('DOWNLOAD_COURSE_CONTENT_DISABLED', 0);
  79  define('DOWNLOAD_COURSE_CONTENT_ENABLED', 1);
  80  define('DOWNLOAD_COURSE_CONTENT_SITE_DEFAULT', 2);
  81  
  82  function make_log_url($module, $url) {
  83      switch ($module) {
  84          case 'course':
  85              if (strpos($url, 'report/') === 0) {
  86                  // there is only one report type, course reports are deprecated
  87                  $url = "/$url";
  88                  break;
  89              }
  90          case 'file':
  91          case 'login':
  92          case 'lib':
  93          case 'admin':
  94          case 'category':
  95          case 'mnet course':
  96              if (strpos($url, '../') === 0) {
  97                  $url = ltrim($url, '.');
  98              } else {
  99                  $url = "/course/$url";
 100              }
 101              break;
 102          case 'calendar':
 103              $url = "/calendar/$url";
 104              break;
 105          case 'user':
 106          case 'blog':
 107              $url = "/$module/$url";
 108              break;
 109          case 'upload':
 110              $url = $url;
 111              break;
 112          case 'coursetags':
 113              $url = '/'.$url;
 114              break;
 115          case 'library':
 116          case '':
 117              $url = '/';
 118              break;
 119          case 'message':
 120              $url = "/message/$url";
 121              break;
 122          case 'notes':
 123              $url = "/notes/$url";
 124              break;
 125          case 'tag':
 126              $url = "/tag/$url";
 127              break;
 128          case 'role':
 129              $url = '/'.$url;
 130              break;
 131          case 'grade':
 132              $url = "/grade/$url";
 133              break;
 134          default:
 135              $url = "/mod/$module/$url";
 136              break;
 137      }
 138  
 139      //now let's sanitise urls - there might be some ugly nasties:-(
 140      $parts = explode('?', $url);
 141      $script = array_shift($parts);
 142      if (strpos($script, 'http') === 0) {
 143          $script = clean_param($script, PARAM_URL);
 144      } else {
 145          $script = clean_param($script, PARAM_PATH);
 146      }
 147  
 148      $query = '';
 149      if ($parts) {
 150          $query = implode('', $parts);
 151          $query = str_replace('&amp;', '&', $query); // both & and &amp; are stored in db :-|
 152          $parts = explode('&', $query);
 153          $eq = urlencode('=');
 154          foreach ($parts as $key=>$part) {
 155              $part = urlencode(urldecode($part));
 156              $part = str_replace($eq, '=', $part);
 157              $parts[$key] = $part;
 158          }
 159          $query = '?'.implode('&amp;', $parts);
 160      }
 161  
 162      return $script.$query;
 163  }
 164  
 165  
 166  function build_mnet_logs_array($hostid, $course, $user=0, $date=0, $order="l.time ASC", $limitfrom='', $limitnum='',
 167                     $modname="", $modid=0, $modaction="", $groupid=0) {
 168      global $CFG, $DB;
 169  
 170      // It is assumed that $date is the GMT time of midnight for that day,
 171      // and so the next 86400 seconds worth of logs are printed.
 172  
 173      /// Setup for group handling.
 174  
 175      // TODO: I don't understand group/context/etc. enough to be able to do
 176      // something interesting with it here
 177      // What is the context of a remote course?
 178  
 179      /// If the group mode is separate, and this user does not have editing privileges,
 180      /// then only the user's group can be viewed.
 181      //if ($course->groupmode == SEPARATEGROUPS and !has_capability('moodle/course:managegroups', context_course::instance($course->id))) {
 182      //    $groupid = get_current_group($course->id);
 183      //}
 184      /// If this course doesn't have groups, no groupid can be specified.
 185      //else if (!$course->groupmode) {
 186      //    $groupid = 0;
 187      //}
 188  
 189      $groupid = 0;
 190  
 191      $joins = array();
 192      $where = '';
 193  
 194      $qry = "SELECT l.*, u.firstname, u.lastname, u.picture
 195                FROM {mnet_log} l
 196                 LEFT JOIN {user} u ON l.userid = u.id
 197                WHERE ";
 198      $params = array();
 199  
 200      $where .= "l.hostid = :hostid";
 201      $params['hostid'] = $hostid;
 202  
 203      // TODO: Is 1 really a magic number referring to the sitename?
 204      if ($course != SITEID || $modid != 0) {
 205          $where .= " AND l.course=:courseid";
 206          $params['courseid'] = $course;
 207      }
 208  
 209      if ($modname) {
 210          $where .= " AND l.module = :modname";
 211          $params['modname'] = $modname;
 212      }
 213  
 214      if ('site_errors' === $modid) {
 215          $where .= " AND ( l.action='error' OR l.action='infected' )";
 216      } else if ($modid) {
 217          //TODO: This assumes that modids are the same across sites... probably
 218          //not true
 219          $where .= " AND l.cmid = :modid";
 220          $params['modid'] = $modid;
 221      }
 222  
 223      if ($modaction) {
 224          $firstletter = substr($modaction, 0, 1);
 225          if ($firstletter == '-') {
 226              $where .= " AND ".$DB->sql_like('l.action', ':modaction', false, true, true);
 227              $params['modaction'] = '%'.substr($modaction, 1).'%';
 228          } else {
 229              $where .= " AND ".$DB->sql_like('l.action', ':modaction', false);
 230              $params['modaction'] = '%'.$modaction.'%';
 231          }
 232      }
 233  
 234      if ($user) {
 235          $where .= " AND l.userid = :user";
 236          $params['user'] = $user;
 237      }
 238  
 239      if ($date) {
 240          $enddate = $date + 86400;
 241          $where .= " AND l.time > :date AND l.time < :enddate";
 242          $params['date'] = $date;
 243          $params['enddate'] = $enddate;
 244      }
 245  
 246      $result = array();
 247      $result['totalcount'] = $DB->count_records_sql("SELECT COUNT('x') FROM {mnet_log} l WHERE $where", $params);
 248      if(!empty($result['totalcount'])) {
 249          $where .= " ORDER BY $order";
 250          $result['logs'] = $DB->get_records_sql("$qry $where", $params, $limitfrom, $limitnum);
 251      } else {
 252          $result['logs'] = array();
 253      }
 254      return $result;
 255  }
 256  
 257  /**
 258   * Checks the integrity of the course data.
 259   *
 260   * In summary - compares course_sections.sequence and course_modules.section.
 261   *
 262   * More detailed, checks that:
 263   * - course_sections.sequence contains each module id not more than once in the course
 264   * - for each moduleid from course_sections.sequence the field course_modules.section
 265   *   refers to the same section id (this means course_sections.sequence is more
 266   *   important if they are different)
 267   * - ($fullcheck only) each module in the course is present in one of
 268   *   course_sections.sequence
 269   * - ($fullcheck only) removes non-existing course modules from section sequences
 270   *
 271   * If there are any mismatches, the changes are made and records are updated in DB.
 272   *
 273   * Course cache is NOT rebuilt if there are any errors!
 274   *
 275   * This function is used each time when course cache is being rebuilt with $fullcheck = false
 276   * and in CLI script admin/cli/fix_course_sequence.php with $fullcheck = true
 277   *
 278   * @param int $courseid id of the course
 279   * @param array $rawmods result of funciton {@link get_course_mods()} - containst
 280   *     the list of enabled course modules in the course. Retrieved from DB if not specified.
 281   *     Argument ignored in cashe of $fullcheck, the list is retrieved form DB anyway.
 282   * @param array $sections records from course_sections table for this course.
 283   *     Retrieved from DB if not specified
 284   * @param bool $fullcheck Will add orphaned modules to their sections and remove non-existing
 285   *     course modules from sequences. Only to be used in site maintenance mode when we are
 286   *     sure that another user is not in the middle of the process of moving/removing a module.
 287   * @param bool $checkonly Only performs the check without updating DB, outputs all errors as debug messages.
 288   * @return array array of messages with found problems. Empty output means everything is ok
 289   */
 290  function course_integrity_check($courseid, $rawmods = null, $sections = null, $fullcheck = false, $checkonly = false) {
 291      global $DB;
 292      $messages = array();
 293      if ($sections === null) {
 294          $sections = $DB->get_records('course_sections', array('course' => $courseid), 'section', 'id,section,sequence');
 295      }
 296      if ($fullcheck) {
 297          // Retrieve all records from course_modules regardless of module type visibility.
 298          $rawmods = $DB->get_records('course_modules', array('course' => $courseid), 'id', 'id,section');
 299      }
 300      if ($rawmods === null) {
 301          $rawmods = get_course_mods($courseid);
 302      }
 303      if (!$fullcheck && (empty($sections) || empty($rawmods))) {
 304          // If either of the arrays is empty, no modules are displayed anyway.
 305          return true;
 306      }
 307      $debuggingprefix = 'Failed integrity check for course ['.$courseid.']. ';
 308  
 309      // First make sure that each module id appears in section sequences only once.
 310      // If it appears in several section sequences the last section wins.
 311      // If it appears twice in one section sequence, the first occurence wins.
 312      $modsection = array();
 313      foreach ($sections as $sectionid => $section) {
 314          $sections[$sectionid]->newsequence = $section->sequence;
 315          if (!empty($section->sequence)) {
 316              $sequence = explode(",", $section->sequence);
 317              $sequenceunique = array_unique($sequence);
 318              if (count($sequenceunique) != count($sequence)) {
 319                  // Some course module id appears in this section sequence more than once.
 320                  ksort($sequenceunique); // Preserve initial order of modules.
 321                  $sequence = array_values($sequenceunique);
 322                  $sections[$sectionid]->newsequence = join(',', $sequence);
 323                  $messages[] = $debuggingprefix.'Sequence for course section ['.
 324                          $sectionid.'] is "'.$sections[$sectionid]->sequence.'", must be "'.$sections[$sectionid]->newsequence.'"';
 325              }
 326              foreach ($sequence as $cmid) {
 327                  if (array_key_exists($cmid, $modsection) && isset($rawmods[$cmid])) {
 328                      // Some course module id appears to be in more than one section's sequences.
 329                      $wrongsectionid = $modsection[$cmid];
 330                      $sections[$wrongsectionid]->newsequence = trim(preg_replace("/,$cmid,/", ',', ','.$sections[$wrongsectionid]->newsequence. ','), ',');
 331                      $messages[] = $debuggingprefix.'Course module ['.$cmid.'] must be removed from sequence of section ['.
 332                              $wrongsectionid.'] because it is also present in sequence of section ['.$sectionid.']';
 333                  }
 334                  $modsection[$cmid] = $sectionid;
 335              }
 336          }
 337      }
 338  
 339      // Add orphaned modules to their sections if they exist or to section 0 otherwise.
 340      if ($fullcheck) {
 341          foreach ($rawmods as $cmid => $mod) {
 342              if (!isset($modsection[$cmid])) {
 343                  // This is a module that is not mentioned in course_section.sequence at all.
 344                  // Add it to the section $mod->section or to the last available section.
 345                  if ($mod->section && isset($sections[$mod->section])) {
 346                      $modsection[$cmid] = $mod->section;
 347                  } else {
 348                      $firstsection = reset($sections);
 349                      $modsection[$cmid] = $firstsection->id;
 350                  }
 351                  $sections[$modsection[$cmid]]->newsequence = trim($sections[$modsection[$cmid]]->newsequence.','.$cmid, ',');
 352                  $messages[] = $debuggingprefix.'Course module ['.$cmid.'] is missing from sequence of section ['.
 353                          $modsection[$cmid].']';
 354              }
 355          }
 356          foreach ($modsection as $cmid => $sectionid) {
 357              if (!isset($rawmods[$cmid])) {
 358                  // Section $sectionid refers to module id that does not exist.
 359                  $sections[$sectionid]->newsequence = trim(preg_replace("/,$cmid,/", ',', ','.$sections[$sectionid]->newsequence.','), ',');
 360                  $messages[] = $debuggingprefix.'Course module ['.$cmid.
 361                          '] does not exist but is present in the sequence of section ['.$sectionid.']';
 362              }
 363          }
 364      }
 365  
 366      // Update changed sections.
 367      if (!$checkonly && !empty($messages)) {
 368          foreach ($sections as $sectionid => $section) {
 369              if ($section->newsequence !== $section->sequence) {
 370                  $DB->update_record('course_sections', array('id' => $sectionid, 'sequence' => $section->newsequence));
 371              }
 372          }
 373      }
 374  
 375      // Now make sure that all modules point to the correct sections.
 376      foreach ($rawmods as $cmid => $mod) {
 377          if (isset($modsection[$cmid]) && $modsection[$cmid] != $mod->section) {
 378              if (!$checkonly) {
 379                  $DB->update_record('course_modules', array('id' => $cmid, 'section' => $modsection[$cmid]));
 380              }
 381              $messages[] = $debuggingprefix.'Course module ['.$cmid.
 382                      '] points to section ['.$mod->section.'] instead of ['.$modsection[$cmid].']';
 383          }
 384      }
 385  
 386      return $messages;
 387  }
 388  
 389  /**
 390   * Returns an array where the key is the module name (component name without 'mod_')
 391   * and the value is a lang_string object with a human-readable string.
 392   *
 393   * @param bool $plural If true, the function returns the plural forms of the names.
 394   * @param bool $resetcache If true, the static cache will be reset
 395   * @return lang_string[] Localised human-readable names of all used modules.
 396   */
 397  function get_module_types_names($plural = false, $resetcache = false) {
 398      static $modnames = null;
 399      global $DB, $CFG;
 400      if ($modnames === null || $resetcache) {
 401          $modnames = array(0 => array(), 1 => array());
 402          if ($allmods = $DB->get_records("modules")) {
 403              foreach ($allmods as $mod) {
 404                  if (file_exists("$CFG->dirroot/mod/$mod->name/lib.php") && $mod->visible) {
 405                      $modnames[0][$mod->name] = get_string("modulename", "$mod->name", null, true);
 406                      $modnames[1][$mod->name] = get_string("modulenameplural", "$mod->name", null, true);
 407                  }
 408              }
 409          }
 410      }
 411      return $modnames[(int)$plural];
 412  }
 413  
 414  /**
 415   * Set highlighted section. Only one section can be highlighted at the time.
 416   *
 417   * @param int $courseid course id
 418   * @param int $marker highlight section with this number, 0 means remove higlightin
 419   * @return void
 420   */
 421  function course_set_marker($courseid, $marker) {
 422      global $DB, $COURSE;
 423      $DB->set_field("course", "marker", $marker, array('id' => $courseid));
 424      if ($COURSE && $COURSE->id == $courseid) {
 425          $COURSE->marker = $marker;
 426      }
 427      core_courseformat\base::reset_course_cache($courseid);
 428      course_modinfo::clear_instance_cache($courseid);
 429  }
 430  
 431  /**
 432   * For a given course section, marks it visible or hidden,
 433   * and does the same for every activity in that section
 434   *
 435   * @param int $courseid course id
 436   * @param int $sectionnumber The section number to adjust
 437   * @param int $visibility The new visibility
 438   * @return array A list of resources which were hidden in the section
 439   */
 440  function set_section_visible($courseid, $sectionnumber, $visibility) {
 441      global $DB;
 442  
 443      $resourcestotoggle = array();
 444      if ($section = $DB->get_record("course_sections", array("course"=>$courseid, "section"=>$sectionnumber))) {
 445          course_update_section($courseid, $section, array('visible' => $visibility));
 446  
 447          // Determine which modules are visible for AJAX update
 448          $modules = !empty($section->sequence) ? explode(',', $section->sequence) : array();
 449          if (!empty($modules)) {
 450              list($insql, $params) = $DB->get_in_or_equal($modules);
 451              $select = 'id ' . $insql . ' AND visible = ?';
 452              array_push($params, $visibility);
 453              if (!$visibility) {
 454                  $select .= ' AND visibleold = 1';
 455              }
 456              $resourcestotoggle = $DB->get_fieldset_select('course_modules', 'id', $select, $params);
 457          }
 458      }
 459      return $resourcestotoggle;
 460  }
 461  
 462  /**
 463   * Return the course category context for the category with id $categoryid, except
 464   * that if $categoryid is 0, return the system context.
 465   *
 466   * @param integer $categoryid a category id or 0.
 467   * @return context the corresponding context
 468   */
 469  function get_category_or_system_context($categoryid) {
 470      if ($categoryid) {
 471          return context_coursecat::instance($categoryid, IGNORE_MISSING);
 472      } else {
 473          return context_system::instance();
 474      }
 475  }
 476  
 477  /**
 478   * Print the buttons relating to course requests.
 479   *
 480   * @param context $context current page context.
 481   * @deprecated since Moodle 4.0
 482   * @todo Final deprecation MDL-73976
 483   */
 484  function print_course_request_buttons($context) {
 485      global $CFG, $DB, $OUTPUT;
 486      debugging("print_course_request_buttons() is deprecated. " .
 487          "This is replaced with the category_action_bar tertiary navigation.", DEBUG_DEVELOPER);
 488      if (empty($CFG->enablecourserequests)) {
 489          return;
 490      }
 491      if (course_request::can_request($context)) {
 492          // Print a button to request a new course.
 493          $params = [];
 494          if ($context instanceof context_coursecat) {
 495              $params['category'] = $context->instanceid;
 496          }
 497          echo $OUTPUT->single_button(new moodle_url('/course/request.php', $params),
 498              get_string('requestcourse'), 'get');
 499      }
 500      /// Print a button to manage pending requests
 501      if (has_capability('moodle/site:approvecourse', $context)) {
 502          $disabled = !$DB->record_exists('course_request', array());
 503          echo $OUTPUT->single_button(new moodle_url('/course/pending.php'), get_string('coursespending'), 'get', array('disabled' => $disabled));
 504      }
 505  }
 506  
 507  /**
 508   * Does the user have permission to edit things in this category?
 509   *
 510   * @param integer $categoryid The id of the category we are showing, or 0 for system context.
 511   * @return boolean has_any_capability(array(...), ...); in the appropriate context.
 512   */
 513  function can_edit_in_category($categoryid = 0) {
 514      $context = get_category_or_system_context($categoryid);
 515      return has_any_capability(array('moodle/category:manage', 'moodle/course:create'), $context);
 516  }
 517  
 518  /// MODULE FUNCTIONS /////////////////////////////////////////////////////////////////
 519  
 520  function add_course_module($mod) {
 521      global $DB;
 522  
 523      $mod->added = time();
 524      unset($mod->id);
 525  
 526      $cmid = $DB->insert_record("course_modules", $mod);
 527      rebuild_course_cache($mod->course, true);
 528      return $cmid;
 529  }
 530  
 531  /**
 532   * Creates a course section and adds it to the specified position
 533   *
 534   * @param int|stdClass $courseorid course id or course object
 535   * @param int $position position to add to, 0 means to the end. If position is greater than
 536   *        number of existing secitons, the section is added to the end. This will become sectionnum of the
 537   *        new section. All existing sections at this or bigger position will be shifted down.
 538   * @param bool $skipcheck the check has already been made and we know that the section with this position does not exist
 539   * @return stdClass created section object
 540   */
 541  function course_create_section($courseorid, $position = 0, $skipcheck = false) {
 542      global $DB;
 543      $courseid = is_object($courseorid) ? $courseorid->id : $courseorid;
 544  
 545      // Find the last sectionnum among existing sections.
 546      if ($skipcheck) {
 547          $lastsection = $position - 1;
 548      } else {
 549          $lastsection = (int)$DB->get_field_sql('SELECT max(section) from {course_sections} WHERE course = ?', [$courseid]);
 550      }
 551  
 552      // First add section to the end.
 553      $cw = new stdClass();
 554      $cw->course   = $courseid;
 555      $cw->section  = $lastsection + 1;
 556      $cw->summary  = '';
 557      $cw->summaryformat = FORMAT_HTML;
 558      $cw->sequence = '';
 559      $cw->name = null;
 560      $cw->visible = 1;
 561      $cw->availability = null;
 562      $cw->timemodified = time();
 563      $cw->id = $DB->insert_record("course_sections", $cw);
 564  
 565      // Now move it to the specified position.
 566      if ($position > 0 && $position <= $lastsection) {
 567          $course = is_object($courseorid) ? $courseorid : get_course($courseorid);
 568          move_section_to($course, $cw->section, $position, true);
 569          $cw->section = $position;
 570      }
 571  
 572      core\event\course_section_created::create_from_section($cw)->trigger();
 573  
 574      rebuild_course_cache($courseid, true);
 575      return $cw;
 576  }
 577  
 578  /**
 579   * Creates missing course section(s) and rebuilds course cache
 580   *
 581   * @param int|stdClass $courseorid course id or course object
 582   * @param int|array $sections list of relative section numbers to create
 583   * @return bool if there were any sections created
 584   */
 585  function course_create_sections_if_missing($courseorid, $sections) {
 586      if (!is_array($sections)) {
 587          $sections = array($sections);
 588      }
 589      $existing = array_keys(get_fast_modinfo($courseorid)->get_section_info_all());
 590      if ($newsections = array_diff($sections, $existing)) {
 591          foreach ($newsections as $sectionnum) {
 592              course_create_section($courseorid, $sectionnum, true);
 593          }
 594          return true;
 595      }
 596      return false;
 597  }
 598  
 599  /**
 600   * Adds an existing module to the section
 601   *
 602   * Updates both tables {course_sections} and {course_modules}
 603   *
 604   * Note: This function does not use modinfo PROVIDED that the section you are
 605   * adding the module to already exists. If the section does not exist, it will
 606   * build modinfo if necessary and create the section.
 607   *
 608   * @param int|stdClass $courseorid course id or course object
 609   * @param int $cmid id of the module already existing in course_modules table
 610   * @param int $sectionnum relative number of the section (field course_sections.section)
 611   *     If section does not exist it will be created
 612   * @param int|stdClass $beforemod id or object with field id corresponding to the module
 613   *     before which the module needs to be included. Null for inserting in the
 614   *     end of the section
 615   * @return int The course_sections ID where the module is inserted
 616   */
 617  function course_add_cm_to_section($courseorid, $cmid, $sectionnum, $beforemod = null) {
 618      global $DB, $COURSE;
 619      if (is_object($beforemod)) {
 620          $beforemod = $beforemod->id;
 621      }
 622      if (is_object($courseorid)) {
 623          $courseid = $courseorid->id;
 624      } else {
 625          $courseid = $courseorid;
 626      }
 627      // Do not try to use modinfo here, there is no guarantee it is valid!
 628      $section = $DB->get_record('course_sections',
 629              array('course' => $courseid, 'section' => $sectionnum), '*', IGNORE_MISSING);
 630      if (!$section) {
 631          // This function call requires modinfo.
 632          course_create_sections_if_missing($courseorid, $sectionnum);
 633          $section = $DB->get_record('course_sections',
 634                  array('course' => $courseid, 'section' => $sectionnum), '*', MUST_EXIST);
 635      }
 636  
 637      $modarray = explode(",", trim($section->sequence));
 638      if (empty($section->sequence)) {
 639          $newsequence = "$cmid";
 640      } else if ($beforemod && ($key = array_keys($modarray, $beforemod))) {
 641          $insertarray = array($cmid, $beforemod);
 642          array_splice($modarray, $key[0], 1, $insertarray);
 643          $newsequence = implode(",", $modarray);
 644      } else {
 645          $newsequence = "$section->sequence,$cmid";
 646      }
 647      $DB->set_field("course_sections", "sequence", $newsequence, array("id" => $section->id));
 648      $DB->set_field('course_modules', 'section', $section->id, array('id' => $cmid));
 649      rebuild_course_cache($courseid, true);
 650      return $section->id;     // Return course_sections ID that was used.
 651  }
 652  
 653  /**
 654   * Change the group mode of a course module.
 655   *
 656   * Note: Do not forget to trigger the event \core\event\course_module_updated as it needs
 657   * to be triggered manually, refer to {@link \core\event\course_module_updated::create_from_cm()}.
 658   *
 659   * @param int $id course module ID.
 660   * @param int $groupmode the new groupmode value.
 661   * @return bool True if the $groupmode was updated.
 662   */
 663  function set_coursemodule_groupmode($id, $groupmode) {
 664      global $DB;
 665      $cm = $DB->get_record('course_modules', array('id' => $id), 'id,course,groupmode', MUST_EXIST);
 666      if ($cm->groupmode != $groupmode) {
 667          $DB->set_field('course_modules', 'groupmode', $groupmode, array('id' => $cm->id));
 668          \course_modinfo::purge_course_module_cache($cm->course, $cm->id);
 669          rebuild_course_cache($cm->course, false, true);
 670      }
 671      return ($cm->groupmode != $groupmode);
 672  }
 673  
 674  function set_coursemodule_idnumber($id, $idnumber) {
 675      global $DB;
 676      $cm = $DB->get_record('course_modules', array('id' => $id), 'id,course,idnumber', MUST_EXIST);
 677      if ($cm->idnumber != $idnumber) {
 678          $DB->set_field('course_modules', 'idnumber', $idnumber, array('id' => $cm->id));
 679          \course_modinfo::purge_course_module_cache($cm->course, $cm->id);
 680          rebuild_course_cache($cm->course, false, true);
 681      }
 682      return ($cm->idnumber != $idnumber);
 683  }
 684  
 685  /**
 686   * Set downloadcontent value to course module.
 687   *
 688   * @param int $id The id of the module.
 689   * @param bool $downloadcontent Whether the module can be downloaded when download course content is enabled.
 690   * @return bool True if downloadcontent has been updated, false otherwise.
 691   */
 692  function set_downloadcontent(int $id, bool $downloadcontent): bool {
 693      global $DB;
 694      $cm = $DB->get_record('course_modules', ['id' => $id], 'id, course, downloadcontent', MUST_EXIST);
 695      if ($cm->downloadcontent != $downloadcontent) {
 696          $DB->set_field('course_modules', 'downloadcontent', $downloadcontent, ['id' => $cm->id]);
 697          rebuild_course_cache($cm->course, true);
 698      }
 699      return ($cm->downloadcontent != $downloadcontent);
 700  }
 701  
 702  /**
 703   * Set the visibility of a module and inherent properties.
 704   *
 705   * Note: Do not forget to trigger the event \core\event\course_module_updated as it needs
 706   * to be triggered manually, refer to {@link \core\event\course_module_updated::create_from_cm()}.
 707   *
 708   * From 2.4 the parameter $prevstateoverrides has been removed, the logic it triggered
 709   * has been moved to {@link set_section_visible()} which was the only place from which
 710   * the parameter was used.
 711   *
 712   * If $rebuildcache is set to false, the calling code is responsible for ensuring the cache is purged
 713   * and rebuilt as appropriate. Consider using this if set_coursemodule_visible is called multiple times
 714   * (e.g. in a loop).
 715   *
 716   * @param int $id of the module
 717   * @param int $visible state of the module
 718   * @param int $visibleoncoursepage state of the module on the course page
 719   * @param bool $rebuildcache If true (default), perform a partial cache purge and rebuild.
 720   * @return bool false when the module was not found, true otherwise
 721   */
 722  function set_coursemodule_visible($id, $visible, $visibleoncoursepage = 1, bool $rebuildcache = true) {
 723      global $DB, $CFG;
 724      require_once($CFG->libdir.'/gradelib.php');
 725      require_once($CFG->dirroot.'/calendar/lib.php');
 726  
 727      if (!$cm = $DB->get_record('course_modules', array('id'=>$id))) {
 728          return false;
 729      }
 730  
 731      // Create events and propagate visibility to associated grade items if the value has changed.
 732      // Only do this if it's changed to avoid accidently overwriting manual showing/hiding of student grades.
 733      if ($cm->visible == $visible && $cm->visibleoncoursepage == $visibleoncoursepage) {
 734          return true;
 735      }
 736  
 737      if (!$modulename = $DB->get_field('modules', 'name', array('id'=>$cm->module))) {
 738          return false;
 739      }
 740      if (($cm->visible != $visible) &&
 741              ($events = $DB->get_records('event', array('instance' => $cm->instance, 'modulename' => $modulename)))) {
 742          foreach($events as $event) {
 743              if ($visible) {
 744                  $event = new calendar_event($event);
 745                  $event->toggle_visibility(true);
 746              } else {
 747                  $event = new calendar_event($event);
 748                  $event->toggle_visibility(false);
 749              }
 750          }
 751      }
 752  
 753      // Updating visible and visibleold to keep them in sync. Only changing a section visibility will
 754      // affect visibleold to allow for an original visibility restore. See set_section_visible().
 755      $cminfo = new stdClass();
 756      $cminfo->id = $id;
 757      $cminfo->visible = $visible;
 758      $cminfo->visibleoncoursepage = $visibleoncoursepage;
 759      $cminfo->visibleold = $visible;
 760      $DB->update_record('course_modules', $cminfo);
 761  
 762      // Hide the associated grade items so the teacher doesn't also have to go to the gradebook and hide them there.
 763      // Note that this must be done after updating the row in course_modules, in case
 764      // the modules grade_item_update function needs to access $cm->visible.
 765      if ($cm->visible != $visible &&
 766              plugin_supports('mod', $modulename, FEATURE_CONTROLS_GRADE_VISIBILITY) &&
 767              component_callback_exists('mod_' . $modulename, 'grade_item_update')) {
 768          $instance = $DB->get_record($modulename, array('id' => $cm->instance), '*', MUST_EXIST);
 769          component_callback('mod_' . $modulename, 'grade_item_update', array($instance));
 770      } else if ($cm->visible != $visible) {
 771          $grade_items = grade_item::fetch_all(array('itemtype'=>'mod', 'itemmodule'=>$modulename, 'iteminstance'=>$cm->instance, 'courseid'=>$cm->course));
 772          if ($grade_items) {
 773              foreach ($grade_items as $grade_item) {
 774                  $grade_item->set_hidden(!$visible);
 775              }
 776          }
 777      }
 778  
 779      if ($rebuildcache) {
 780          \course_modinfo::purge_course_module_cache($cm->course, $cm->id);
 781          rebuild_course_cache($cm->course, false, true);
 782      }
 783      return true;
 784  }
 785  
 786  /**
 787   * Changes the course module name
 788   *
 789   * @param int $id course module id
 790   * @param string $name new value for a name
 791   * @return bool whether a change was made
 792   */
 793  function set_coursemodule_name($id, $name) {
 794      global $CFG, $DB;
 795      require_once($CFG->libdir . '/gradelib.php');
 796  
 797      $cm = get_coursemodule_from_id('', $id, 0, false, MUST_EXIST);
 798  
 799      $module = new \stdClass();
 800      $module->id = $cm->instance;
 801  
 802      // Escape strings as they would be by mform.
 803      if (!empty($CFG->formatstringstriptags)) {
 804          $module->name = clean_param($name, PARAM_TEXT);
 805      } else {
 806          $module->name = clean_param($name, PARAM_CLEANHTML);
 807      }
 808      if ($module->name === $cm->name || strval($module->name) === '') {
 809          return false;
 810      }
 811      if (\core_text::strlen($module->name) > 255) {
 812          throw new \moodle_exception('maximumchars', 'moodle', '', 255);
 813      }
 814  
 815      $module->timemodified = time();
 816      $DB->update_record($cm->modname, $module);
 817      $cm->name = $module->name;
 818      \core\event\course_module_updated::create_from_cm($cm)->trigger();
 819      \course_modinfo::purge_course_module_cache($cm->course, $cm->id);
 820      rebuild_course_cache($cm->course, false, true);
 821  
 822      // Attempt to update the grade item if relevant.
 823      $grademodule = $DB->get_record($cm->modname, array('id' => $cm->instance));
 824      $grademodule->cmidnumber = $cm->idnumber;
 825      $grademodule->modname = $cm->modname;
 826      grade_update_mod_grades($grademodule);
 827  
 828      // Update calendar events with the new name.
 829      course_module_update_calendar_events($cm->modname, $grademodule, $cm);
 830  
 831      return true;
 832  }
 833  
 834  /**
 835   * This function will handle the whole deletion process of a module. This includes calling
 836   * the modules delete_instance function, deleting files, events, grades, conditional data,
 837   * the data in the course_module and course_sections table and adding a module deletion
 838   * event to the DB.
 839   *
 840   * @param int $cmid the course module id
 841   * @param bool $async whether or not to try to delete the module using an adhoc task. Async also depends on a plugin hook.
 842   * @throws moodle_exception
 843   * @since Moodle 2.5
 844   */
 845  function course_delete_module($cmid, $async = false) {
 846      // Check the 'course_module_background_deletion_recommended' hook first.
 847      // Only use asynchronous deletion if at least one plugin returns true and if async deletion has been requested.
 848      // Both are checked because plugins should not be allowed to dictate the deletion behaviour, only support/decline it.
 849      // It's up to plugins to handle things like whether or not they are enabled.
 850      if ($async && $pluginsfunction = get_plugins_with_function('course_module_background_deletion_recommended')) {
 851          foreach ($pluginsfunction as $plugintype => $plugins) {
 852              foreach ($plugins as $pluginfunction) {
 853                  if ($pluginfunction()) {
 854                      return course_module_flag_for_async_deletion($cmid);
 855                  }
 856              }
 857          }
 858      }
 859  
 860      global $CFG, $DB;
 861  
 862      require_once($CFG->libdir.'/gradelib.php');
 863      require_once($CFG->libdir.'/questionlib.php');
 864      require_once($CFG->dirroot.'/blog/lib.php');
 865      require_once($CFG->dirroot.'/calendar/lib.php');
 866  
 867      // Get the course module.
 868      if (!$cm = $DB->get_record('course_modules', array('id' => $cmid))) {
 869          return true;
 870      }
 871  
 872      // Get the module context.
 873      $modcontext = context_module::instance($cm->id);
 874  
 875      // Get the course module name.
 876      $modulename = $DB->get_field('modules', 'name', array('id' => $cm->module), MUST_EXIST);
 877  
 878      // Get the file location of the delete_instance function for this module.
 879      $modlib = "$CFG->dirroot/mod/$modulename/lib.php";
 880  
 881      // Include the file required to call the delete_instance function for this module.
 882      if (file_exists($modlib)) {
 883          require_once($modlib);
 884      } else {
 885          throw new moodle_exception('cannotdeletemodulemissinglib', '', '', null,
 886              "Cannot delete this module as the file mod/$modulename/lib.php is missing.");
 887      }
 888  
 889      $deleteinstancefunction = $modulename . '_delete_instance';
 890  
 891      // Ensure the delete_instance function exists for this module.
 892      if (!function_exists($deleteinstancefunction)) {
 893          throw new moodle_exception('cannotdeletemodulemissingfunc', '', '', null,
 894              "Cannot delete this module as the function {$modulename}_delete_instance is missing in mod/$modulename/lib.php.");
 895      }
 896  
 897      // Allow plugins to use this course module before we completely delete it.
 898      if ($pluginsfunction = get_plugins_with_function('pre_course_module_delete')) {
 899          foreach ($pluginsfunction as $plugintype => $plugins) {
 900              foreach ($plugins as $pluginfunction) {
 901                  $pluginfunction($cm);
 902              }
 903          }
 904      }
 905  
 906      // Call the delete_instance function, if it returns false throw an exception.
 907      if (!$deleteinstancefunction($cm->instance)) {
 908          throw new moodle_exception('cannotdeletemoduleinstance', '', '', null,
 909              "Cannot delete the module $modulename (instance).");
 910      }
 911  
 912      question_delete_activity($cm);
 913  
 914      // Remove all module files in case modules forget to do that.
 915      $fs = get_file_storage();
 916      $fs->delete_area_files($modcontext->id);
 917  
 918      // Delete events from calendar.
 919      if ($events = $DB->get_records('event', array('instance' => $cm->instance, 'modulename' => $modulename))) {
 920          $coursecontext = context_course::instance($cm->course);
 921          foreach($events as $event) {
 922              $event->context = $coursecontext;
 923              $calendarevent = calendar_event::load($event);
 924              $calendarevent->delete();
 925          }
 926      }
 927  
 928      // Delete grade items, outcome items and grades attached to modules.
 929      if ($grade_items = grade_item::fetch_all(array('itemtype' => 'mod', 'itemmodule' => $modulename,
 930                                                     'iteminstance' => $cm->instance, 'courseid' => $cm->course))) {
 931          foreach ($grade_items as $grade_item) {
 932              $grade_item->delete('moddelete');
 933          }
 934      }
 935  
 936      // Delete associated blogs and blog tag instances.
 937      blog_remove_associations_for_module($modcontext->id);
 938  
 939      // Delete completion and availability data; it is better to do this even if the
 940      // features are not turned on, in case they were turned on previously (these will be
 941      // very quick on an empty table).
 942      $DB->delete_records('course_modules_completion', array('coursemoduleid' => $cm->id));
 943      $DB->delete_records('course_modules_viewed', ['coursemoduleid' => $cm->id]);
 944      $DB->delete_records('course_completion_criteria', array('moduleinstance' => $cm->id,
 945                                                              'course' => $cm->course,
 946                                                              'criteriatype' => COMPLETION_CRITERIA_TYPE_ACTIVITY));
 947  
 948      // Delete all tag instances associated with the instance of this module.
 949      core_tag_tag::delete_instances('mod_' . $modulename, null, $modcontext->id);
 950      core_tag_tag::remove_all_item_tags('core', 'course_modules', $cm->id);
 951  
 952      // Notify the competency subsystem.
 953      \core_competency\api::hook_course_module_deleted($cm);
 954  
 955      // Delete the context.
 956      context_helper::delete_instance(CONTEXT_MODULE, $cm->id);
 957  
 958      // Delete the module from the course_modules table.
 959      $DB->delete_records('course_modules', array('id' => $cm->id));
 960  
 961      // Delete module from that section.
 962      if (!delete_mod_from_section($cm->id, $cm->section)) {
 963          throw new moodle_exception('cannotdeletemodulefromsection', '', '', null,
 964              "Cannot delete the module $modulename (instance) from section.");
 965      }
 966  
 967      // Trigger event for course module delete action.
 968      $event = \core\event\course_module_deleted::create(array(
 969          'courseid' => $cm->course,
 970          'context'  => $modcontext,
 971          'objectid' => $cm->id,
 972          'other'    => array(
 973              'modulename'   => $modulename,
 974              'instanceid'   => $cm->instance,
 975          )
 976      ));
 977      $event->add_record_snapshot('course_modules', $cm);
 978      $event->trigger();
 979      \course_modinfo::purge_course_module_cache($cm->course, $cm->id);
 980      rebuild_course_cache($cm->course, false, true);
 981  }
 982  
 983  /**
 984   * Schedule a course module for deletion in the background using an adhoc task.
 985   *
 986   * This method should not be called directly. Instead, please use course_delete_module($cmid, true), to denote async deletion.
 987   * The real deletion of the module is handled by the task, which calls 'course_delete_module($cmid)'.
 988   *
 989   * @param int $cmid the course module id.
 990   * @return bool whether the module was successfully scheduled for deletion.
 991   * @throws \moodle_exception
 992   */
 993  function course_module_flag_for_async_deletion($cmid) {
 994      global $CFG, $DB, $USER;
 995      require_once($CFG->libdir.'/gradelib.php');
 996      require_once($CFG->libdir.'/questionlib.php');
 997      require_once($CFG->dirroot.'/blog/lib.php');
 998      require_once($CFG->dirroot.'/calendar/lib.php');
 999  
1000      // Get the course module.
1001      if (!$cm = $DB->get_record('course_modules', array('id' => $cmid))) {
1002          return true;
1003      }
1004  
1005      // We need to be reasonably certain the deletion is going to succeed before we background the process.
1006      // Make the necessary delete_instance checks, etc. before proceeding further. Throw exceptions if required.
1007  
1008      // Get the course module name.
1009      $modulename = $DB->get_field('modules', 'name', array('id' => $cm->module), MUST_EXIST);
1010  
1011      // Get the file location of the delete_instance function for this module.
1012      $modlib = "$CFG->dirroot/mod/$modulename/lib.php";
1013  
1014      // Include the file required to call the delete_instance function for this module.
1015      if (file_exists($modlib)) {
1016          require_once($modlib);
1017      } else {
1018          throw new \moodle_exception('cannotdeletemodulemissinglib', '', '', null,
1019              "Cannot delete this module as the file mod/$modulename/lib.php is missing.");
1020      }
1021  
1022      $deleteinstancefunction = $modulename . '_delete_instance';
1023  
1024      // Ensure the delete_instance function exists for this module.
1025      if (!function_exists($deleteinstancefunction)) {
1026          throw new \moodle_exception('cannotdeletemodulemissingfunc', '', '', null,
1027              "Cannot delete this module as the function {$modulename}_delete_instance is missing in mod/$modulename/lib.php.");
1028      }
1029  
1030      // We are going to defer the deletion as we can't be sure how long the module's pre_delete code will run for.
1031      $cm->deletioninprogress = '1';
1032      $DB->update_record('course_modules', $cm);
1033  
1034      // Create an adhoc task for the deletion of the course module. The task takes an array of course modules for removal.
1035      $removaltask = new \core_course\task\course_delete_modules();
1036      $removaltask->set_custom_data(array(
1037          'cms' => array($cm),
1038          'userid' => $USER->id,
1039          'realuserid' => \core\session\manager::get_realuser()->id
1040      ));
1041  
1042      // Queue the task for the next run.
1043      \core\task\manager::queue_adhoc_task($removaltask);
1044  
1045      // Reset the course cache to hide the module.
1046      rebuild_course_cache($cm->course, true);
1047  }
1048  
1049  /**
1050   * Checks whether the given course has any course modules scheduled for adhoc deletion.
1051   *
1052   * @param int $courseid the id of the course.
1053   * @param bool $onlygradable whether to check only gradable modules or all modules.
1054   * @return bool true if the course contains any modules pending deletion, false otherwise.
1055   */
1056  function course_modules_pending_deletion(int $courseid, bool $onlygradable = false) : bool {
1057      if (empty($courseid)) {
1058          return false;
1059      }
1060  
1061      if ($onlygradable) {
1062          // Fetch modules with grade items.
1063          if (!$coursegradeitems = grade_item::fetch_all(['itemtype' => 'mod', 'courseid' => $courseid])) {
1064              // Return early when there is none.
1065              return false;
1066          }
1067      }
1068  
1069      $modinfo = get_fast_modinfo($courseid);
1070      foreach ($modinfo->get_cms() as $module) {
1071          if ($module->deletioninprogress == '1') {
1072              if ($onlygradable) {
1073                  // Check if the module being deleted is in the list of course modules with grade items.
1074                  foreach ($coursegradeitems as $coursegradeitem) {
1075                      if ($coursegradeitem->itemmodule == $module->modname && $coursegradeitem->iteminstance == $module->instance) {
1076                          // The module being deleted is within the gradable  modules.
1077                          return true;
1078                      }
1079                  }
1080              } else {
1081                  return true;
1082              }
1083          }
1084      }
1085      return false;
1086  }
1087  
1088  /**
1089   * Checks whether the course module, as defined by modulename and instanceid, is scheduled for deletion within the given course.
1090   *
1091   * @param int $courseid the course id.
1092   * @param string $modulename the module name. E.g. 'assign', 'book', etc.
1093   * @param int $instanceid the module instance id.
1094   * @return bool true if the course module is pending deletion, false otherwise.
1095   */
1096  function course_module_instance_pending_deletion($courseid, $modulename, $instanceid) {
1097      if (empty($courseid) || empty($modulename) || empty($instanceid)) {
1098          return false;
1099      }
1100      $modinfo = get_fast_modinfo($courseid);
1101      $instances = $modinfo->get_instances_of($modulename);
1102      return isset($instances[$instanceid]) && $instances[$instanceid]->deletioninprogress;
1103  }
1104  
1105  function delete_mod_from_section($modid, $sectionid) {
1106      global $DB;
1107  
1108      if ($section = $DB->get_record("course_sections", array("id"=>$sectionid)) ) {
1109  
1110          $modarray = explode(",", $section->sequence);
1111  
1112          if ($key = array_keys ($modarray, $modid)) {
1113              array_splice($modarray, $key[0], 1);
1114              $newsequence = implode(",", $modarray);
1115              $DB->set_field("course_sections", "sequence", $newsequence, array("id"=>$section->id));
1116              rebuild_course_cache($section->course, true);
1117              return true;
1118          } else {
1119              return false;
1120          }
1121  
1122      }
1123      return false;
1124  }
1125  
1126  /**
1127   * This function updates the calendar events from the information stored in the module table and the course
1128   * module table.
1129   *
1130   * @param  string $modulename Module name
1131   * @param  stdClass $instance Module object. Either the $instance or the $cm must be supplied.
1132   * @param  stdClass $cm Course module object. Either the $instance or the $cm must be supplied.
1133   * @return bool Returns true if calendar events are updated.
1134   * @since  Moodle 3.3.4
1135   */
1136  function course_module_update_calendar_events($modulename, $instance = null, $cm = null) {
1137      global $DB;
1138  
1139      if (isset($instance) || isset($cm)) {
1140  
1141          if (!isset($instance)) {
1142              $instance = $DB->get_record($modulename, array('id' => $cm->instance), '*', MUST_EXIST);
1143          }
1144          if (!isset($cm)) {
1145              $cm = get_coursemodule_from_instance($modulename, $instance->id, $instance->course);
1146          }
1147          if (!empty($cm)) {
1148              course_module_calendar_event_update_process($instance, $cm);
1149          }
1150          return true;
1151      }
1152      return false;
1153  }
1154  
1155  /**
1156   * Update all instances through out the site or in a course.
1157   *
1158   * @param  string  $modulename Module type to update.
1159   * @param  integer $courseid   Course id to update events. 0 for the whole site.
1160   * @return bool Returns True if the update was successful.
1161   * @since  Moodle 3.3.4
1162   */
1163  function course_module_bulk_update_calendar_events($modulename, $courseid = 0) {
1164      global $DB;
1165  
1166      $instances = null;
1167      if ($courseid) {
1168          if (!$instances = $DB->get_records($modulename, array('course' => $courseid))) {
1169              return false;
1170          }
1171      } else {
1172          if (!$instances = $DB->get_records($modulename)) {
1173              return false;
1174          }
1175      }
1176  
1177      foreach ($instances as $instance) {
1178          if ($cm = get_coursemodule_from_instance($modulename, $instance->id, $instance->course)) {
1179              course_module_calendar_event_update_process($instance, $cm);
1180          }
1181      }
1182      return true;
1183  }
1184  
1185  /**
1186   * Calendar events for a module instance are updated.
1187   *
1188   * @param  stdClass $instance Module instance object.
1189   * @param  stdClass $cm Course Module object.
1190   * @since  Moodle 3.3.4
1191   */
1192  function course_module_calendar_event_update_process($instance, $cm) {
1193      // We need to call *_refresh_events() first because some modules delete 'old' events at the end of the code which
1194      // will remove the completion events.
1195      $refresheventsfunction = $cm->modname . '_refresh_events';
1196      if (function_exists($refresheventsfunction)) {
1197          call_user_func($refresheventsfunction, $cm->course, $instance, $cm);
1198      }
1199      $completionexpected = (!empty($cm->completionexpected)) ? $cm->completionexpected : null;
1200      \core_completion\api::update_completion_date_event($cm->id, $cm->modname, $instance, $completionexpected);
1201  }
1202  
1203  /**
1204   * Moves a section within a course, from a position to another.
1205   * Be very careful: $section and $destination refer to section number,
1206   * not id!.
1207   *
1208   * @param object $course
1209   * @param int $section Section number (not id!!!)
1210   * @param int $destination
1211   * @param bool $ignorenumsections
1212   * @return boolean Result
1213   */
1214  function move_section_to($course, $section, $destination, $ignorenumsections = false) {
1215  /// Moves a whole course section up and down within the course
1216      global $USER, $DB;
1217  
1218      if (!$destination && $destination != 0) {
1219          return true;
1220      }
1221  
1222      // compartibility with course formats using field 'numsections'
1223      $courseformatoptions = course_get_format($course)->get_format_options();
1224      if ((!$ignorenumsections && array_key_exists('numsections', $courseformatoptions) &&
1225              ($destination > $courseformatoptions['numsections'])) || ($destination < 1)) {
1226          return false;
1227      }
1228  
1229      // Get all sections for this course and re-order them (2 of them should now share the same section number)
1230      if (!$sections = $DB->get_records_menu('course_sections', array('course' => $course->id),
1231              'section ASC, id ASC', 'id, section')) {
1232          return false;
1233      }
1234  
1235      $movedsections = reorder_sections($sections, $section, $destination);
1236  
1237      // Update all sections. Do this in 2 steps to avoid breaking database
1238      // uniqueness constraint
1239      $transaction = $DB->start_delegated_transaction();
1240      foreach ($movedsections as $id => $position) {
1241          if ((int) $sections[$id] !== $position) {
1242              $DB->set_field('course_sections', 'section', -$position, ['id' => $id]);
1243              // Invalidate the section cache by given section id.
1244              course_modinfo::purge_course_section_cache_by_id($course->id, $id);
1245          }
1246      }
1247      foreach ($movedsections as $id => $position) {
1248          if ((int) $sections[$id] !== $position) {
1249              $DB->set_field('course_sections', 'section', $position, ['id' => $id]);
1250              // Invalidate the section cache by given section id.
1251              course_modinfo::purge_course_section_cache_by_id($course->id, $id);
1252          }
1253      }
1254  
1255      // If we move the highlighted section itself, then just highlight the destination.
1256      // Adjust the higlighted section location if we move something over it either direction.
1257      if ($section == $course->marker) {
1258          course_set_marker($course->id, $destination);
1259      } else if ($section > $course->marker && $course->marker >= $destination) {
1260          course_set_marker($course->id, $course->marker+1);
1261      } else if ($section < $course->marker && $course->marker <= $destination) {
1262          course_set_marker($course->id, $course->marker-1);
1263      }
1264  
1265      $transaction->allow_commit();
1266      rebuild_course_cache($course->id, true, true);
1267      return true;
1268  }
1269  
1270  /**
1271   * This method will delete a course section and may delete all modules inside it.
1272   *
1273   * No permissions are checked here, use {@link course_can_delete_section()} to
1274   * check if section can actually be deleted.
1275   *
1276   * @param int|stdClass $course
1277   * @param int|stdClass|section_info $section
1278   * @param bool $forcedeleteifnotempty if set to false section will not be deleted if it has modules in it.
1279   * @param bool $async whether or not to try to delete the section using an adhoc task. Async also depends on a plugin hook.
1280   * @return bool whether section was deleted
1281   */
1282  function course_delete_section($course, $section, $forcedeleteifnotempty = true, $async = false) {
1283      global $DB;
1284  
1285      // Prepare variables.
1286      $courseid = (is_object($course)) ? $course->id : (int)$course;
1287      $sectionnum = (is_object($section)) ? $section->section : (int)$section;
1288      $section = $DB->get_record('course_sections', array('course' => $courseid, 'section' => $sectionnum));
1289      if (!$section) {
1290          // No section exists, can't proceed.
1291          return false;
1292      }
1293  
1294      // Check the 'course_module_background_deletion_recommended' hook first.
1295      // Only use asynchronous deletion if at least one plugin returns true and if async deletion has been requested.
1296      // Both are checked because plugins should not be allowed to dictate the deletion behaviour, only support/decline it.
1297      // It's up to plugins to handle things like whether or not they are enabled.
1298      if ($async && $pluginsfunction = get_plugins_with_function('course_module_background_deletion_recommended')) {
1299          foreach ($pluginsfunction as $plugintype => $plugins) {
1300              foreach ($plugins as $pluginfunction) {
1301                  if ($pluginfunction()) {
1302                      return course_delete_section_async($section, $forcedeleteifnotempty);
1303                  }
1304              }
1305          }
1306      }
1307  
1308      $format = course_get_format($course);
1309      $sectionname = $format->get_section_name($section);
1310  
1311      // Delete section.
1312      $result = $format->delete_section($section, $forcedeleteifnotempty);
1313  
1314      // Trigger an event for course section deletion.
1315      if ($result) {
1316          $context = context_course::instance($courseid);
1317          $event = \core\event\course_section_deleted::create(
1318              array(
1319                  'objectid' => $section->id,
1320                  'courseid' => $courseid,
1321                  'context' => $context,
1322                  'other' => array(
1323                      'sectionnum' => $section->section,
1324                      'sectionname' => $sectionname,
1325                  )
1326              )
1327          );
1328          $event->add_record_snapshot('course_sections', $section);
1329          $event->trigger();
1330      }
1331      return $result;
1332  }
1333  
1334  /**
1335   * Course section deletion, using an adhoc task for deletion of the modules it contains.
1336   * 1. Schedule all modules within the section for adhoc removal.
1337   * 2. Move all modules to course section 0.
1338   * 3. Delete the resulting empty section.
1339   *
1340   * @param \stdClass $section the section to schedule for deletion.
1341   * @param bool $forcedeleteifnotempty whether to force section deletion if it contains modules.
1342   * @return bool true if the section was scheduled for deletion, false otherwise.
1343   */
1344  function course_delete_section_async($section, $forcedeleteifnotempty = true) {
1345      global $DB, $USER;
1346  
1347      // Objects only, and only valid ones.
1348      if (!is_object($section) || empty($section->id)) {
1349          return false;
1350      }
1351  
1352      // Does the object currently exist in the DB for removal (check for stale objects).
1353      $section = $DB->get_record('course_sections', array('id' => $section->id));
1354      if (!$section || !$section->section) {
1355          // No section exists, or the section is 0. Can't proceed.
1356          return false;
1357      }
1358  
1359      // Check whether the section can be removed.
1360      if (!$forcedeleteifnotempty && (!empty($section->sequence) || !empty($section->summary))) {
1361          return false;
1362      }
1363  
1364      $format = course_get_format($section->course);
1365      $sectionname = $format->get_section_name($section);
1366  
1367      // Flag those modules having no existing deletion flag. Some modules may have been scheduled for deletion manually, and we don't
1368      // want to create additional adhoc deletion tasks for these. Moving them to section 0 will suffice.
1369      $affectedmods = $DB->get_records_select('course_modules', 'course = ? AND section = ? AND deletioninprogress <> ?',
1370                                              [$section->course, $section->id, 1], '', 'id');
1371      $DB->set_field('course_modules', 'deletioninprogress', '1', ['course' => $section->course, 'section' => $section->id]);
1372  
1373      // Move all modules to section 0.
1374      $modules = $DB->get_records('course_modules', ['section' => $section->id], '');
1375      $sectionzero = $DB->get_record('course_sections', ['course' => $section->course, 'section' => '0']);
1376      foreach ($modules as $mod) {
1377          moveto_module($mod, $sectionzero);
1378      }
1379  
1380      // Create and queue an adhoc task for the deletion of the modules.
1381      $removaltask = new \core_course\task\course_delete_modules();
1382      $data = array(
1383          'cms' => $affectedmods,
1384          'userid' => $USER->id,
1385          'realuserid' => \core\session\manager::get_realuser()->id
1386      );
1387      $removaltask->set_custom_data($data);
1388      \core\task\manager::queue_adhoc_task($removaltask);
1389  
1390      // Delete the now empty section, passing in only the section number, which forces the function to fetch a new object.
1391      // The refresh is needed because the section->sequence is now stale.
1392      $result = $format->delete_section($section->section, $forcedeleteifnotempty);
1393  
1394      // Trigger an event for course section deletion.
1395      if ($result) {
1396          $context = \context_course::instance($section->course);
1397          $event = \core\event\course_section_deleted::create(
1398              array(
1399                  'objectid' => $section->id,
1400                  'courseid' => $section->course,
1401                  'context' => $context,
1402                  'other' => array(
1403                      'sectionnum' => $section->section,
1404                      'sectionname' => $sectionname,
1405                  )
1406              )
1407          );
1408          $event->add_record_snapshot('course_sections', $section);
1409          $event->trigger();
1410      }
1411      rebuild_course_cache($section->course, true);
1412  
1413      return $result;
1414  }
1415  
1416  /**
1417   * Updates the course section
1418   *
1419   * This function does not check permissions or clean values - this has to be done prior to calling it.
1420   *
1421   * @param int|stdClass $course
1422   * @param stdClass $section record from course_sections table - it will be updated with the new values
1423   * @param array|stdClass $data
1424   */
1425  function course_update_section($course, $section, $data) {
1426      global $DB;
1427  
1428      $courseid = (is_object($course)) ? $course->id : (int)$course;
1429  
1430      // Some fields can not be updated using this method.
1431      $data = array_diff_key((array)$data, array('id', 'course', 'section', 'sequence'));
1432      $changevisibility = (array_key_exists('visible', $data) && (bool)$data['visible'] != (bool)$section->visible);
1433      if (array_key_exists('name', $data) && \core_text::strlen($data['name']) > 255) {
1434          throw new moodle_exception('maximumchars', 'moodle', '', 255);
1435      }
1436  
1437      // Update record in the DB and course format options.
1438      $data['id'] = $section->id;
1439      $data['timemodified'] = time();
1440      $DB->update_record('course_sections', $data);
1441      // Invalidate the section cache by given section id.
1442      course_modinfo::purge_course_section_cache_by_id($courseid, $section->id);
1443      rebuild_course_cache($courseid, false, true);
1444      course_get_format($courseid)->update_section_format_options($data);
1445  
1446      // Update fields of the $section object.
1447      foreach ($data as $key => $value) {
1448          if (property_exists($section, $key)) {
1449              $section->$key = $value;
1450          }
1451      }
1452  
1453      // Trigger an event for course section update.
1454      $event = \core\event\course_section_updated::create(
1455          array(
1456              'objectid' => $section->id,
1457              'courseid' => $courseid,
1458              'context' => context_course::instance($courseid),
1459              'other' => array('sectionnum' => $section->section)
1460          )
1461      );
1462      $event->trigger();
1463  
1464      // If section visibility was changed, hide the modules in this section too.
1465      if ($changevisibility && !empty($section->sequence)) {
1466          $modules = explode(',', $section->sequence);
1467          $cmids = [];
1468          foreach ($modules as $moduleid) {
1469              if ($cm = get_coursemodule_from_id(null, $moduleid, $courseid)) {
1470                  $cmids[] = $cm->id;
1471                  if ($data['visible']) {
1472                      // As we unhide the section, we use the previously saved visibility stored in visibleold.
1473                      set_coursemodule_visible($moduleid, $cm->visibleold, $cm->visibleoncoursepage, false);
1474                  } else {
1475                      // We hide the section, so we hide the module but we store the original state in visibleold.
1476                      set_coursemodule_visible($moduleid, 0, $cm->visibleoncoursepage, false);
1477                      $DB->set_field('course_modules', 'visibleold', $cm->visible, ['id' => $moduleid]);
1478                  }
1479                  \core\event\course_module_updated::create_from_cm($cm)->trigger();
1480              }
1481          }
1482          \course_modinfo::purge_course_modules_cache($courseid, $cmids);
1483          rebuild_course_cache($courseid, false, true);
1484      }
1485  }
1486  
1487  /**
1488   * Checks if the current user can delete a section (if course format allows it and user has proper permissions).
1489   *
1490   * @param int|stdClass $course
1491   * @param int|stdClass|section_info $section
1492   * @return bool
1493   */
1494  function course_can_delete_section($course, $section) {
1495      if (is_object($section)) {
1496          $section = $section->section;
1497      }
1498      if (!$section) {
1499          // Not possible to delete 0-section.
1500          return false;
1501      }
1502      // Course format should allow to delete sections.
1503      if (!course_get_format($course)->can_delete_section($section)) {
1504          return false;
1505      }
1506      // Make sure user has capability to update course and move sections.
1507      $context = context_course::instance(is_object($course) ? $course->id : $course);
1508      if (!has_all_capabilities(array('moodle/course:movesections', 'moodle/course:update'), $context)) {
1509          return false;
1510      }
1511      // Make sure user has capability to delete each activity in this section.
1512      $modinfo = get_fast_modinfo($course);
1513      if (!empty($modinfo->sections[$section])) {
1514          foreach ($modinfo->sections[$section] as $cmid) {
1515              if (!has_capability('moodle/course:manageactivities', context_module::instance($cmid))) {
1516                  return false;
1517              }
1518          }
1519      }
1520      return true;
1521  }
1522  
1523  /**
1524   * Reordering algorithm for course sections. Given an array of section->section indexed by section->id,
1525   * an original position number and a target position number, rebuilds the array so that the
1526   * move is made without any duplication of section positions.
1527   * Note: The target_position is the position AFTER WHICH the moved section will be inserted. If you want to
1528   * insert a section before the first one, you must give 0 as the target (section 0 can never be moved).
1529   *
1530   * @param array $sections
1531   * @param int $origin_position
1532   * @param int $target_position
1533   * @return array
1534   */
1535  function reorder_sections($sections, $origin_position, $target_position) {
1536      if (!is_array($sections)) {
1537          return false;
1538      }
1539  
1540      // We can't move section position 0
1541      if ($origin_position < 1) {
1542          echo "We can't move section position 0";
1543          return false;
1544      }
1545  
1546      // Locate origin section in sections array
1547      if (!$origin_key = array_search($origin_position, $sections)) {
1548          echo "searched position not in sections array";
1549          return false; // searched position not in sections array
1550      }
1551  
1552      // Extract origin section
1553      $origin_section = $sections[$origin_key];
1554      unset($sections[$origin_key]);
1555  
1556      // Find offset of target position (stupid PHP's array_splice requires offset instead of key index!)
1557      $found = false;
1558      $append_array = array();
1559      foreach ($sections as $id => $position) {
1560          if ($found) {
1561              $append_array[$id] = $position;
1562              unset($sections[$id]);
1563          }
1564          if ($position == $target_position) {
1565              if ($target_position < $origin_position) {
1566                  $append_array[$id] = $position;
1567                  unset($sections[$id]);
1568              }
1569              $found = true;
1570          }
1571      }
1572  
1573      // Append moved section
1574      $sections[$origin_key] = $origin_section;
1575  
1576      // Append rest of array (if applicable)
1577      if (!empty($append_array)) {
1578          foreach ($append_array as $id => $position) {
1579              $sections[$id] = $position;
1580          }
1581      }
1582  
1583      // Renumber positions
1584      $position = 0;
1585      foreach ($sections as $id => $p) {
1586          $sections[$id] = $position;
1587          $position++;
1588      }
1589  
1590      return $sections;
1591  
1592  }
1593  
1594  /**
1595   * Move the module object $mod to the specified $section
1596   * If $beforemod exists then that is the module
1597   * before which $modid should be inserted
1598   *
1599   * @param stdClass|cm_info $mod
1600   * @param stdClass|section_info $section
1601   * @param int|stdClass $beforemod id or object with field id corresponding to the module
1602   *     before which the module needs to be included. Null for inserting in the
1603   *     end of the section
1604   * @return int new value for module visibility (0 or 1)
1605   */
1606  function moveto_module($mod, $section, $beforemod=NULL) {
1607      global $OUTPUT, $DB;
1608  
1609      // Current module visibility state - return value of this function.
1610      $modvisible = $mod->visible;
1611  
1612      // Remove original module from original section.
1613      if (! delete_mod_from_section($mod->id, $mod->section)) {
1614          echo $OUTPUT->notification("Could not delete module from existing section");
1615      }
1616  
1617      // Add the module into the new section.
1618      course_add_cm_to_section($section->course, $mod->id, $section->section, $beforemod);
1619  
1620      // If moving to a hidden section then hide module.
1621      if ($mod->section != $section->id) {
1622          if (!$section->visible && $mod->visible) {
1623              // Module was visible but must become hidden after moving to hidden section.
1624              $modvisible = 0;
1625              set_coursemodule_visible($mod->id, 0);
1626              // Set visibleold to 1 so module will be visible when section is made visible.
1627              $DB->set_field('course_modules', 'visibleold', 1, array('id' => $mod->id));
1628          }
1629          if ($section->visible && !$mod->visible) {
1630              // Hidden module was moved to the visible section, restore the module visibility from visibleold.
1631              set_coursemodule_visible($mod->id, $mod->visibleold);
1632              $modvisible = $mod->visibleold;
1633          }
1634      }
1635  
1636      return $modvisible;
1637  }
1638  
1639  /**
1640   * Returns the list of all editing actions that current user can perform on the module
1641   *
1642   * @param cm_info $mod The module to produce editing buttons for
1643   * @param int $indent The current indenting (default -1 means no move left-right actions)
1644   * @param int $sr The section to link back to (used for creating the links)
1645   * @return array array of action_link or pix_icon objects
1646   */
1647  function course_get_cm_edit_actions(cm_info $mod, $indent = -1, $sr = null) {
1648      global $COURSE, $SITE, $CFG;
1649  
1650      static $str;
1651  
1652      $coursecontext = context_course::instance($mod->course);
1653      $modcontext = context_module::instance($mod->id);
1654      $courseformat = course_get_format($mod->get_course());
1655      $usecomponents = $courseformat->supports_components();
1656  
1657      $editcaps = array('moodle/course:manageactivities', 'moodle/course:activityvisibility', 'moodle/role:assign');
1658      $dupecaps = array('moodle/backup:backuptargetimport', 'moodle/restore:restoretargetimport');
1659  
1660      // No permission to edit anything.
1661      if (!has_any_capability($editcaps, $modcontext) and !has_all_capabilities($dupecaps, $coursecontext)) {
1662          return array();
1663      }
1664  
1665      $hasmanageactivities = has_capability('moodle/course:manageactivities', $modcontext);
1666  
1667      if (!isset($str)) {
1668          $str = get_strings(array('delete', 'move', 'moveright', 'moveleft',
1669              'editsettings', 'duplicate', 'modhide', 'makeavailable', 'makeunavailable', 'modshow'), 'moodle');
1670          $str->assign         = get_string('assignroles', 'role');
1671          $str->groupsnone     = get_string('clicktochangeinbrackets', 'moodle', get_string("groupsnone"));
1672          $str->groupsseparate = get_string('clicktochangeinbrackets', 'moodle', get_string("groupsseparate"));
1673          $str->groupsvisible  = get_string('clicktochangeinbrackets', 'moodle', get_string("groupsvisible"));
1674      }
1675  
1676      $baseurl = new moodle_url('/course/mod.php', array('sesskey' => sesskey()));
1677  
1678      if ($sr !== null) {
1679          $baseurl->param('sr', $sr);
1680      }
1681      $actions = array();
1682  
1683      // Update.
1684      if ($hasmanageactivities) {
1685          $actions['update'] = new action_menu_link_secondary(
1686              new moodle_url($baseurl, array('update' => $mod->id)),
1687              new pix_icon('t/edit', '', 'moodle', array('class' => 'iconsmall')),
1688              $str->editsettings,
1689              array('class' => 'editing_update', 'data-action' => 'update')
1690          );
1691      }
1692  
1693      // Move (only for component compatible formats).
1694      if ($usecomponents) {
1695          $actions['move'] = new action_menu_link_secondary(
1696              new moodle_url($baseurl, [
1697                  'sesskey' => sesskey(),
1698                  'copy' => $mod->id,
1699              ]),
1700              new pix_icon('i/dragdrop', '', 'moodle', ['class' => 'iconsmall']),
1701              $str->move,
1702              [
1703                  'class' => 'editing_movecm',
1704                  'data-action' => 'moveCm',
1705                  'data-id' => $mod->id,
1706              ]
1707          );
1708      }
1709  
1710      // Indent.
1711      if ($hasmanageactivities && $indent >= 0) {
1712          $indentlimits = new stdClass();
1713          $indentlimits->min = 0;
1714          // Legacy indentation could continue using a limit of 16,
1715          // but components based formats will be forced to use one level indentation only.
1716          $indentlimits->max = ($usecomponents) ? 1 : 16;
1717          if (right_to_left()) {   // Exchange arrows on RTL
1718              $rightarrow = 't/left';
1719              $leftarrow  = 't/right';
1720          } else {
1721              $rightarrow = 't/right';
1722              $leftarrow  = 't/left';
1723          }
1724  
1725          if ($indent >= $indentlimits->max) {
1726              $enabledclass = 'hidden';
1727          } else {
1728              $enabledclass = '';
1729          }
1730          $actions['moveright'] = new action_menu_link_secondary(
1731              new moodle_url($baseurl, ['id' => $mod->id, 'indent' => '1']),
1732              new pix_icon($rightarrow, '', 'moodle', ['class' => 'iconsmall']),
1733              $str->moveright,
1734              [
1735                  'class' => 'editing_moveright ' . $enabledclass,
1736                  'data-action' => ($usecomponents) ? 'cmMoveRight' : 'moveright',
1737                  'data-keepopen' => true,
1738                  'data-sectionreturn' => $sr,
1739                  'data-id' => $mod->id,
1740              ]
1741          );
1742  
1743          if ($indent <= $indentlimits->min) {
1744              $enabledclass = 'hidden';
1745          } else {
1746              $enabledclass = '';
1747          }
1748          $actions['moveleft'] = new action_menu_link_secondary(
1749              new moodle_url($baseurl, ['id' => $mod->id, 'indent' => '0']),
1750              new pix_icon($leftarrow, '', 'moodle', ['class' => 'iconsmall']),
1751              $str->moveleft,
1752              [
1753                  'class' => 'editing_moveleft ' . $enabledclass,
1754                  'data-action' => ($usecomponents) ? 'cmMoveLeft' : 'moveleft',
1755                  'data-keepopen' => true,
1756                  'data-sectionreturn' => $sr,
1757                  'data-id' => $mod->id,
1758              ]
1759          );
1760  
1761      }
1762  
1763      // Hide/Show/Available/Unavailable.
1764      if (has_capability('moodle/course:activityvisibility', $modcontext)) {
1765          $allowstealth = !empty($CFG->allowstealth) && $courseformat->allow_stealth_module_visibility($mod, $mod->get_section_info());
1766  
1767          $sectionvisible = $mod->get_section_info()->visible;
1768          // The module on the course page may be in one of the following states:
1769          // - Available and displayed on the course page ($displayedoncoursepage);
1770          // - Not available and not displayed on the course page ($unavailable);
1771          // - Available but not displayed on the course page ($stealth) - this can also be a visible activity in a hidden section.
1772          $displayedoncoursepage = $mod->visible && $mod->visibleoncoursepage && $sectionvisible;
1773          $unavailable = !$mod->visible;
1774          $stealth = $mod->visible && (!$mod->visibleoncoursepage || !$sectionvisible);
1775          if ($displayedoncoursepage) {
1776              $actions['hide'] = new action_menu_link_secondary(
1777                  new moodle_url($baseurl, array('hide' => $mod->id)),
1778                  new pix_icon('t/hide', '', 'moodle', array('class' => 'iconsmall')),
1779                  $str->modhide,
1780                  [
1781                      'class' => 'editing_hide',
1782                      'data-action' => ($usecomponents) ? 'cmHide' : 'hide',
1783                      'data-id' => $mod->id,
1784                  ]
1785              );
1786          } else if (!$displayedoncoursepage && $sectionvisible) {
1787              // Offer to "show" only if the section is visible.
1788              $actions['show'] = new action_menu_link_secondary(
1789                  new moodle_url($baseurl, array('show' => $mod->id)),
1790                  new pix_icon('t/show', '', 'moodle', array('class' => 'iconsmall')),
1791                  $str->modshow,
1792                  [
1793                      'class' => 'editing_show',
1794                      'data-action' => ($usecomponents) ? 'cmShow' : 'show',
1795                      'data-id' => $mod->id,
1796                  ]
1797              );
1798          }
1799  
1800          if ($stealth) {
1801              // When making the "stealth" module unavailable we perform the same action as hiding the visible module.
1802              $actions['hide'] = new action_menu_link_secondary(
1803                  new moodle_url($baseurl, array('hide' => $mod->id)),
1804                  new pix_icon('t/unblock', '', 'moodle', array('class' => 'iconsmall')),
1805                  $str->makeunavailable,
1806                  [
1807                      'class' => 'editing_makeunavailable',
1808                      'data-action' => ($usecomponents) ? 'cmHide' : 'hide',
1809                      'data-sectionreturn' => $sr,
1810                      'data-id' => $mod->id,
1811                  ]
1812              );
1813          } else if ($unavailable && (!$sectionvisible || $allowstealth) && $mod->has_view()) {
1814              // Allow to make visually hidden module available in gradebook and other reports by making it a "stealth" module.
1815              // When the section is hidden it is an equivalent of "showing" the module.
1816              // Activities without the link (i.e. labels) can not be made available but hidden on course page.
1817              $action = $sectionvisible ? 'stealth' : 'show';
1818              if ($usecomponents) {
1819                  $action = 'cm' . ucfirst($action);
1820              }
1821              $actions[$action] = new action_menu_link_secondary(
1822                  new moodle_url($baseurl, array('stealth' => $mod->id)),
1823                  new pix_icon('t/block', '', 'moodle', array('class' => 'iconsmall')),
1824                  $str->makeavailable,
1825                  [
1826                      'class' => 'editing_makeavailable',
1827                      'data-action' => $action,
1828                      'data-sectionreturn' => $sr,
1829                      'data-id' => $mod->id,
1830                  ]
1831              );
1832          }
1833      }
1834  
1835      // Duplicate (require both target import caps to be able to duplicate and backup2 support, see modduplicate.php)
1836      if (has_all_capabilities($dupecaps, $coursecontext) &&
1837              plugin_supports('mod', $mod->modname, FEATURE_BACKUP_MOODLE2) &&
1838              course_allowed_module($mod->get_course(), $mod->modname)) {
1839          $actions['duplicate'] = new action_menu_link_secondary(
1840              new moodle_url($baseurl, array('duplicate' => $mod->id)),
1841              new pix_icon('t/copy', '', 'moodle', array('class' => 'iconsmall')),
1842              $str->duplicate,
1843              array('class' => 'editing_duplicate', 'data-action' => 'duplicate', 'data-sectionreturn' => $sr)
1844          );
1845      }
1846  
1847      // Assign.
1848      if (has_capability('moodle/role:assign', $modcontext)){
1849          $actions['assign'] = new action_menu_link_secondary(
1850              new moodle_url('/admin/roles/assign.php', array('contextid' => $modcontext->id)),
1851              new pix_icon('t/assignroles', '', 'moodle', array('class' => 'iconsmall')),
1852              $str->assign,
1853              array('class' => 'editing_assign', 'data-action' => 'assignroles', 'data-sectionreturn' => $sr)
1854          );
1855      }
1856  
1857      // Delete.
1858      if ($hasmanageactivities) {
1859          $actions['delete'] = new action_menu_link_secondary(
1860              new moodle_url($baseurl, array('delete' => $mod->id)),
1861              new pix_icon('t/delete', '', 'moodle', array('class' => 'iconsmall')),
1862              $str->delete,
1863              array('class' => 'editing_delete', 'data-action' => 'delete', 'data-sectionreturn' => $sr)
1864          );
1865      }
1866  
1867      return $actions;
1868  }
1869  
1870  /**
1871   * Returns the move action.
1872   *
1873   * @param cm_info $mod The module to produce a move button for
1874   * @param int $sr The section to link back to (used for creating the links)
1875   * @return The markup for the move action, or an empty string if not available.
1876   */
1877  function course_get_cm_move(cm_info $mod, $sr = null) {
1878      global $OUTPUT;
1879  
1880      static $str;
1881      static $baseurl;
1882  
1883      $modcontext = context_module::instance($mod->id);
1884      $hasmanageactivities = has_capability('moodle/course:manageactivities', $modcontext);
1885  
1886      if (!isset($str)) {
1887          $str = get_strings(array('move'));
1888      }
1889  
1890      if (!isset($baseurl)) {
1891          $baseurl = new moodle_url('/course/mod.php', array('sesskey' => sesskey()));
1892  
1893          if ($sr !== null) {
1894              $baseurl->param('sr', $sr);
1895          }
1896      }
1897  
1898      if ($hasmanageactivities) {
1899          $pixicon = 'i/dragdrop';
1900  
1901          if (!course_ajax_enabled($mod->get_course())) {
1902              // Override for course frontpage until we get drag/drop working there.
1903              $pixicon = 't/move';
1904          }
1905  
1906          $attributes = [
1907              'class' => 'editing_move',
1908              'data-action' => 'move',
1909              'data-sectionreturn' => $sr,
1910              'title' => $str->move,
1911              'aria-label' => $str->move,
1912          ];
1913          return html_writer::link(
1914              new moodle_url($baseurl, ['copy' => $mod->id]),
1915              $OUTPUT->pix_icon($pixicon, '', 'moodle', ['class' => 'iconsmall']),
1916              $attributes
1917          );
1918      }
1919      return '';
1920  }
1921  
1922  /**
1923   * given a course object with shortname & fullname, this function will
1924   * truncate the the number of chars allowed and add ... if it was too long
1925   */
1926  function course_format_name ($course,$max=100) {
1927  
1928      $context = context_course::instance($course->id);
1929      $shortname = format_string($course->shortname, true, array('context' => $context));
1930      $fullname = format_string($course->fullname, true, array('context' => context_course::instance($course->id)));
1931      $str = $shortname.': '. $fullname;
1932      if (core_text::strlen($str) <= $max) {
1933          return $str;
1934      }
1935      else {
1936          return core_text::substr($str,0,$max-3).'...';
1937      }
1938  }
1939  
1940  /**
1941   * Is the user allowed to add this type of module to this course?
1942   * @param object $course the course settings. Only $course->id is used.
1943   * @param string $modname the module name. E.g. 'forum' or 'quiz'.
1944   * @param \stdClass $user the user to check, defaults to the global user if not provided.
1945   * @return bool whether the current user is allowed to add this type of module to this course.
1946   */
1947  function course_allowed_module($course, $modname, \stdClass $user = null) {
1948      global $USER;
1949      $user = $user ?? $USER;
1950      if (is_numeric($modname)) {
1951          throw new coding_exception('Function course_allowed_module no longer
1952                  supports numeric module ids. Please update your code to pass the module name.');
1953      }
1954  
1955      $capability = 'mod/' . $modname . ':addinstance';
1956      if (!get_capability_info($capability)) {
1957          // Debug warning that the capability does not exist, but no more than once per page.
1958          static $warned = array();
1959          $archetype = plugin_supports('mod', $modname, FEATURE_MOD_ARCHETYPE, MOD_ARCHETYPE_OTHER);
1960          if (!isset($warned[$modname]) && $archetype !== MOD_ARCHETYPE_SYSTEM) {
1961              debugging('The module ' . $modname . ' does not define the standard capability ' .
1962                      $capability , DEBUG_DEVELOPER);
1963              $warned[$modname] = 1;
1964          }
1965  
1966          // If the capability does not exist, the module can always be added.
1967          return true;
1968      }
1969  
1970      $coursecontext = context_course::instance($course->id);
1971      return has_capability($capability, $coursecontext, $user);
1972  }
1973  
1974  /**
1975   * Efficiently moves many courses around while maintaining
1976   * sortorder in order.
1977   *
1978   * @param array $courseids is an array of course ids
1979   * @param int $categoryid
1980   * @return bool success
1981   */
1982  function move_courses($courseids, $categoryid) {
1983      global $DB;
1984  
1985      if (empty($courseids)) {
1986          // Nothing to do.
1987          return false;
1988      }
1989  
1990      if (!$category = $DB->get_record('course_categories', array('id' => $categoryid))) {
1991          return false;
1992      }
1993  
1994      $courseids = array_reverse($courseids);
1995      $newparent = context_coursecat::instance($category->id);
1996      $i = 1;
1997  
1998      list($where, $params) = $DB->get_in_or_equal($courseids);
1999      $dbcourses = $DB->get_records_select('course', 'id ' . $where, $params, '', 'id, category, shortname, fullname');
2000      foreach ($dbcourses as $dbcourse) {
2001          $course = new stdClass();
2002          $course->id = $dbcourse->id;
2003          $course->timemodified = time();
2004          $course->category  = $category->id;
2005          $course->sortorder = $category->sortorder + get_max_courses_in_category() - $i++;
2006          if ($category->visible == 0) {
2007              // Hide the course when moving into hidden category, do not update the visibleold flag - we want to get
2008              // to previous state if somebody unhides the category.
2009              $course->visible = 0;
2010          }
2011  
2012          $DB->update_record('course', $course);
2013  
2014          // Update context, so it can be passed to event.
2015          $context = context_course::instance($course->id);
2016          $context->update_moved($newparent);
2017  
2018          // Trigger a course updated event.
2019          $event = \core\event\course_updated::create(array(
2020              'objectid' => $course->id,
2021              'context' => context_course::instance($course->id),
2022              'other' => array('shortname' => $dbcourse->shortname,
2023                               'fullname' => $dbcourse->fullname,
2024                               'updatedfields' => array('category' => $category->id))
2025          ));
2026          $event->set_legacy_logdata(array($course->id, 'course', 'move', 'edit.php?id=' . $course->id, $course->id));
2027          $event->trigger();
2028      }
2029      fix_course_sortorder();
2030      cache_helper::purge_by_event('changesincourse');
2031  
2032      return true;
2033  }
2034  
2035  /**
2036   * Returns the display name of the given section that the course prefers
2037   *
2038   * Implementation of this function is provided by course format
2039   * @see core_courseformat\base::get_section_name()
2040   *
2041   * @param int|stdClass $courseorid The course to get the section name for (object or just course id)
2042   * @param int|stdClass $section Section object from database or just field course_sections.section
2043   * @return string Display name that the course format prefers, e.g. "Week 2"
2044   */
2045  function get_section_name($courseorid, $section) {
2046      return course_get_format($courseorid)->get_section_name($section);
2047  }
2048  
2049  /**
2050   * Tells if current course format uses sections
2051   *
2052   * @param string $format Course format ID e.g. 'weeks' $course->format
2053   * @return bool
2054   */
2055  function course_format_uses_sections($format) {
2056      $course = new stdClass();
2057      $course->format = $format;
2058      return course_get_format($course)->uses_sections();
2059  }
2060  
2061  /**
2062   * Returns the information about the ajax support in the given source format
2063   *
2064   * The returned object's property (boolean)capable indicates that
2065   * the course format supports Moodle course ajax features.
2066   *
2067   * @param string $format
2068   * @return stdClass
2069   */
2070  function course_format_ajax_support($format) {
2071      $course = new stdClass();
2072      $course->format = $format;
2073      return course_get_format($course)->supports_ajax();
2074  }
2075  
2076  /**
2077   * Can the current user delete this course?
2078   * Course creators have exception,
2079   * 1 day after the creation they can sill delete the course.
2080   * @param int $courseid
2081   * @return boolean
2082   */
2083  function can_delete_course($courseid) {
2084      global $USER;
2085  
2086      $context = context_course::instance($courseid);
2087  
2088      if (has_capability('moodle/course:delete', $context)) {
2089          return true;
2090      }
2091  
2092      // hack: now try to find out if creator created this course recently (1 day)
2093      if (!has_capability('moodle/course:create', $context)) {
2094          return false;
2095      }
2096  
2097      $since = time() - 60*60*24;
2098      $course = get_course($courseid);
2099  
2100      if ($course->timecreated < $since) {
2101          return false; // Return if the course was not created in last 24 hours.
2102      }
2103  
2104      $logmanger = get_log_manager();
2105      $readers = $logmanger->get_readers('\core\log\sql_reader');
2106      $reader = reset($readers);
2107  
2108      if (empty($reader)) {
2109          return false; // No log reader found.
2110      }
2111  
2112      // A proper reader.
2113      $select = "userid = :userid AND courseid = :courseid AND eventname = :eventname AND timecreated > :since";
2114      $params = array('userid' => $USER->id, 'since' => $since, 'courseid' => $course->id, 'eventname' => '\core\event\course_created');
2115  
2116      return (bool)$reader->get_events_select_count($select, $params);
2117  }
2118  
2119  /**
2120   * Save the Your name for 'Some role' strings.
2121   *
2122   * @param integer $courseid the id of this course.
2123   * @param array $data the data that came from the course settings form.
2124   */
2125  function save_local_role_names($courseid, $data) {
2126      global $DB;
2127      $context = context_course::instance($courseid);
2128  
2129      foreach ($data as $fieldname => $value) {
2130          if (strpos($fieldname, 'role_') !== 0) {
2131              continue;
2132          }
2133          list($ignored, $roleid) = explode('_', $fieldname);
2134  
2135          // make up our mind whether we want to delete, update or insert
2136          if (!$value) {
2137              $DB->delete_records('role_names', array('contextid' => $context->id, 'roleid' => $roleid));
2138  
2139          } else if ($rolename = $DB->get_record('role_names', array('contextid' => $context->id, 'roleid' => $roleid))) {
2140              $rolename->name = $value;
2141              $DB->update_record('role_names', $rolename);
2142  
2143          } else {
2144              $rolename = new stdClass;
2145              $rolename->contextid = $context->id;
2146              $rolename->roleid = $roleid;
2147              $rolename->name = $value;
2148              $DB->insert_record('role_names', $rolename);
2149          }
2150          // This will ensure the course contacts cache is purged..
2151          core_course_category::role_assignment_changed($roleid, $context);
2152      }
2153  }
2154  
2155  /**
2156   * Returns options to use in course overviewfiles filemanager
2157   *
2158   * @param null|stdClass|core_course_list_element|int $course either object that has 'id' property or just the course id;
2159   *     may be empty if course does not exist yet (course create form)
2160   * @return array|null array of options such as maxfiles, maxbytes, accepted_types, etc.
2161   *     or null if overviewfiles are disabled
2162   */
2163  function course_overviewfiles_options($course) {
2164      global $CFG;
2165      if (empty($CFG->courseoverviewfileslimit)) {
2166          return null;
2167      }
2168  
2169      // Create accepted file types based on config value, falling back to default all.
2170      $acceptedtypes = (new \core_form\filetypes_util)->normalize_file_types($CFG->courseoverviewfilesext);
2171      if (in_array('*', $acceptedtypes) || empty($acceptedtypes)) {
2172          $acceptedtypes = '*';
2173      }
2174  
2175      $options = array(
2176          'maxfiles' => $CFG->courseoverviewfileslimit,
2177          'maxbytes' => $CFG->maxbytes,
2178          'subdirs' => 0,
2179          'accepted_types' => $acceptedtypes
2180      );
2181      if (!empty($course->id)) {
2182          $options['context'] = context_course::instance($course->id);
2183      } else if (is_int($course) && $course > 0) {
2184          $options['context'] = context_course::instance($course);
2185      }
2186      return $options;
2187  }
2188  
2189  /**
2190   * Create a course and either return a $course object
2191   *
2192   * Please note this functions does not verify any access control,
2193   * the calling code is responsible for all validation (usually it is the form definition).
2194   *
2195   * @param array $editoroptions course description editor options
2196   * @param object $data  - all the data needed for an entry in the 'course' table
2197   * @return object new course instance
2198   */
2199  function create_course($data, $editoroptions = NULL) {
2200      global $DB, $CFG;
2201  
2202      //check the categoryid - must be given for all new courses
2203      $category = $DB->get_record('course_categories', array('id'=>$data->category), '*', MUST_EXIST);
2204  
2205      // Check if the shortname already exists.
2206      if (!empty($data->shortname)) {
2207          if ($DB->record_exists('course', array('shortname' => $data->shortname))) {
2208              throw new moodle_exception('shortnametaken', '', '', $data->shortname);
2209          }
2210      }
2211  
2212      // Check if the idnumber already exists.
2213      if (!empty($data->idnumber)) {
2214          if ($DB->record_exists('course', array('idnumber' => $data->idnumber))) {
2215              throw new moodle_exception('courseidnumbertaken', '', '', $data->idnumber);
2216          }
2217      }
2218  
2219      if (empty($CFG->enablecourserelativedates)) {
2220          // Make sure we're not setting the relative dates mode when the setting is disabled.
2221          unset($data->relativedatesmode);
2222      }
2223  
2224      if ($errorcode = course_validate_dates((array)$data)) {
2225          throw new moodle_exception($errorcode);
2226      }
2227  
2228      // Check if timecreated is given.
2229      $data->timecreated  = !empty($data->timecreated) ? $data->timecreated : time();
2230      $data->timemodified = $data->timecreated;
2231  
2232      // place at beginning of any category
2233      $data->sortorder = 0;
2234  
2235      if ($editoroptions) {
2236          // summary text is updated later, we need context to store the files first
2237          $data->summary = '';
2238          $data->summary_format = $data->summary_editor['format'];
2239      }
2240  
2241      // Get default completion settings as a fallback in case the enablecompletion field is not set.
2242      $courseconfig = get_config('moodlecourse');
2243      $defaultcompletion = !empty($CFG->enablecompletion) ? $courseconfig->enablecompletion : COMPLETION_DISABLED;
2244      $enablecompletion = $data->enablecompletion ?? $defaultcompletion;
2245      // Unset showcompletionconditions when completion tracking is not enabled for the course.
2246      if ($enablecompletion == COMPLETION_DISABLED) {
2247          unset($data->showcompletionconditions);
2248      } else if (!isset($data->showcompletionconditions)) {
2249          // Show completion conditions should have a default value when completion is enabled. Set it to the site defaults.
2250          // This scenario can happen when a course is created through data generators or through a web service.
2251          $data->showcompletionconditions = $courseconfig->showcompletionconditions;
2252      }
2253  
2254      if (!isset($data->visible)) {
2255          // data not from form, add missing visibility info
2256          $data->visible = $category->visible;
2257      }
2258      $data->visibleold = $data->visible;
2259  
2260      $newcourseid = $DB->insert_record('course', $data);
2261      $context = context_course::instance($newcourseid, MUST_EXIST);
2262  
2263      if ($editoroptions) {
2264          // Save the files used in the summary editor and store
2265          $data = file_postupdate_standard_editor($data, 'summary', $editoroptions, $context, 'course', 'summary', 0);
2266          $DB->set_field('course', 'summary', $data->summary, array('id'=>$newcourseid));
2267          $DB->set_field('course', 'summaryformat', $data->summary_format, array('id'=>$newcourseid));
2268      }
2269      if ($overviewfilesoptions = course_overviewfiles_options($newcourseid)) {
2270          // Save the course overviewfiles
2271          $data = file_postupdate_standard_filemanager($data, 'overviewfiles', $overviewfilesoptions, $context, 'course', 'overviewfiles', 0);
2272      }
2273  
2274      // update course format options
2275      course_get_format($newcourseid)->update_course_format_options($data);
2276  
2277      $course = course_get_format($newcourseid)->get_course();
2278  
2279      fix_course_sortorder();
2280      // purge appropriate caches in case fix_course_sortorder() did not change anything
2281      cache_helper::purge_by_event('changesincourse');
2282  
2283      // Trigger a course created event.
2284      $event = \core\event\course_created::create(array(
2285          'objectid' => $course->id,
2286          'context' => context_course::instance($course->id),
2287          'other' => array('shortname' => $course->shortname,
2288              'fullname' => $course->fullname)
2289      ));
2290  
2291      $event->trigger();
2292  
2293      // Setup the blocks
2294      blocks_add_default_course_blocks($course);
2295  
2296      // Create default section and initial sections if specified (unless they've already been created earlier).
2297      // We do not want to call course_create_sections_if_missing() because to avoid creating course cache.
2298      $numsections = isset($data->numsections) ? $data->numsections : 0;
2299      $existingsections = $DB->get_fieldset_sql('SELECT section from {course_sections} WHERE course = ?', [$newcourseid]);
2300      $newsections = array_diff(range(0, $numsections), $existingsections);
2301      foreach ($newsections as $sectionnum) {
2302          course_create_section($newcourseid, $sectionnum, true);
2303      }
2304  
2305      // Save any custom role names.
2306      save_local_role_names($course->id, (array)$data);
2307  
2308      // set up enrolments
2309      enrol_course_updated(true, $course, $data);
2310  
2311      // Update course tags.
2312      if (isset($data->tags)) {
2313          core_tag_tag::set_item_tags('core', 'course', $course->id, context_course::instance($course->id), $data->tags);
2314      }
2315  
2316      // Save custom fields if there are any of them in the form.
2317      $handler = core_course\customfield\course_handler::create();
2318      // Make sure to set the handler's parent context first.
2319      $coursecatcontext = context_coursecat::instance($category->id);
2320      $handler->set_parent_context($coursecatcontext);
2321      // Save the custom field data.
2322      $data->id = $course->id;
2323      $handler->instance_form_save($data, true);
2324  
2325      return $course;
2326  }
2327  
2328  /**
2329   * Update a course.
2330   *
2331   * Please note this functions does not verify any access control,
2332   * the calling code is responsible for all validation (usually it is the form definition).
2333   *
2334   * @param object $data  - all the data needed for an entry in the 'course' table
2335   * @param array $editoroptions course description editor options
2336   * @return void
2337   */
2338  function update_course($data, $editoroptions = NULL) {
2339      global $DB, $CFG;
2340  
2341      // Prevent changes on front page course.
2342      if ($data->id == SITEID) {
2343          throw new moodle_exception('invalidcourse', 'error');
2344      }
2345  
2346      $oldcourse = course_get_format($data->id)->get_course();
2347      $context   = context_course::instance($oldcourse->id);
2348  
2349      // Make sure we're not changing whatever the course's relativedatesmode setting is.
2350      unset($data->relativedatesmode);
2351  
2352      // Capture the updated fields for the log data.
2353      $updatedfields = [];
2354      foreach (get_object_vars($oldcourse) as $field => $value) {
2355          if ($field == 'summary_editor') {
2356              if (($data->$field)['text'] !== $value['text']) {
2357                  // The summary might be very long, we don't wan't to fill up the log record with the full text.
2358                  $updatedfields[$field] = '(updated)';
2359              }
2360          } else if ($field == 'tags' && isset($data->tags)) {
2361              // Tags might not have the same array keys, just check the values.
2362              if (array_values($data->$field) !== array_values($value)) {
2363                  $updatedfields[$field] = $data->$field;
2364              }
2365          } else {
2366              if (isset($data->$field) && $data->$field != $value) {
2367                  $updatedfields[$field] = $data->$field;
2368              }
2369          }
2370      }
2371  
2372      $data->timemodified = time();
2373  
2374      if ($editoroptions) {
2375          $data = file_postupdate_standard_editor($data, 'summary', $editoroptions, $context, 'course', 'summary', 0);
2376      }
2377      if ($overviewfilesoptions = course_overviewfiles_options($data->id)) {
2378          $data = file_postupdate_standard_filemanager($data, 'overviewfiles', $overviewfilesoptions, $context, 'course', 'overviewfiles', 0);
2379      }
2380  
2381      // Check we don't have a duplicate shortname.
2382      if (!empty($data->shortname) && $oldcourse->shortname != $data->shortname) {
2383          if ($DB->record_exists_sql('SELECT id from {course} WHERE shortname = ? AND id <> ?', array($data->shortname, $data->id))) {
2384              throw new moodle_exception('shortnametaken', '', '', $data->shortname);
2385          }
2386      }
2387  
2388      // Check we don't have a duplicate idnumber.
2389      if (!empty($data->idnumber) && $oldcourse->idnumber != $data->idnumber) {
2390          if ($DB->record_exists_sql('SELECT id from {course} WHERE idnumber = ? AND id <> ?', array($data->idnumber, $data->id))) {
2391              throw new moodle_exception('courseidnumbertaken', '', '', $data->idnumber);
2392          }
2393      }
2394  
2395      if ($errorcode = course_validate_dates((array)$data)) {
2396          throw new moodle_exception($errorcode);
2397      }
2398  
2399      if (!isset($data->category) or empty($data->category)) {
2400          // prevent nulls and 0 in category field
2401          unset($data->category);
2402      }
2403      $changesincoursecat = $movecat = (isset($data->category) and $oldcourse->category != $data->category);
2404  
2405      if (!isset($data->visible)) {
2406          // data not from form, add missing visibility info
2407          $data->visible = $oldcourse->visible;
2408      }
2409  
2410      if ($data->visible != $oldcourse->visible) {
2411          // reset the visibleold flag when manually hiding/unhiding course
2412          $data->visibleold = $data->visible;
2413          $changesincoursecat = true;
2414      } else {
2415          if ($movecat) {
2416              $newcategory = $DB->get_record('course_categories', array('id'=>$data->category));
2417              if (empty($newcategory->visible)) {
2418                  // make sure when moving into hidden category the course is hidden automatically
2419                  $data->visible = 0;
2420              }
2421          }
2422      }
2423  
2424      // Set newsitems to 0 if format does not support announcements.
2425      if (isset($data->format)) {
2426          $newcourseformat = course_get_format((object)['format' => $data->format]);
2427          if (!$newcourseformat->supports_news()) {
2428              $data->newsitems = 0;
2429          }
2430      }
2431  
2432      // Set showcompletionconditions to null when completion tracking has been disabled for the course.
2433      if (isset($data->enablecompletion) && $data->enablecompletion == COMPLETION_DISABLED) {
2434          $data->showcompletionconditions = null;
2435      }
2436  
2437      // Update custom fields if there are any of them in the form.
2438      $handler = core_course\customfield\course_handler::create();
2439      $handler->instance_form_save($data);
2440  
2441      // Update with the new data
2442      $DB->update_record('course', $data);
2443      // make sure the modinfo cache is reset
2444      rebuild_course_cache($data->id);
2445  
2446      // Purge course image cache in case if course image has been updated.
2447      \cache::make('core', 'course_image')->delete($data->id);
2448  
2449      // update course format options with full course data
2450      course_get_format($data->id)->update_course_format_options($data, $oldcourse);
2451  
2452      $course = $DB->get_record('course', array('id'=>$data->id));
2453  
2454      if ($movecat) {
2455          $newparent = context_coursecat::instance($course->category);
2456          $context->update_moved($newparent);
2457      }
2458      $fixcoursesortorder = $movecat || (isset($data->sortorder) && ($oldcourse->sortorder != $data->sortorder));
2459      if ($fixcoursesortorder) {
2460          fix_course_sortorder();
2461      }
2462  
2463      // purge appropriate caches in case fix_course_sortorder() did not change anything
2464      cache_helper::purge_by_event('changesincourse');
2465      if ($changesincoursecat) {
2466          cache_helper::purge_by_event('changesincoursecat');
2467      }
2468  
2469      // Test for and remove blocks which aren't appropriate anymore
2470      blocks_remove_inappropriate($course);
2471  
2472      // Save any custom role names.
2473      save_local_role_names($course->id, $data);
2474  
2475      // update enrol settings
2476      enrol_course_updated(false, $course, $data);
2477  
2478      // Update course tags.
2479      if (isset($data->tags)) {
2480          core_tag_tag::set_item_tags('core', 'course', $course->id, context_course::instance($course->id), $data->tags);
2481      }
2482  
2483      // Trigger a course updated event.
2484      $event = \core\event\course_updated::create(array(
2485          'objectid' => $course->id,
2486          'context' => context_course::instance($course->id),
2487          'other' => array('shortname' => $course->shortname,
2488                           'fullname' => $course->fullname,
2489                           'updatedfields' => $updatedfields)
2490      ));
2491  
2492      $event->set_legacy_logdata(array($course->id, 'course', 'update', 'edit.php?id=' . $course->id, $course->id));
2493      $event->trigger();
2494  
2495      if ($oldcourse->format !== $course->format) {
2496          // Remove all options stored for the previous format
2497          // We assume that new course format migrated everything it needed watching trigger
2498          // 'course_updated' and in method format_XXX::update_course_format_options()
2499          $DB->delete_records('course_format_options',
2500                  array('courseid' => $course->id, 'format' => $oldcourse->format));
2501      }
2502  }
2503  
2504  /**
2505   * Calculate the average number of enrolled participants per course.
2506   *
2507   * This is intended for statistics purposes during the site registration. Only visible courses are taken into account.
2508   * Front page enrolments are excluded.
2509   *
2510   * @param bool $onlyactive Consider only active enrolments in enabled plugins and obey the enrolment time restrictions.
2511   * @param int $lastloginsince If specified, count only users who logged in after this timestamp.
2512   * @return float
2513   */
2514  function average_number_of_participants(bool $onlyactive = false, int $lastloginsince = null): float {
2515      global $DB;
2516  
2517      $params = [];
2518  
2519      $sql = "SELECT DISTINCT ue.userid, e.courseid
2520                FROM {user_enrolments} ue
2521                JOIN {enrol} e ON e.id = ue.enrolid
2522                JOIN {course} c ON c.id = e.courseid ";
2523  
2524      if ($onlyactive || $lastloginsince) {
2525          $sql .= "JOIN {user} u ON u.id = ue.userid ";
2526      }
2527  
2528      $sql .= "WHERE e.courseid <> " . SITEID . " AND c.visible = 1 ";
2529  
2530      if ($onlyactive) {
2531          $sql .= "AND ue.status = :active
2532                   AND e.status = :enabled
2533                   AND ue.timestart < :now1
2534                   AND (ue.timeend = 0 OR ue.timeend > :now2) ";
2535  
2536          // Same as in the enrollib - the rounding should help caching in the database.
2537          $now = round(time(), -2);
2538  
2539          $params += [
2540              'active' => ENROL_USER_ACTIVE,
2541              'enabled' => ENROL_INSTANCE_ENABLED,
2542              'now1' => $now,
2543              'now2' => $now,
2544          ];
2545      }
2546  
2547      if ($lastloginsince) {
2548          $sql .= "AND u.lastlogin > :lastlogin ";
2549          $params['lastlogin'] = $lastloginsince;
2550      }
2551  
2552      $sql = "SELECT COUNT(*)
2553                FROM ($sql) total";
2554  
2555      $enrolmenttotal = $DB->count_records_sql($sql, $params);
2556  
2557      // Get the number of visible courses (exclude the front page).
2558      $coursetotal = $DB->count_records('course', ['visible' => 1]);
2559      $coursetotal = $coursetotal - 1;
2560  
2561      if (empty($coursetotal)) {
2562          $participantaverage = 0;
2563  
2564      } else {
2565          $participantaverage = $enrolmenttotal / $coursetotal;
2566      }
2567  
2568      return $participantaverage;
2569  }
2570  
2571  /**
2572   * Average number of course modules
2573   * @return integer
2574   */
2575  function average_number_of_courses_modules() {
2576      global $DB, $SITE;
2577  
2578      //count total of visible course module (except front page)
2579      $sql = 'SELECT COUNT(*) FROM (
2580          SELECT cm.course, cm.module
2581          FROM {course} c, {course_modules} cm
2582          WHERE c.id = cm.course
2583              AND c.id <> :siteid
2584              AND cm.visible = 1
2585              AND c.visible = 1) total';
2586      $params = array('siteid' => $SITE->id);
2587      $moduletotal = $DB->count_records_sql($sql, $params);
2588  
2589  
2590      //count total of visible courses (minus front page)
2591      $coursetotal = $DB->count_records('course', array('visible' => 1));
2592      $coursetotal = $coursetotal - 1 ;
2593  
2594      //average of course module
2595      if (empty($coursetotal)) {
2596          $coursemoduleaverage = 0;
2597      } else {
2598          $coursemoduleaverage = $moduletotal / $coursetotal;
2599      }
2600  
2601      return $coursemoduleaverage;
2602  }
2603  
2604  /**
2605   * This class pertains to course requests and contains methods associated with
2606   * create, approving, and removing course requests.
2607   *
2608   * Please note we do not allow embedded images here because there is no context
2609   * to store them with proper access control.
2610   *
2611   * @copyright 2009 Sam Hemelryk
2612   * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
2613   * @since Moodle 2.0
2614   *
2615   * @property-read int $id
2616   * @property-read string $fullname
2617   * @property-read string $shortname
2618   * @property-read string $summary
2619   * @property-read int $summaryformat
2620   * @property-read int $summarytrust
2621   * @property-read string $reason
2622   * @property-read int $requester
2623   */
2624  class course_request {
2625  
2626      /**
2627       * This is the stdClass that stores the properties for the course request
2628       * and is externally accessed through the __get magic method
2629       * @var stdClass
2630       */
2631      protected $properties;
2632  
2633      /**
2634       * An array of options for the summary editor used by course request forms.
2635       * This is initially set by {@link summary_editor_options()}
2636       * @var array
2637       * @static
2638       */
2639      protected static $summaryeditoroptions;
2640  
2641      /**
2642       * Static function to prepare the summary editor for working with a course
2643       * request.
2644       *
2645       * @static
2646       * @param null|stdClass $data Optional, an object containing the default values
2647       *                       for the form, these may be modified when preparing the
2648       *                       editor so this should be called before creating the form
2649       * @return stdClass An object that can be used to set the default values for
2650       *                   an mforms form
2651       */
2652      public static function prepare($data=null) {
2653          if ($data === null) {
2654              $data = new stdClass;
2655          }
2656          $data = file_prepare_standard_editor($data, 'summary', self::summary_editor_options());
2657          return $data;
2658      }
2659  
2660      /**
2661       * Static function to create a new course request when passed an array of properties
2662       * for it.
2663       *
2664       * This function also handles saving any files that may have been used in the editor
2665       *
2666       * @static
2667       * @param stdClass $data
2668       * @return course_request The newly created course request
2669       */
2670      public static function create($data) {
2671          global $USER, $DB, $CFG;
2672          $data->requester = $USER->id;
2673  
2674          // Setting the default category if none set.
2675          if (empty($data->category) || !empty($CFG->lockrequestcategory)) {
2676              $data->category = $CFG->defaultrequestcategory;
2677          }
2678  
2679          // Summary is a required field so copy the text over
2680          $data->summary       = $data->summary_editor['text'];
2681          $data->summaryformat = $data->summary_editor['format'];
2682  
2683          $data->id = $DB->insert_record('course_request', $data);
2684  
2685          // Create a new course_request object and return it
2686          $request = new course_request($data);
2687  
2688          // Notify the admin if required.
2689          if ($users = get_users_from_config($CFG->courserequestnotify, 'moodle/site:approvecourse')) {
2690  
2691              $a = new stdClass;
2692              $a->link = "$CFG->wwwroot/course/pending.php";
2693              $a->user = fullname($USER);
2694              $subject = get_string('courserequest');
2695              $message = get_string('courserequestnotifyemail', 'admin', $a);
2696              foreach ($users as $user) {
2697                  $request->notify($user, $USER, 'courserequested', $subject, $message);
2698              }
2699          }
2700  
2701          return $request;
2702      }
2703  
2704      /**
2705       * Returns an array of options to use with a summary editor
2706       *
2707       * @uses course_request::$summaryeditoroptions
2708       * @return array An array of options to use with the editor
2709       */
2710      public static function summary_editor_options() {
2711          global $CFG;
2712          if (self::$summaryeditoroptions === null) {
2713              self::$summaryeditoroptions = array('maxfiles' => 0, 'maxbytes'=>0);
2714          }
2715          return self::$summaryeditoroptions;
2716      }
2717  
2718      /**
2719       * Loads the properties for this course request object. Id is required and if
2720       * only id is provided then we load the rest of the properties from the database
2721       *
2722       * @param stdClass|int $properties Either an object containing properties
2723       *                      or the course_request id to load
2724       */
2725      public function __construct($properties) {
2726          global $DB;
2727          if (empty($properties->id)) {
2728              if (empty($properties)) {
2729                  throw new coding_exception('You must provide a course request id when creating a course_request object');
2730              }
2731              $id = $properties;
2732              $properties = new stdClass;
2733              $properties->id = (int)$id;
2734              unset($id);
2735          }
2736          if (empty($properties->requester)) {
2737              if (!($this->properties = $DB->get_record('course_request', array('id' => $properties->id)))) {
2738                  throw new \moodle_exception('unknowncourserequest');
2739              }
2740          } else {
2741              $this->properties = $properties;
2742          }
2743          $this->properties->collision = null;
2744      }
2745  
2746      /**
2747       * Returns the requested property
2748       *
2749       * @param string $key
2750       * @return mixed
2751       */
2752      public function __get($key) {
2753          return $this->properties->$key;
2754      }
2755  
2756      /**
2757       * Override this to ensure empty($request->blah) calls return a reliable answer...
2758       *
2759       * This is required because we define the __get method
2760       *
2761       * @param mixed $key
2762       * @return bool True is it not empty, false otherwise
2763       */
2764      public function __isset($key) {
2765          return (!empty($this->properties->$key));
2766      }
2767  
2768      /**
2769       * Returns the user who requested this course
2770       *
2771       * Uses a static var to cache the results and cut down the number of db queries
2772       *
2773       * @staticvar array $requesters An array of cached users
2774       * @return stdClass The user who requested the course
2775       */
2776      public function get_requester() {
2777          global $DB;
2778          static $requesters= array();
2779          if (!array_key_exists($this->properties->requester, $requesters)) {
2780              $requesters[$this->properties->requester] = $DB->get_record('user', array('id'=>$this->properties->requester));
2781          }
2782          return $requesters[$this->properties->requester];
2783      }
2784  
2785      /**
2786       * Checks that the shortname used by the course does not conflict with any other
2787       * courses that exist
2788       *
2789       * @param string|null $shortnamemark The string to append to the requests shortname
2790       *                     should a conflict be found
2791       * @return bool true is there is a conflict, false otherwise
2792       */
2793      public function check_shortname_collision($shortnamemark = '[*]') {
2794          global $DB;
2795  
2796          if ($this->properties->collision !== null) {
2797              return $this->properties->collision;
2798          }
2799  
2800          if (empty($this->properties->shortname)) {
2801              debugging('Attempting to check a course request shortname before it has been set', DEBUG_DEVELOPER);
2802              $this->properties->collision = false;
2803          } else if ($DB->record_exists('course', array('shortname' => $this->properties->shortname))) {
2804              if (!empty($shortnamemark)) {
2805                  $this->properties->shortname .= ' '.$shortnamemark;
2806              }
2807              $this->properties->collision = true;
2808          } else {
2809              $this->properties->collision = false;
2810          }
2811          return $this->properties->collision;
2812      }
2813  
2814      /**
2815       * Checks user capability to approve a requested course
2816       *
2817       * If course was requested without category for some reason (might happen if $CFG->defaultrequestcategory is
2818       * misconfigured), we check capabilities 'moodle/site:approvecourse' and 'moodle/course:changecategory'.
2819       *
2820       * @return bool
2821       */
2822      public function can_approve() {
2823          global $CFG;
2824          $category = null;
2825          if ($this->properties->category) {
2826              $category = core_course_category::get($this->properties->category, IGNORE_MISSING);
2827          } else if ($CFG->defaultrequestcategory) {
2828              $category = core_course_category::get($CFG->defaultrequestcategory, IGNORE_MISSING);
2829          }
2830          if ($category) {
2831              return has_capability('moodle/site:approvecourse', $category->get_context());
2832          }
2833  
2834          // We can not determine the context where the course should be created. The approver should have
2835          // both capabilities to approve courses and change course category in the system context.
2836          return has_all_capabilities(['moodle/site:approvecourse', 'moodle/course:changecategory'], context_system::instance());
2837      }
2838  
2839      /**
2840       * Returns the category where this course request should be created
2841       *
2842       * Note that we don't check here that user has a capability to view
2843       * hidden categories if he has capabilities 'moodle/site:approvecourse' and
2844       * 'moodle/course:changecategory'
2845       *
2846       * @return core_course_category
2847       */
2848      public function get_category() {
2849          global $CFG;
2850          if ($this->properties->category && ($category = core_course_category::get($this->properties->category, IGNORE_MISSING))) {
2851              return $category;
2852          } else if ($CFG->defaultrequestcategory &&
2853                  ($category = core_course_category::get($CFG->defaultrequestcategory, IGNORE_MISSING))) {
2854              return $category;
2855          } else {
2856              return core_course_category::get_default();
2857          }
2858      }
2859  
2860      /**
2861       * This function approves the request turning it into a course
2862       *
2863       * This function converts the course request into a course, at the same time
2864       * transferring any files used in the summary to the new course and then removing
2865       * the course request and the files associated with it.
2866       *
2867       * @return int The id of the course that was created from this request
2868       */
2869      public function approve() {
2870          global $CFG, $DB, $USER;
2871  
2872          require_once($CFG->dirroot . '/backup/util/includes/restore_includes.php');
2873  
2874          $user = $DB->get_record('user', array('id' => $this->properties->requester, 'deleted'=>0), '*', MUST_EXIST);
2875  
2876          $courseconfig = get_config('moodlecourse');
2877  
2878          // Transfer appropriate settings
2879          $data = clone($this->properties);
2880          unset($data->id);
2881          unset($data->reason);
2882          unset($data->requester);
2883  
2884          // Set category
2885          $category = $this->get_category();
2886          $data->category = $category->id;
2887          // Set misc settings
2888          $data->requested = 1;
2889  
2890          // Apply course default settings
2891          $data->format             = $courseconfig->format;
2892          $data->newsitems          = $courseconfig->newsitems;
2893          $data->showgrades         = $courseconfig->showgrades;
2894          $data->showreports        = $courseconfig->showreports;
2895          $data->maxbytes           = $courseconfig->maxbytes;
2896          $data->groupmode          = $courseconfig->groupmode;
2897          $data->groupmodeforce     = $courseconfig->groupmodeforce;
2898          $data->visible            = $courseconfig->visible;
2899          $data->visibleold         = $data->visible;
2900          $data->lang               = $courseconfig->lang;
2901          $data->enablecompletion   = $courseconfig->enablecompletion;
2902          $data->numsections        = $courseconfig->numsections;
2903          $data->startdate          = usergetmidnight(time());
2904          if ($courseconfig->courseenddateenabled) {
2905              $data->enddate        = usergetmidnight(time()) + $courseconfig->courseduration;
2906          }
2907  
2908          list($data->fullname, $data->shortname) = restore_dbops::calculate_course_names(0, $data->fullname, $data->shortname);
2909  
2910          $course = create_course($data);
2911          $context = context_course::instance($course->id, MUST_EXIST);
2912  
2913          // add enrol instances
2914          if (!$DB->record_exists('enrol', array('courseid'=>$course->id, 'enrol'=>'manual'))) {
2915              if ($manual = enrol_get_plugin('manual')) {
2916                  $manual->add_default_instance($course);
2917              }
2918          }
2919  
2920          // enrol the requester as teacher if necessary
2921          if (!empty($CFG->creatornewroleid) and !is_viewing($context, $user, 'moodle/role:assign') and !is_enrolled($context, $user, 'moodle/role:assign')) {
2922              enrol_try_internal_enrol($course->id, $user->id, $CFG->creatornewroleid);
2923          }
2924  
2925          $this->delete();
2926  
2927          $a = new stdClass();
2928          $a->name = format_string($course->fullname, true, array('context' => context_course::instance($course->id)));
2929          $a->url = $CFG->wwwroot.'/course/view.php?id=' . $course->id;
2930          $this->notify($user, $USER, 'courserequestapproved', get_string('courseapprovedsubject'), get_string('courseapprovedemail2', 'moodle', $a), $course->id);
2931  
2932          return $course->id;
2933      }
2934  
2935      /**
2936       * Reject a course request
2937       *
2938       * This function rejects a course request, emailing the requesting user the
2939       * provided notice and then removing the request from the database
2940       *
2941       * @param string $notice The message to display to the user
2942       */
2943      public function reject($notice) {
2944          global $USER, $DB;
2945          $user = $DB->get_record('user', array('id' => $this->properties->requester), '*', MUST_EXIST);
2946          $this->notify($user, $USER, 'courserequestrejected', get_string('courserejectsubject'), get_string('courserejectemail', 'moodle', $notice));
2947          $this->delete();
2948      }
2949  
2950      /**
2951       * Deletes the course request and any associated files
2952       */
2953      public function delete() {
2954          global $DB;
2955          $DB->delete_records('course_request', array('id' => $this->properties->id));
2956      }
2957  
2958      /**
2959       * Send a message from one user to another using events_trigger
2960       *
2961       * @param object $touser
2962       * @param object $fromuser
2963       * @param string $name
2964       * @param string $subject
2965       * @param string $message
2966       * @param int|null $courseid
2967       */
2968      protected function notify($touser, $fromuser, $name, $subject, $message, $courseid = null) {
2969          $eventdata = new \core\message\message();
2970          $eventdata->courseid          = empty($courseid) ? SITEID : $courseid;
2971          $eventdata->component         = 'moodle';
2972          $eventdata->name              = $name;
2973          $eventdata->userfrom          = $fromuser;
2974          $eventdata->userto            = $touser;
2975          $eventdata->subject           = $subject;
2976          $eventdata->fullmessage       = $message;
2977          $eventdata->fullmessageformat = FORMAT_PLAIN;
2978          $eventdata->fullmessagehtml   = '';
2979          $eventdata->smallmessage      = '';
2980          $eventdata->notification      = 1;
2981          message_send($eventdata);
2982      }
2983  
2984      /**
2985       * Checks if current user can request a course in this context
2986       *
2987       * @param context $context
2988       * @return bool
2989       */
2990      public static function can_request(context $context) {
2991          global $CFG;
2992          if (empty($CFG->enablecourserequests)) {
2993              return false;
2994          }
2995          if (has_capability('moodle/course:create', $context)) {
2996              return false;
2997          }
2998  
2999          if ($context instanceof context_system) {
3000              $defaultcontext = context_coursecat::instance($CFG->defaultrequestcategory, IGNORE_MISSING);
3001              return $defaultcontext &&
3002                  has_capability('moodle/course:request', $defaultcontext);
3003          } else if ($context instanceof context_coursecat) {
3004              if (!$CFG->lockrequestcategory || $CFG->defaultrequestcategory == $context->instanceid) {
3005                  return has_capability('moodle/course:request', $context);
3006              }
3007          }
3008          return false;
3009      }
3010  }
3011  
3012  /**
3013   * Return a list of page types
3014   * @param string $pagetype current page type
3015   * @param context $parentcontext Block's parent context
3016   * @param context $currentcontext Current context of block
3017   * @return array array of page types
3018   */
3019  function course_page_type_list($pagetype, $parentcontext, $currentcontext) {
3020      if ($pagetype === 'course-index' || $pagetype === 'course-index-category') {
3021          // For courses and categories browsing pages (/course/index.php) add option to show on ANY category page
3022          $pagetypes = array('*' => get_string('page-x', 'pagetype'),
3023              'course-index-*' => get_string('page-course-index-x', 'pagetype'),
3024          );
3025      } else if ($currentcontext && (!($coursecontext = $currentcontext->get_course_context(false)) || $coursecontext->instanceid == SITEID)) {
3026          // We know for sure that despite pagetype starts with 'course-' this is not a page in course context (i.e. /course/search.php, etc.)
3027          $pagetypes = array('*' => get_string('page-x', 'pagetype'));
3028      } else {
3029          // Otherwise consider it a page inside a course even if $currentcontext is null
3030          $pagetypes = array('*' => get_string('page-x', 'pagetype'),
3031              'course-*' => get_string('page-course-x', 'pagetype'),
3032              'course-view-*' => get_string('page-course-view-x', 'pagetype')
3033          );
3034      }
3035      return $pagetypes;
3036  }
3037  
3038  /**
3039   * Determine whether course ajax should be enabled for the specified course
3040   *
3041   * @param stdClass $course The course to test against
3042   * @return boolean Whether course ajax is enabled or note
3043   */
3044  function course_ajax_enabled($course) {
3045      global $CFG, $PAGE, $SITE;
3046  
3047      // The user must be editing for AJAX to be included
3048      if (!$PAGE->user_is_editing()) {
3049          return false;
3050      }
3051  
3052      // Check that the theme suports
3053      if (!$PAGE->theme->enablecourseajax) {
3054          return false;
3055      }
3056  
3057      // Check that the course format supports ajax functionality
3058      // The site 'format' doesn't have information on course format support
3059      if ($SITE->id !== $course->id) {
3060          $courseformatajaxsupport = course_format_ajax_support($course->format);
3061          if (!$courseformatajaxsupport->capable) {
3062              return false;
3063          }
3064      }
3065  
3066      // All conditions have been met so course ajax should be enabled
3067      return true;
3068  }
3069  
3070  /**
3071   * Include the relevant javascript and language strings for the resource
3072   * toolbox YUI module
3073   *
3074   * @param integer $id The ID of the course being applied to
3075   * @param array $usedmodules An array containing the names of the modules in use on the page
3076   * @param array $enabledmodules An array containing the names of the enabled (visible) modules on this site
3077   * @param stdClass $config An object containing configuration parameters for ajax modules including:
3078   *          * resourceurl   The URL to post changes to for resource changes
3079   *          * sectionurl    The URL to post changes to for section changes
3080   *          * pageparams    Additional parameters to pass through in the post
3081   * @return bool
3082   */
3083  function include_course_ajax($course, $usedmodules = array(), $enabledmodules = null, $config = null) {
3084      global $CFG, $PAGE, $SITE;
3085  
3086      // Init the course editor module to support UI components.
3087      $format = course_get_format($course);
3088      include_course_editor($format);
3089  
3090      // Ensure that ajax should be included
3091      if (!course_ajax_enabled($course)) {
3092          return false;
3093      }
3094  
3095      // Component based formats don't use YUI drag and drop anymore.
3096      if (!$format->supports_components() && course_format_uses_sections($course->format)) {
3097  
3098          if (!$config) {
3099              $config = new stdClass();
3100          }
3101  
3102          // The URL to use for resource changes.
3103          if (!isset($config->resourceurl)) {
3104              $config->resourceurl = '/course/rest.php';
3105          }
3106  
3107          // The URL to use for section changes.
3108          if (!isset($config->sectionurl)) {
3109              $config->sectionurl = '/course/rest.php';
3110          }
3111  
3112          // Any additional parameters which need to be included on page submission.
3113          if (!isset($config->pageparams)) {
3114              $config->pageparams = array();
3115          }
3116  
3117          $PAGE->requires->yui_module('moodle-course-dragdrop', 'M.course.init_section_dragdrop',
3118              array(array(
3119                  'courseid' => $course->id,
3120                  'ajaxurl' => $config->sectionurl,
3121                  'config' => $config,
3122              )), null, true);
3123  
3124          $PAGE->requires->yui_module('moodle-course-dragdrop', 'M.course.init_resource_dragdrop',
3125              array(array(
3126                  'courseid' => $course->id,
3127                  'ajaxurl' => $config->resourceurl,
3128                  'config' => $config,
3129              )), null, true);
3130      }
3131  
3132      // Require various strings for the command toolbox
3133      $PAGE->requires->strings_for_js(array(
3134              'moveleft',
3135              'deletechecktype',
3136              'deletechecktypename',
3137              'edittitle',
3138              'edittitleinstructions',
3139              'show',
3140              'hide',
3141              'highlight',
3142              'highlightoff',
3143              'groupsnone',
3144              'groupsvisible',
3145              'groupsseparate',
3146              'clicktochangeinbrackets',
3147              'markthistopic',
3148              'markedthistopic',
3149              'movesection',
3150              'movecoursemodule',
3151              'movecoursesection',
3152              'movecontent',
3153              'tocontent',
3154              'emptydragdropregion',
3155              'afterresource',
3156              'aftersection',
3157              'totopofsection',
3158          ), 'moodle');
3159  
3160      // Include section-specific strings for formats which support sections.
3161      if (course_format_uses_sections($course->format)) {
3162          $PAGE->requires->strings_for_js(array(
3163                  'showfromothers',
3164                  'hidefromothers',
3165              ), 'format_' . $course->format);
3166      }
3167  
3168      // For confirming resource deletion we need the name of the module in question
3169      foreach ($usedmodules as $module => $modname) {
3170          $PAGE->requires->string_for_js('pluginname', $module);
3171      }
3172  
3173      // Load drag and drop upload AJAX.
3174      require_once($CFG->dirroot.'/course/dnduploadlib.php');
3175      dndupload_add_to_course($course, $enabledmodules);
3176  
3177      $PAGE->requires->js_call_amd('core_course/actions', 'initCoursePage', array($course->format));
3178  
3179      return true;
3180  }
3181  
3182  /**
3183   * Include and configure the course editor modules.
3184   *
3185   * @param course_format $format the course format instance.
3186   */
3187  function include_course_editor(course_format $format) {
3188      global $PAGE, $SITE;
3189  
3190      $course = $format->get_course();
3191  
3192      if ($SITE->id === $course->id) {
3193          return;
3194      }
3195  
3196      $statekey = course_format::session_cache($course);
3197  
3198      // Edition mode and some format specs must be passed to the init method.
3199      $setup = (object)[
3200          'editing' => $format->show_editor(),
3201          'supportscomponents' => $format->supports_components(),
3202          'statekey' => $statekey,
3203      ];
3204      // All the new editor elements will be loaded after the course is presented and
3205      // the initial course state will be generated using core_course_get_state webservice.
3206      $PAGE->requires->js_call_amd('core_courseformat/courseeditor', 'setViewFormat', [$course->id, $setup]);
3207  }
3208  
3209  /**
3210   * Returns the sorted list of available course formats, filtered by enabled if necessary
3211   *
3212   * @param bool $enabledonly return only formats that are enabled
3213   * @return array array of sorted format names
3214   */
3215  function get_sorted_course_formats($enabledonly = false) {
3216      global $CFG;
3217      $formats = core_component::get_plugin_list('format');
3218  
3219      if (!empty($CFG->format_plugins_sortorder)) {
3220          $order = explode(',', $CFG->format_plugins_sortorder);
3221          $order = array_merge(array_intersect($order, array_keys($formats)),
3222                      array_diff(array_keys($formats), $order));
3223      } else {
3224          $order = array_keys($formats);
3225      }
3226      if (!$enabledonly) {
3227          return $order;
3228      }
3229      $sortedformats = array();
3230      foreach ($order as $formatname) {
3231          if (!get_config('format_'.$formatname, 'disabled')) {
3232              $sortedformats[] = $formatname;
3233          }
3234      }
3235      return $sortedformats;
3236  }
3237  
3238  /**
3239   * The URL to use for the specified course (with section)
3240   *
3241   * @param int|stdClass $courseorid The course to get the section name for (either object or just course id)
3242   * @param int|stdClass $section Section object from database or just field course_sections.section
3243   *     if omitted the course view page is returned
3244   * @param array $options options for view URL. At the moment core uses:
3245   *     'navigation' (bool) if true and section has no separate page, the function returns null
3246   *     'sr' (int) used by multipage formats to specify to which section to return
3247   * @return moodle_url The url of course
3248   */
3249  function course_get_url($courseorid, $section = null, $options = array()) {
3250      return course_get_format($courseorid)->get_view_url($section, $options);
3251  }
3252  
3253  /**
3254   * Create a module.
3255   *
3256   * It includes:
3257   *      - capability checks and other checks
3258   *      - create the module from the module info
3259   *
3260   * @param object $module
3261   * @return object the created module info
3262   * @throws moodle_exception if user is not allowed to perform the action or module is not allowed in this course
3263   */
3264  function create_module($moduleinfo) {
3265      global $DB, $CFG;
3266  
3267      require_once($CFG->dirroot . '/course/modlib.php');
3268  
3269      // Check manadatory attributs.
3270      $mandatoryfields = array('modulename', 'course', 'section', 'visible');
3271      if (plugin_supports('mod', $moduleinfo->modulename, FEATURE_MOD_INTRO, true)) {
3272          $mandatoryfields[] = 'introeditor';
3273      }
3274      foreach($mandatoryfields as $mandatoryfield) {
3275          if (!isset($moduleinfo->{$mandatoryfield})) {
3276              throw new moodle_exception('createmodulemissingattribut', '', '', $mandatoryfield);
3277          }
3278      }
3279  
3280      // Some additional checks (capability / existing instances).
3281      $course = $DB->get_record('course', array('id'=>$moduleinfo->course), '*', MUST_EXIST);
3282      list($module, $context, $cw) = can_add_moduleinfo($course, $moduleinfo->modulename, $moduleinfo->section);
3283  
3284      // Add the module.
3285      $moduleinfo->module = $module->id;
3286      $moduleinfo = add_moduleinfo($moduleinfo, $course, null);
3287  
3288      return $moduleinfo;
3289  }
3290  
3291  /**
3292   * Update a module.
3293   *
3294   * It includes:
3295   *      - capability and other checks
3296   *      - update the module
3297   *
3298   * @param object $module
3299   * @return object the updated module info
3300   * @throws moodle_exception if current user is not allowed to update the module
3301   */
3302  function update_module($moduleinfo) {
3303      global $DB, $CFG;
3304  
3305      require_once($CFG->dirroot . '/course/modlib.php');
3306  
3307      // Check the course module exists.
3308      $cm = get_coursemodule_from_id('', $moduleinfo->coursemodule, 0, false, MUST_EXIST);
3309  
3310      // Check the course exists.
3311      $course = $DB->get_record('course', array('id'=>$cm->course), '*', MUST_EXIST);
3312  
3313      // Some checks (capaibility / existing instances).
3314      list($cm, $context, $module, $data, $cw) = can_update_moduleinfo($cm);
3315  
3316      // Retrieve few information needed by update_moduleinfo.
3317      $moduleinfo->modulename = $cm->modname;
3318      if (!isset($moduleinfo->scale)) {
3319          $moduleinfo->scale = 0;
3320      }
3321      $moduleinfo->type = 'mod';
3322  
3323      // Update the module.
3324      list($cm, $moduleinfo) = update_moduleinfo($cm, $moduleinfo, $course, null);
3325  
3326      return $moduleinfo;
3327  }
3328  
3329  /**
3330   * Duplicate a module on the course for ajax.
3331   *
3332   * @see mod_duplicate_module()
3333   * @param object $course The course
3334   * @param object $cm The course module to duplicate
3335   * @param int $sr The section to link back to (used for creating the links)
3336   * @throws moodle_exception if the plugin doesn't support duplication
3337   * @return Object containing:
3338   * - fullcontent: The HTML markup for the created CM
3339   * - cmid: The CMID of the newly created CM
3340   * - redirect: Whether to trigger a redirect following this change
3341   */
3342  function mod_duplicate_activity($course, $cm, $sr = null) {
3343      global $PAGE;
3344  
3345      $newcm = duplicate_module($course, $cm);
3346  
3347      $resp = new stdClass();
3348      if ($newcm) {
3349  
3350          $format = course_get_format($course);
3351          $renderer = $format->get_renderer($PAGE);
3352          $modinfo = $format->get_modinfo();
3353          $section = $modinfo->get_section_info($newcm->sectionnum);
3354  
3355          // Get the new element html content.
3356          $resp->fullcontent = $renderer->course_section_updated_cm_item($format, $section, $newcm);
3357  
3358          $resp->cmid = $newcm->id;
3359      } else {
3360          // Trigger a redirect.
3361          $resp->redirect = true;
3362      }
3363      return $resp;
3364  }
3365  
3366  /**
3367   * Api to duplicate a module.
3368   *
3369   * @param object $course course object.
3370   * @param object $cm course module object to be duplicated.
3371   * @since Moodle 2.8
3372   *
3373   * @throws Exception
3374   * @throws coding_exception
3375   * @throws moodle_exception
3376   * @throws restore_controller_exception
3377   *
3378   * @return cm_info|null cminfo object if we sucessfully duplicated the mod and found the new cm.
3379   */
3380  function duplicate_module($course, $cm) {
3381      global $CFG, $DB, $USER;
3382      require_once($CFG->dirroot . '/backup/util/includes/backup_includes.php');
3383      require_once($CFG->dirroot . '/backup/util/includes/restore_includes.php');
3384      require_once($CFG->libdir . '/filelib.php');
3385  
3386      $a          = new stdClass();
3387      $a->modtype = get_string('modulename', $cm->modname);
3388      $a->modname = format_string($cm->name);
3389  
3390      if (!plugin_supports('mod', $cm->modname, FEATURE_BACKUP_MOODLE2)) {
3391          throw new moodle_exception('duplicatenosupport', 'error', '', $a);
3392      }
3393  
3394      // Backup the activity.
3395  
3396      $bc = new backup_controller(backup::TYPE_1ACTIVITY, $cm->id, backup::FORMAT_MOODLE,
3397              backup::INTERACTIVE_NO, backup::MODE_IMPORT, $USER->id);
3398  
3399      $backupid       = $bc->get_backupid();
3400      $backupbasepath = $bc->get_plan()->get_basepath();
3401  
3402      $bc->execute_plan();
3403  
3404      $bc->destroy();
3405  
3406      // Restore the backup immediately.
3407  
3408      $rc = new restore_controller($backupid, $course->id,
3409              backup::INTERACTIVE_NO, backup::MODE_IMPORT, $USER->id, backup::TARGET_CURRENT_ADDING);
3410  
3411      // Make sure that the restore_general_groups setting is always enabled when duplicating an activity.
3412      $plan = $rc->get_plan();
3413      $groupsetting = $plan->get_setting('groups');
3414      if (empty($groupsetting->get_value())) {
3415          $groupsetting->set_value(true);
3416      }
3417  
3418      $cmcontext = context_module::instance($cm->id);
3419      if (!$rc->execute_precheck()) {
3420          $precheckresults = $rc->get_precheck_results();
3421          if (is_array($precheckresults) && !empty($precheckresults['errors'])) {
3422              if (empty($CFG->keeptempdirectoriesonbackup)) {
3423                  fulldelete($backupbasepath);
3424              }
3425          }
3426      }
3427  
3428      $rc->execute_plan();
3429  
3430      // Now a bit hacky part follows - we try to get the cmid of the newly
3431      // restored copy of the module.
3432      $newcmid = null;
3433      $tasks = $rc->get_plan()->get_tasks();
3434      foreach ($tasks as $task) {
3435          if (is_subclass_of($task, 'restore_activity_task')) {
3436              if ($task->get_old_contextid() == $cmcontext->id) {
3437                  $newcmid = $task->get_moduleid();
3438                  break;
3439              }
3440          }
3441      }
3442  
3443      $rc->destroy();
3444  
3445      if (empty($CFG->keeptempdirectoriesonbackup)) {
3446          fulldelete($backupbasepath);
3447      }
3448  
3449      // If we know the cmid of the new course module, let us move it
3450      // right below the original one. otherwise it will stay at the
3451      // end of the section.
3452      if ($newcmid) {
3453          // Proceed with activity renaming before everything else. We don't use APIs here to avoid
3454          // triggering a lot of create/update duplicated events.
3455          $newcm = get_coursemodule_from_id($cm->modname, $newcmid, $cm->course);
3456          // Add ' (copy)' language string postfix to duplicated module.
3457          $newname = get_string('duplicatedmodule', 'moodle', $newcm->name);
3458          set_coursemodule_name($newcm->id, $newname);
3459  
3460          $section = $DB->get_record('course_sections', array('id' => $cm->section, 'course' => $cm->course));
3461          $modarray = explode(",", trim($section->sequence));
3462          $cmindex = array_search($cm->id, $modarray);
3463          if ($cmindex !== false && $cmindex < count($modarray) - 1) {
3464              moveto_module($newcm, $section, $modarray[$cmindex + 1]);
3465          }
3466  
3467          // Update calendar events with the duplicated module.
3468          // The following line is to be removed in MDL-58906.
3469          course_module_update_calendar_events($newcm->modname, null, $newcm);
3470  
3471          // Trigger course module created event. We can trigger the event only if we know the newcmid.
3472          $newcm = get_fast_modinfo($cm->course)->get_cm($newcmid);
3473          $event = \core\event\course_module_created::create_from_cm($newcm);
3474          $event->trigger();
3475      }
3476  
3477      return isset($newcm) ? $newcm : null;
3478  }
3479  
3480  /**
3481   * Compare two objects to find out their correct order based on timestamp (to be used by usort).
3482   * Sorts by descending order of time.
3483   *
3484   * @param stdClass $a First object
3485   * @param stdClass $b Second object
3486   * @return int 0,1,-1 representing the order
3487   */
3488  function compare_activities_by_time_desc($a, $b) {
3489      // Make sure the activities actually have a timestamp property.
3490      if ((!property_exists($a, 'timestamp')) && (!property_exists($b, 'timestamp'))) {
3491          return 0;
3492      }
3493      // We treat instances without timestamp as if they have a timestamp of 0.
3494      if ((!property_exists($a, 'timestamp')) && (property_exists($b,'timestamp'))) {
3495          return 1;
3496      }
3497      if ((property_exists($a, 'timestamp')) && (!property_exists($b, 'timestamp'))) {
3498          return -1;
3499      }
3500      if ($a->timestamp == $b->timestamp) {
3501          return 0;
3502      }
3503      return ($a->timestamp > $b->timestamp) ? -1 : 1;
3504  }
3505  
3506  /**
3507   * Compare two objects to find out their correct order based on timestamp (to be used by usort).
3508   * Sorts by ascending order of time.
3509   *
3510   * @param stdClass $a First object
3511   * @param stdClass $b Second object
3512   * @return int 0,1,-1 representing the order
3513   */
3514  function compare_activities_by_time_asc($a, $b) {
3515      // Make sure the activities actually have a timestamp property.
3516      if ((!property_exists($a, 'timestamp')) && (!property_exists($b, 'timestamp'))) {
3517        return 0;
3518      }
3519      // We treat instances without timestamp as if they have a timestamp of 0.
3520      if ((!property_exists($a, 'timestamp')) && (property_exists($b, 'timestamp'))) {
3521          return -1;
3522      }
3523      if ((property_exists($a, 'timestamp')) && (!property_exists($b, 'timestamp'))) {
3524          return 1;
3525      }
3526      if ($a->timestamp == $b->timestamp) {
3527          return 0;
3528      }
3529      return ($a->timestamp < $b->timestamp) ? -1 : 1;
3530  }
3531  
3532  /**
3533   * Changes the visibility of a course.
3534   *
3535   * @param int $courseid The course to change.
3536   * @param bool $show True to make it visible, false otherwise.
3537   * @return bool
3538   */
3539  function course_change_visibility($courseid, $show = true) {
3540      $course = new stdClass;
3541      $course->id = $courseid;
3542      $course->visible = ($show) ? '1' : '0';
3543      $course->visibleold = $course->visible;
3544      update_course($course);
3545      return true;
3546  }
3547  
3548  /**
3549   * Changes the course sortorder by one, moving it up or down one in respect to sort order.
3550   *
3551   * @param stdClass|core_course_list_element $course
3552   * @param bool $up If set to true the course will be moved up one. Otherwise down one.
3553   * @return bool
3554   */
3555  function course_change_sortorder_by_one($course, $up) {
3556      global $DB;
3557      $params = array($course->sortorder, $course->category);
3558      if ($up) {
3559          $select = 'sortorder < ? AND category = ?';
3560          $sort = 'sortorder DESC';
3561      } else {
3562          $select = 'sortorder > ? AND category = ?';
3563          $sort = 'sortorder ASC';
3564      }
3565      fix_course_sortorder();
3566      $swapcourse = $DB->get_records_select('course', $select, $params, $sort, '*', 0, 1);
3567      if ($swapcourse) {
3568          $swapcourse = reset($swapcourse);
3569          $DB->set_field('course', 'sortorder', $swapcourse->sortorder, array('id' => $course->id));
3570          $DB->set_field('course', 'sortorder', $course->sortorder, array('id' => $swapcourse->id));
3571          // Finally reorder courses.
3572          fix_course_sortorder();
3573          cache_helper::purge_by_event('changesincourse');
3574          return true;
3575      }
3576      return false;
3577  }
3578  
3579  /**
3580   * Changes the sort order of courses in a category so that the first course appears after the second.
3581   *
3582   * @param int|stdClass $courseorid The course to focus on.
3583   * @param int $moveaftercourseid The course to shifter after or 0 if you want it to be the first course in the category.
3584   * @return bool
3585   */
3586  function course_change_sortorder_after_course($courseorid, $moveaftercourseid) {
3587      global $DB;
3588  
3589      if (!is_object($courseorid)) {
3590          $course = get_course($courseorid);
3591      } else {
3592          $course = $courseorid;
3593      }
3594  
3595      if ((int)$moveaftercourseid === 0) {
3596          // We've moving the course to the start of the queue.
3597          $sql = 'SELECT sortorder
3598                        FROM {course}
3599                       WHERE category = :categoryid
3600                    ORDER BY sortorder';
3601          $params = array(
3602              'categoryid' => $course->category
3603          );
3604          $sortorder = $DB->get_field_sql($sql, $params, IGNORE_MULTIPLE);
3605  
3606          $sql = 'UPDATE {course}
3607                     SET sortorder = sortorder + 1
3608                   WHERE category = :categoryid
3609                     AND id <> :id';
3610          $params = array(
3611              'categoryid' => $course->category,
3612              'id' => $course->id,
3613          );
3614          $DB->execute($sql, $params);
3615          $DB->set_field('course', 'sortorder', $sortorder, array('id' => $course->id));
3616      } else if ($course->id === $moveaftercourseid) {
3617          // They're the same - moronic.
3618          debugging("Invalid move after course given.", DEBUG_DEVELOPER);
3619          return false;
3620      } else {
3621          // Moving this course after the given course. It could be before it could be after.
3622          $moveaftercourse = get_course($moveaftercourseid);
3623          if ($course->category !== $moveaftercourse->category) {
3624              debugging("Cannot re-order courses. The given courses do not belong to the same category.", DEBUG_DEVELOPER);
3625              return false;
3626          }
3627          // Increment all courses in the same category that are ordered after the moveafter course.
3628          // This makes a space for the course we're moving.
3629          $sql = 'UPDATE {course}
3630                         SET sortorder = sortorder + 1
3631                       WHERE category = :categoryid
3632                         AND sortorder > :sortorder';
3633          $params = array(
3634              'categoryid' => $moveaftercourse->category,
3635              'sortorder' => $moveaftercourse->sortorder
3636          );
3637          $DB->execute($sql, $params);
3638          $DB->set_field('course', 'sortorder', $moveaftercourse->sortorder + 1, array('id' => $course->id));
3639      }
3640      fix_course_sortorder();
3641      cache_helper::purge_by_event('changesincourse');
3642      return true;
3643  }
3644  
3645  /**
3646   * Trigger course viewed event. This API function is used when course view actions happens,
3647   * usually in course/view.php but also in external functions.
3648   *
3649   * @param stdClass  $context course context object
3650   * @param int $sectionnumber section number
3651   * @since Moodle 2.9
3652   */
3653  function course_view($context, $sectionnumber = 0) {
3654  
3655      $eventdata = array('context' => $context);
3656  
3657      if (!empty($sectionnumber)) {
3658          $eventdata['other']['coursesectionnumber'] = $sectionnumber;
3659      }
3660  
3661      $event = \core\event\course_viewed::create($eventdata);
3662      $event->trigger();
3663  
3664      user_accesstime_log($context->instanceid);
3665  }
3666  
3667  /**
3668   * Returns courses tagged with a specified tag.
3669   *
3670   * @param core_tag_tag $tag
3671   * @param bool $exclusivemode if set to true it means that no other entities tagged with this tag
3672   *             are displayed on the page and the per-page limit may be bigger
3673   * @param int $fromctx context id where the link was displayed, may be used by callbacks
3674   *            to display items in the same context first
3675   * @param int $ctx context id where to search for records
3676   * @param bool $rec search in subcontexts as well
3677   * @param int $page 0-based number of page being displayed
3678   * @return \core_tag\output\tagindex
3679   */
3680  function course_get_tagged_courses($tag, $exclusivemode = false, $fromctx = 0, $ctx = 0, $rec = 1, $page = 0) {
3681      global $CFG, $PAGE;
3682  
3683      $perpage = $exclusivemode ? $CFG->coursesperpage : 5;
3684      $displayoptions = array(
3685          'limit' => $perpage,
3686          'offset' => $page * $perpage,
3687          'viewmoreurl' => null,
3688      );
3689  
3690      $courserenderer = $PAGE->get_renderer('core', 'course');
3691      $totalcount = core_course_category::search_courses_count(array('tagid' => $tag->id, 'ctx' => $ctx, 'rec' => $rec));
3692      $content = $courserenderer->tagged_courses($tag->id, $exclusivemode, $ctx, $rec, $displayoptions);
3693      $totalpages = ceil($totalcount / $perpage);
3694  
3695      return new core_tag\output\tagindex($tag, 'core', 'course', $content,
3696              $exclusivemode, $fromctx, $ctx, $rec, $page, $totalpages);
3697  }
3698  
3699  /**
3700   * Implements callback inplace_editable() allowing to edit values in-place
3701   *
3702   * @param string $itemtype
3703   * @param int $itemid
3704   * @param mixed $newvalue
3705   * @return \core\output\inplace_editable
3706   */
3707  function core_course_inplace_editable($itemtype, $itemid, $newvalue) {
3708      if ($itemtype === 'activityname') {
3709          return \core_courseformat\output\local\content\cm\title::update($itemid, $newvalue);
3710      }
3711  }
3712  
3713  /**
3714   * This function calculates the minimum and maximum cutoff values for the timestart of
3715   * the given event.
3716   *
3717   * It will return an array with two values, the first being the minimum cutoff value and
3718   * the second being the maximum cutoff value. Either or both values can be null, which
3719   * indicates there is no minimum or maximum, respectively.
3720   *
3721   * If a cutoff is required then the function must return an array containing the cutoff
3722   * timestamp and error string to display to the user if the cutoff value is violated.
3723   *
3724   * A minimum and maximum cutoff return value will look like:
3725   * [
3726   *     [1505704373, 'The date must be after this date'],
3727   *     [1506741172, 'The date must be before this date']
3728   * ]
3729   *
3730   * @param calendar_event $event The calendar event to get the time range for
3731   * @param stdClass $course The course object to get the range from
3732   * @return array Returns an array with min and max date.
3733   */
3734  function core_course_core_calendar_get_valid_event_timestart_range(\calendar_event $event, $course) {
3735      $mindate = null;
3736      $maxdate = null;
3737  
3738      if ($course->startdate) {
3739          $mindate = [
3740              $course->startdate,
3741              get_string('errorbeforecoursestart', 'calendar')
3742          ];
3743      }
3744  
3745      return [$mindate, $maxdate];
3746  }
3747  
3748  /**
3749   * Render the message drawer to be included in the top of the body of each page.
3750   *
3751   * @return string HTML
3752   */
3753  function core_course_drawer(): string {
3754      global $PAGE;
3755  
3756      // Only add course index on non-site course pages.
3757      if (!$PAGE->course || $PAGE->course->id == SITEID) {
3758          return '';
3759      }
3760  
3761      // Show course index to users can access the course only.
3762      if (!can_access_course($PAGE->course, null, '', true)) {
3763          return '';
3764      }
3765  
3766      $format = course_get_format($PAGE->course);
3767      $renderer = $format->get_renderer($PAGE);
3768      if (method_exists($renderer, 'course_index_drawer')) {
3769          return $renderer->course_index_drawer($format);
3770      }
3771  
3772      return '';
3773  }
3774  
3775  /**
3776   * Returns course modules tagged with a specified tag ready for output on tag/index.php page
3777   *
3778   * This is a callback used by the tag area core/course_modules to search for course modules
3779   * tagged with a specific tag.
3780   *
3781   * @param core_tag_tag $tag
3782   * @param bool $exclusivemode if set to true it means that no other entities tagged with this tag
3783   *             are displayed on the page and the per-page limit may be bigger
3784   * @param int $fromcontextid context id where the link was displayed, may be used by callbacks
3785   *            to display items in the same context first
3786   * @param int $contextid context id where to search for records
3787   * @param bool $recursivecontext search in subcontexts as well
3788   * @param int $page 0-based number of page being displayed
3789   * @return \core_tag\output\tagindex
3790   */
3791  function course_get_tagged_course_modules($tag, $exclusivemode = false, $fromcontextid = 0, $contextid = 0,
3792                                            $recursivecontext = 1, $page = 0) {
3793      global $OUTPUT;
3794      $perpage = $exclusivemode ? 20 : 5;
3795  
3796      // Build select query.
3797      $ctxselect = context_helper::get_preload_record_columns_sql('ctx');
3798      $query = "SELECT cm.id AS cmid, c.id AS courseid, $ctxselect
3799                  FROM {course_modules} cm
3800                  JOIN {tag_instance} tt ON cm.id = tt.itemid
3801                  JOIN {course} c ON cm.course = c.id
3802                  JOIN {context} ctx ON ctx.instanceid = cm.id AND ctx.contextlevel = :coursemodulecontextlevel
3803                 WHERE tt.itemtype = :itemtype AND tt.tagid = :tagid AND tt.component = :component
3804                  AND cm.deletioninprogress = 0
3805                  AND c.id %COURSEFILTER% AND cm.id %ITEMFILTER%";
3806  
3807      $params = array('itemtype' => 'course_modules', 'tagid' => $tag->id, 'component' => 'core',
3808          'coursemodulecontextlevel' => CONTEXT_MODULE);
3809      if ($contextid) {
3810          $context = context::instance_by_id($contextid);
3811          $query .= $recursivecontext ? ' AND (ctx.id = :contextid OR ctx.path LIKE :path)' : ' AND ctx.id = :contextid';
3812          $params['contextid'] = $context->id;
3813          $params['path'] = $context->path.'/%';
3814      }
3815  
3816      $query .= ' ORDER BY';
3817      if ($fromcontextid) {
3818          // In order-clause specify that modules from inside "fromctx" context should be returned first.
3819          $fromcontext = context::instance_by_id($fromcontextid);
3820          $query .= ' (CASE WHEN ctx.id = :fromcontextid OR ctx.path LIKE :frompath THEN 0 ELSE 1 END),';
3821          $params['fromcontextid'] = $fromcontext->id;
3822          $params['frompath'] = $fromcontext->path.'/%';
3823      }
3824      $query .= ' c.sortorder, cm.id';
3825      $totalpages = $page + 1;
3826  
3827      // Use core_tag_index_builder to build and filter the list of items.
3828      // Request one item more than we need so we know if next page exists.
3829      $builder = new core_tag_index_builder('core', 'course_modules', $query, $params, $page * $perpage, $perpage + 1);
3830      while ($item = $builder->has_item_that_needs_access_check()) {
3831          context_helper::preload_from_record($item);
3832          $courseid = $item->courseid;
3833          if (!$builder->can_access_course($courseid)) {
3834              $builder->set_accessible($item, false);
3835              continue;
3836          }
3837          $modinfo = get_fast_modinfo($builder->get_course($courseid));
3838          // Set accessibility of this item and all other items in the same course.
3839          $builder->walk(function ($taggeditem) use ($courseid, $modinfo, $builder) {
3840              if ($taggeditem->courseid == $courseid) {
3841                  $cm = $modinfo->get_cm($taggeditem->cmid);
3842                  $builder->set_accessible($taggeditem, $cm->uservisible);
3843              }
3844          });
3845      }
3846  
3847      $items = $builder->get_items();
3848      if (count($items) > $perpage) {
3849          $totalpages = $page + 2; // We don't need exact page count, just indicate that the next page exists.
3850          array_pop($items);
3851      }
3852  
3853      // Build the display contents.
3854      if ($items) {
3855          $tagfeed = new core_tag\output\tagfeed();
3856          foreach ($items as $item) {
3857              context_helper::preload_from_record($item);
3858              $course = $builder->get_course($item->courseid);
3859              $modinfo = get_fast_modinfo($course);
3860              $cm = $modinfo->get_cm($item->cmid);
3861              $courseurl = course_get_url($item->courseid, $cm->sectionnum);
3862              $cmname = $cm->get_formatted_name();
3863              if (!$exclusivemode) {
3864                  $cmname = shorten_text($cmname, 100);
3865              }
3866              $cmname = html_writer::link($cm->url?:$courseurl, $cmname);
3867              $coursename = format_string($course->fullname, true,
3868                      array('context' => context_course::instance($item->courseid)));
3869              $coursename = html_writer::link($courseurl, $coursename);
3870              $icon = html_writer::empty_tag('img', array('src' => $cm->get_icon_url()));
3871              $tagfeed->add($icon, $cmname, $coursename);
3872          }
3873  
3874          $content = $OUTPUT->render_from_template('core_tag/tagfeed',
3875                  $tagfeed->export_for_template($OUTPUT));
3876  
3877          return new core_tag\output\tagindex($tag, 'core', 'course_modules', $content,
3878                  $exclusivemode, $fromcontextid, $contextid, $recursivecontext, $page, $totalpages);
3879      }
3880  }
3881  
3882  /**
3883   * Return an object with the list of navigation options in a course that are avaialable or not for the current user.
3884   * This function also handles the frontpage course.
3885   *
3886   * @param  stdClass $context context object (it can be a course context or the system context for frontpage settings)
3887   * @param  stdClass $course  the course where the settings are being rendered
3888   * @return stdClass          the navigation options in a course and their availability status
3889   * @since  Moodle 3.2
3890   */
3891  function course_get_user_navigation_options($context, $course = null) {
3892      global $CFG, $USER;
3893  
3894      $isloggedin = isloggedin();
3895      $isguestuser = isguestuser();
3896      $isfrontpage = $context->contextlevel == CONTEXT_SYSTEM;
3897  
3898      if ($isfrontpage) {
3899          $sitecontext = $context;
3900      } else {
3901          $sitecontext = context_system::instance();
3902      }
3903  
3904      // Sets defaults for all options.
3905      $options = (object) [
3906          'badges' => false,
3907          'blogs' => false,
3908          'competencies' => false,
3909          'grades' => false,
3910          'notes' => false,
3911          'participants' => false,
3912          'search' => false,
3913          'tags' => false,
3914      ];
3915  
3916      $options->blogs = !empty($CFG->enableblogs) &&
3917                          ($CFG->bloglevel == BLOG_GLOBAL_LEVEL ||
3918                          ($CFG->bloglevel == BLOG_SITE_LEVEL and ($isloggedin and !$isguestuser)))
3919                          && has_capability('moodle/blog:view', $sitecontext);
3920  
3921      $options->notes = !empty($CFG->enablenotes) && has_any_capability(array('moodle/notes:manage', 'moodle/notes:view'), $context);
3922  
3923      // Frontpage settings?
3924      if ($isfrontpage) {
3925          // We are on the front page, so make sure we use the proper capability (site:viewparticipants).
3926          $options->participants = course_can_view_participants($sitecontext);
3927          $options->badges = !empty($CFG->enablebadges) && has_capability('moodle/badges:viewbadges', $sitecontext);
3928          $options->tags = !empty($CFG->usetags) && $isloggedin;
3929          $options->search = !empty($CFG->enableglobalsearch) && has_capability('moodle/search:query', $sitecontext);
3930      } else {
3931          // We are in a course, so make sure we use the proper capability (course:viewparticipants).
3932          $options->participants = course_can_view_participants($context);
3933  
3934          // Only display badges if they are enabled and the current user can manage them or if they can view them and have,
3935          // at least, one available badge.
3936          if (!empty($CFG->enablebadges) && !empty($CFG->badges_allowcoursebadges)) {
3937              $canmanage = has_any_capability([
3938                      'moodle/badges:createbadge',
3939                      'moodle/badges:awardbadge',
3940                      'moodle/badges:configurecriteria',
3941                      'moodle/badges:configuremessages',
3942                      'moodle/badges:configuredetails',
3943                      'moodle/badges:deletebadge',
3944                  ],
3945                  $context
3946              );
3947              $totalbadges = [];
3948              $canview = false;
3949              if (!$canmanage) {
3950                  // This only needs to be calculated if the user can't manage badges (to improve performance).
3951                  $canview = has_capability('moodle/badges:viewbadges', $context);
3952                  if ($canview) {
3953                      require_once($CFG->dirroot.'/lib/badgeslib.php');
3954                      if (is_null($course)) {
3955                          $totalbadges = count(badges_get_badges(BADGE_TYPE_SITE, 0, '', '', 0, 0, $USER->id));
3956                      } else {
3957                          $totalbadges = count(badges_get_badges(BADGE_TYPE_COURSE, $course->id, '', '', 0, 0, $USER->id));
3958                      }
3959                  }
3960              }
3961  
3962              $options->badges = ($canmanage || ($canview && $totalbadges > 0));
3963          }
3964          // Add view grade report is permitted.
3965          $grades = false;
3966  
3967          if (has_capability('moodle/grade:viewall', $context)) {
3968              $grades = true;
3969          } else if (!empty($course->showgrades)) {
3970              $reports = core_component::get_plugin_list('gradereport');
3971              if (is_array($reports) && count($reports) > 0) {  // Get all installed reports.
3972                  arsort($reports);   // User is last, we want to test it first.
3973                  foreach ($reports as $plugin => $plugindir) {
3974                      if (has_capability('gradereport/'.$plugin.':view', $context)) {
3975                          // Stop when the first visible plugin is found.
3976                          $grades = true;
3977                          break;
3978                      }
3979                  }
3980              }
3981          }
3982          $options->grades = $grades;
3983      }
3984  
3985      if (\core_competency\api::is_enabled()) {
3986          $capabilities = array('moodle/competency:coursecompetencyview', 'moodle/competency:coursecompetencymanage');
3987          $options->competencies = has_any_capability($capabilities, $context);
3988      }
3989      return $options;
3990  }
3991  
3992  /**
3993   * Return an object with the list of administration options in a course that are available or not for the current user.
3994   * This function also handles the frontpage settings.
3995   *
3996   * @param  stdClass $course  course object (for frontpage it should be a clone of $SITE)
3997   * @param  stdClass $context context object (course context)
3998   * @return stdClass          the administration options in a course and their availability status
3999   * @since  Moodle 3.2
4000   */
4001  function course_get_user_administration_options($course, $context) {
4002      global $CFG;
4003      $isfrontpage = $course->id == SITEID;
4004      $completionenabled = $CFG->enablecompletion && $course->enablecompletion;
4005      $hascompletionoptions = count(core_completion\manager::get_available_completion_options($course->id)) > 0;
4006      $options = new stdClass;
4007      $options->update = has_capability('moodle/course:update', $context);
4008      $options->editcompletion = $CFG->enablecompletion && $course->enablecompletion &&
4009          ($options->update || $hascompletionoptions);
4010      $options->filters = has_capability('moodle/filter:manage', $context) &&
4011                          count(filter_get_available_in_context($context)) > 0;
4012      $options->reports = has_capability('moodle/site:viewreports', $context);
4013      $options->backup = has_capability('moodle/backup:backupcourse', $context);
4014      $options->restore = has_capability('moodle/restore:restorecourse', $context);
4015      $options->copy = \core_course\management\helper::can_copy_course($course->id);
4016      $options->files = ($course->legacyfiles == 2 && has_capability('moodle/course:managefiles', $context));
4017  
4018      if (!$isfrontpage) {
4019          $options->tags = has_capability('moodle/course:tag', $context);
4020          $options->gradebook = has_capability('moodle/grade:manage', $context);
4021          $options->outcomes = !empty($CFG->enableoutcomes) && has_capability('moodle/course:update', $context);
4022          $options->badges = !empty($CFG->enablebadges);
4023          $options->import = has_capability('moodle/restore:restoretargetimport', $context);
4024          $options->reset = has_capability('moodle/course:reset', $context);
4025          $options->roles = has_capability('moodle/role:switchroles', $context);
4026      } else {
4027          // Set default options to false.
4028          $listofoptions = array('tags', 'gradebook', 'outcomes', 'badges', 'import', 'publish', 'reset', 'roles', 'grades');
4029  
4030          foreach ($listofoptions as $option) {
4031              $options->$option = false;
4032          }
4033      }
4034  
4035      return $options;
4036  }
4037  
4038  /**
4039   * Validates course start and end dates.
4040   *
4041   * Checks that the end course date is not greater than the start course date.
4042   *
4043   * $coursedata['startdate'] or $coursedata['enddate'] may not be set, it depends on the form and user input.
4044   *
4045   * @param array $coursedata May contain startdate and enddate timestamps, depends on the user input.
4046   * @return mixed False if everything alright, error codes otherwise.
4047   */
4048  function course_validate_dates($coursedata) {
4049  
4050      // If both start and end dates are set end date should be later than the start date.
4051      if (!empty($coursedata['startdate']) && !empty($coursedata['enddate']) &&
4052              ($coursedata['enddate'] < $coursedata['startdate'])) {
4053          return 'enddatebeforestartdate';
4054      }
4055  
4056      // If start date is not set end date can not be set.
4057      if (empty($coursedata['startdate']) && !empty($coursedata['enddate'])) {
4058          return 'nostartdatenoenddate';
4059      }
4060  
4061      return false;
4062  }
4063  
4064  /**
4065   * Check for course updates in the given context level instances (only modules supported right Now)
4066   *
4067   * @param  stdClass $course  course object
4068   * @param  array $tocheck    instances to check for updates
4069   * @param  array $filter check only for updates in these areas
4070   * @return array list of warnings and instances with updates information
4071   * @since  Moodle 3.2
4072   */
4073  function course_check_updates($course, $tocheck, $filter = array()) {
4074      global $CFG, $DB;
4075  
4076      $instances = array();
4077      $warnings = array();
4078      $modulescallbacksupport = array();
4079      $modinfo = get_fast_modinfo($course);
4080  
4081      $supportedplugins = get_plugin_list_with_function('mod', 'check_updates_since');
4082  
4083      // Check instances.
4084      foreach ($tocheck as $instance) {
4085          if ($instance['contextlevel'] == 'module') {
4086              // Check module visibility.
4087              try {
4088                  $cm = $modinfo->get_cm($instance['id']);
4089              } catch (Exception $e) {
4090                  $warnings[] = array(
4091                      'item' => 'module',
4092                      'itemid' => $instance['id'],
4093                      'warningcode' => 'cmidnotincourse',
4094                      'message' => 'This module id does not belong to this course.'
4095                  );
4096                  continue;
4097              }
4098  
4099              if (!$cm->uservisible) {
4100                  $warnings[] = array(
4101                      'item' => 'module',
4102                      'itemid' => $instance['id'],
4103                      'warningcode' => 'nonuservisible',
4104                      'message' => 'You don\'t have access to this module.'
4105                  );
4106                  continue;
4107              }
4108              if (empty($supportedplugins['mod_' . $cm->modname])) {
4109                  $warnings[] = array(
4110                      'item' => 'module',
4111                      'itemid' => $instance['id'],
4112                      'warningcode' => 'missingcallback',
4113                      'message' => 'This module does not implement the check_updates_since callback: ' . $instance['contextlevel'],
4114                  );
4115                  continue;
4116              }
4117              // Retrieve the module instance.
4118              $instances[] = array(
4119                  'contextlevel' => $instance['contextlevel'],
4120                  'id' => $instance['id'],
4121                  'updates' => call_user_func($cm->modname . '_check_updates_since', $cm, $instance['since'], $filter)
4122              );
4123  
4124          } else {
4125              $warnings[] = array(
4126                  'item' => 'contextlevel',
4127                  'itemid' => $instance['id'],
4128                  'warningcode' => 'contextlevelnotsupported',
4129                  'message' => 'Context level not yet supported ' . $instance['contextlevel'],
4130              );
4131          }
4132      }
4133      return array($instances, $warnings);
4134  }
4135  
4136  /**
4137   * This function classifies a course as past, in progress or future.
4138   *
4139   * This function may incur a DB hit to calculate course completion.
4140   * @param stdClass $course Course record
4141   * @param stdClass $user User record (optional - defaults to $USER).
4142   * @param completion_info $completioninfo Completion record for the user (optional - will be fetched if required).
4143   * @return string (one of COURSE_TIMELINE_FUTURE, COURSE_TIMELINE_INPROGRESS or COURSE_TIMELINE_PAST)
4144   */
4145  function course_classify_for_timeline($course, $user = null, $completioninfo = null) {
4146      global $USER;
4147  
4148      if ($user == null) {
4149          $user = $USER;
4150      }
4151  
4152      if ($completioninfo == null) {
4153          $completioninfo = new completion_info($course);
4154      }
4155  
4156      // Let plugins override data for timeline classification.
4157      $pluginsfunction = get_plugins_with_function('extend_course_classify_for_timeline', 'lib.php');
4158      foreach ($pluginsfunction as $plugintype => $plugins) {
4159          foreach ($plugins as $pluginfunction) {
4160              $pluginfunction($course, $user, $completioninfo);
4161          }
4162      }
4163  
4164      $today = time();
4165      // End date past.
4166      if (!empty($course->enddate) && (course_classify_end_date($course) < $today)) {
4167          return COURSE_TIMELINE_PAST;
4168      }
4169  
4170      // Course was completed.
4171      if ($completioninfo->is_enabled() && $completioninfo->is_course_complete($user->id)) {
4172          return COURSE_TIMELINE_PAST;
4173      }
4174  
4175      // Start date not reached.
4176      if (!empty($course->startdate) && (course_classify_start_date($course) > $today)) {
4177          return COURSE_TIMELINE_FUTURE;
4178      }
4179  
4180      // Everything else is in progress.
4181      return COURSE_TIMELINE_INPROGRESS;
4182  }
4183  
4184  /**
4185   * This function calculates the end date to use for display classification purposes,
4186   * incorporating the grace period, if any.
4187   *
4188   * @param stdClass $course The course record.
4189   * @return int The new enddate.
4190   */
4191  function course_classify_end_date($course) {
4192      global $CFG;
4193      $coursegraceperiodafter = (empty($CFG->coursegraceperiodafter)) ? 0 : $CFG->coursegraceperiodafter;
4194      $enddate = (new \DateTimeImmutable())->setTimestamp($course->enddate)->modify("+{$coursegraceperiodafter} days");
4195      return $enddate->getTimestamp();
4196  }
4197  
4198  /**
4199   * This function calculates the start date to use for display classification purposes,
4200   * incorporating the grace period, if any.
4201   *
4202   * @param stdClass $course The course record.
4203   * @return int The new startdate.
4204   */
4205  function course_classify_start_date($course) {
4206      global $CFG;
4207      $coursegraceperiodbefore = (empty($CFG->coursegraceperiodbefore)) ? 0 : $CFG->coursegraceperiodbefore;
4208      $startdate = (new \DateTimeImmutable())->setTimestamp($course->startdate)->modify("-{$coursegraceperiodbefore} days");
4209      return $startdate->getTimestamp();
4210  }
4211  
4212  /**
4213   * Group a list of courses into either past, future, or in progress.
4214   *
4215   * The return value will be an array indexed by the COURSE_TIMELINE_* constants
4216   * with each value being an array of courses in that group.
4217   * E.g.
4218   * [
4219   *      COURSE_TIMELINE_PAST => [... list of past courses ...],
4220   *      COURSE_TIMELINE_FUTURE => [],
4221   *      COURSE_TIMELINE_INPROGRESS => []
4222   * ]
4223   *
4224   * @param array $courses List of courses to be grouped.
4225   * @return array
4226   */
4227  function course_classify_courses_for_timeline(array $courses) {
4228      return array_reduce($courses, function($carry, $course) {
4229          $classification = course_classify_for_timeline($course);
4230          array_push($carry[$classification], $course);
4231  
4232          return $carry;
4233      }, [
4234          COURSE_TIMELINE_PAST => [],
4235          COURSE_TIMELINE_FUTURE => [],
4236          COURSE_TIMELINE_INPROGRESS => []
4237      ]);
4238  }
4239  
4240  /**
4241   * Get the list of enrolled courses for the current user.
4242   *
4243   * This function returns a Generator. The courses will be loaded from the database
4244   * in chunks rather than a single query.
4245   *
4246   * @param int $limit Restrict result set to this amount
4247   * @param int $offset Skip this number of records from the start of the result set
4248   * @param string|null $sort SQL string for sorting
4249   * @param string|null $fields SQL string for fields to be returned
4250   * @param int $dbquerylimit The number of records to load per DB request
4251   * @param array $includecourses courses ids to be restricted
4252   * @param array $hiddencourses courses ids to be excluded
4253   * @return Generator
4254   */
4255  function course_get_enrolled_courses_for_logged_in_user(
4256      int $limit = 0,
4257      int $offset = 0,
4258      string $sort = null,
4259      string $fields = null,
4260      int $dbquerylimit = COURSE_DB_QUERY_LIMIT,
4261      array $includecourses = [],
4262      array $hiddencourses = []
4263  ) : Generator {
4264  
4265      $haslimit = !empty($limit);
4266      $recordsloaded = 0;
4267      $querylimit = (!$haslimit || $limit > $dbquerylimit) ? $dbquerylimit : $limit;
4268  
4269      while ($courses = enrol_get_my_courses($fields, $sort, $querylimit, $includecourses, false, $offset, $hiddencourses)) {
4270          yield from $courses;
4271  
4272          $recordsloaded += $querylimit;
4273  
4274          if (count($courses) < $querylimit) {
4275              break;
4276          }
4277          if ($haslimit && $recordsloaded >= $limit) {
4278              break;
4279          }
4280  
4281          $offset += $querylimit;
4282      }
4283  }
4284  
4285  /**
4286   * Get the list of enrolled courses the current user searched for.
4287   *
4288   * This function returns a Generator. The courses will be loaded from the database
4289   * in chunks rather than a single query.
4290   *
4291   * @param int $limit Restrict result set to this amount
4292   * @param int $offset Skip this number of records from the start of the result set
4293   * @param string|null $sort SQL string for sorting
4294   * @param string|null $fields SQL string for fields to be returned
4295   * @param int $dbquerylimit The number of records to load per DB request
4296   * @param array $searchcriteria contains search criteria
4297   * @param array $options display options, same as in get_courses() except 'recursive' is ignored -
4298   *                       search is always category-independent
4299   * @return Generator
4300   */
4301  function course_get_enrolled_courses_for_logged_in_user_from_search(
4302      int $limit = 0,
4303      int $offset = 0,
4304      string $sort = null,
4305      string $fields = null,
4306      int $dbquerylimit = COURSE_DB_QUERY_LIMIT,
4307      array $searchcriteria = [],
4308      array $options = []
4309  ) : Generator {
4310  
4311      $haslimit = !empty($limit);
4312      $recordsloaded = 0;
4313      $querylimit = (!$haslimit || $limit > $dbquerylimit) ? $dbquerylimit : $limit;
4314      $ids = core_course_category::search_courses($searchcriteria, $options);
4315  
4316      // If no courses were found matching the criteria return back.
4317      if (empty($ids)) {
4318          return;
4319      }
4320  
4321      while ($courses = enrol_get_my_courses($fields, $sort, $querylimit, $ids, false, $offset)) {
4322          yield from $courses;
4323  
4324          $recordsloaded += $querylimit;
4325  
4326          if (count($courses) < $querylimit) {
4327              break;
4328          }
4329          if ($haslimit && $recordsloaded >= $limit) {
4330              break;
4331          }
4332  
4333          $offset += $querylimit;
4334      }
4335  }
4336  
4337  /**
4338   * Search the given $courses for any that match the given $classification up to the specified
4339   * $limit.
4340   *
4341   * This function will return the subset of courses that match the classification as well as the
4342   * number of courses it had to process to build that subset.
4343   *
4344   * It is recommended that for larger sets of courses this function is given a Generator that loads
4345   * the courses from the database in chunks.
4346   *
4347   * @param array|Traversable $courses List of courses to process
4348   * @param string $classification One of the COURSE_TIMELINE_* constants
4349   * @param int $limit Limit the number of results to this amount
4350   * @return array First value is the filtered courses, second value is the number of courses processed
4351   */
4352  function course_filter_courses_by_timeline_classification(
4353      $courses,
4354      string $classification,
4355      int $limit = 0
4356  ) : array {
4357  
4358      if (!in_array($classification,
4359              [COURSE_TIMELINE_ALLINCLUDINGHIDDEN, COURSE_TIMELINE_ALL, COURSE_TIMELINE_PAST, COURSE_TIMELINE_INPROGRESS,
4360                  COURSE_TIMELINE_FUTURE, COURSE_TIMELINE_HIDDEN, COURSE_TIMELINE_SEARCH])) {
4361          $message = 'Classification must be one of COURSE_TIMELINE_ALLINCLUDINGHIDDEN, COURSE_TIMELINE_ALL, COURSE_TIMELINE_PAST, '
4362              . 'COURSE_TIMELINE_INPROGRESS, COURSE_TIMELINE_SEARCH or COURSE_TIMELINE_FUTURE';
4363          throw new moodle_exception($message);
4364      }
4365  
4366      $filteredcourses = [];
4367      $numberofcoursesprocessed = 0;
4368      $filtermatches = 0;
4369  
4370      foreach ($courses as $course) {
4371          $numberofcoursesprocessed++;
4372          $pref = get_user_preferences('block_myoverview_hidden_course_' . $course->id, 0);
4373  
4374          // Added as of MDL-63457 toggle viewability for each user.
4375          if ($classification == COURSE_TIMELINE_ALLINCLUDINGHIDDEN || ($classification == COURSE_TIMELINE_HIDDEN && $pref) ||
4376              $classification == COURSE_TIMELINE_SEARCH||
4377              (($classification == COURSE_TIMELINE_ALL || $classification == course_classify_for_timeline($course)) && !$pref)) {
4378              $filteredcourses[] = $course;
4379              $filtermatches++;
4380          }
4381  
4382          if ($limit && $filtermatches >= $limit) {
4383              // We've found the number of requested courses. No need to continue searching.
4384              break;
4385          }
4386      }
4387  
4388      // Return the number of filtered courses as well as the number of courses that were searched
4389      // in order to find the matching courses. This allows the calling code to do some kind of
4390      // pagination.
4391      return [$filteredcourses, $numberofcoursesprocessed];
4392  }
4393  
4394  /**
4395   * Search the given $courses for any that match the given $classification up to the specified
4396   * $limit.
4397   *
4398   * This function will return the subset of courses that are favourites as well as the
4399   * number of courses it had to process to build that subset.
4400   *
4401   * It is recommended that for larger sets of courses this function is given a Generator that loads
4402   * the courses from the database in chunks.
4403   *
4404   * @param array|Traversable $courses List of courses to process
4405   * @param array $favouritecourseids Array of favourite courses.
4406   * @param int $limit Limit the number of results to this amount
4407   * @return array First value is the filtered courses, second value is the number of courses processed
4408   */
4409  function course_filter_courses_by_favourites(
4410      $courses,
4411      $favouritecourseids,
4412      int $limit = 0
4413  ) : array {
4414  
4415      $filteredcourses = [];
4416      $numberofcoursesprocessed = 0;
4417      $filtermatches = 0;
4418  
4419      foreach ($courses as $course) {
4420          $numberofcoursesprocessed++;
4421  
4422          if (in_array($course->id, $favouritecourseids)) {
4423              $filteredcourses[] = $course;
4424              $filtermatches++;
4425          }
4426  
4427          if ($limit && $filtermatches >= $limit) {
4428              // We've found the number of requested courses. No need to continue searching.
4429              break;
4430          }
4431      }
4432  
4433      // Return the number of filtered courses as well as the number of courses that were searched
4434      // in order to find the matching courses. This allows the calling code to do some kind of
4435      // pagination.
4436      return [$filteredcourses, $numberofcoursesprocessed];
4437  }
4438  
4439  /**
4440   * Search the given $courses for any that have a $customfieldname value that matches the given
4441   * $customfieldvalue, up to the specified $limit.
4442   *
4443   * This function will return the subset of courses that matches the value as well as the
4444   * number of courses it had to process to build that subset.
4445   *
4446   * It is recommended that for larger sets of courses this function is given a Generator that loads
4447   * the courses from the database in chunks.
4448   *
4449   * @param array|Traversable $courses List of courses to process
4450   * @param string $customfieldname the shortname of the custom field to match against
4451   * @param string $customfieldvalue the value this custom field needs to match
4452   * @param int $limit Limit the number of results to this amount
4453   * @return array First value is the filtered courses, second value is the number of courses processed
4454   */
4455  function course_filter_courses_by_customfield(
4456      $courses,
4457      $customfieldname,
4458      $customfieldvalue,
4459      int $limit = 0
4460  ) : array {
4461      global $DB;
4462  
4463      if (!$courses) {
4464          return [[], 0];
4465      }
4466  
4467      // Prepare the list of courses to search through.
4468      $coursesbyid = [];
4469      foreach ($courses as $course) {
4470          $coursesbyid[$course->id] = $course;
4471      }
4472      if (!$coursesbyid) {
4473          return [[], 0];
4474      }
4475      list($csql, $params) = $DB->get_in_or_equal(array_keys($coursesbyid), SQL_PARAMS_NAMED);
4476  
4477      // Get the id of the custom field.
4478      $sql = "
4479         SELECT f.id
4480           FROM {customfield_field} f
4481           JOIN {customfield_category} cat ON cat.id = f.categoryid
4482          WHERE f.shortname = ?
4483            AND cat.component = 'core_course'
4484            AND cat.area = 'course'
4485      ";
4486      $fieldid = $DB->get_field_sql($sql, [$customfieldname]);
4487      if (!$fieldid) {
4488          return [[], 0];
4489      }
4490  
4491      // Get a list of courseids that match that custom field value.
4492      if ($customfieldvalue == COURSE_CUSTOMFIELD_EMPTY) {
4493          $comparevalue = $DB->sql_compare_text('cd.value');
4494          $sql = "
4495             SELECT c.id
4496               FROM {course} c
4497          LEFT JOIN {customfield_data} cd ON cd.instanceid = c.id AND cd.fieldid = :fieldid
4498              WHERE c.id $csql
4499                AND (cd.value IS NULL OR $comparevalue = '' OR $comparevalue = '0')
4500          ";
4501          $params['fieldid'] = $fieldid;
4502          $matchcourseids = $DB->get_fieldset_sql($sql, $params);
4503      } else {
4504          $comparevalue = $DB->sql_compare_text('value');
4505          $select = "fieldid = :fieldid AND $comparevalue = :customfieldvalue AND instanceid $csql";
4506          $params['fieldid'] = $fieldid;
4507          $params['customfieldvalue'] = $customfieldvalue;
4508          $matchcourseids = $DB->get_fieldset_select('customfield_data', 'instanceid', $select, $params);
4509      }
4510  
4511      // Prepare the list of courses to return.
4512      $filteredcourses = [];
4513      $numberofcoursesprocessed = 0;
4514      $filtermatches = 0;
4515  
4516      foreach ($coursesbyid as $course) {
4517          $numberofcoursesprocessed++;
4518  
4519          if (in_array($course->id, $matchcourseids)) {
4520              $filteredcourses[] = $course;
4521              $filtermatches++;
4522          }
4523  
4524          if ($limit && $filtermatches >= $limit) {
4525              // We've found the number of requested courses. No need to continue searching.
4526              break;
4527          }
4528      }
4529  
4530      // Return the number of filtered courses as well as the number of courses that were searched
4531      // in order to find the matching courses. This allows the calling code to do some kind of
4532      // pagination.
4533      return [$filteredcourses, $numberofcoursesprocessed];
4534  }
4535  
4536  /**
4537   * Check module updates since a given time.
4538   * This function checks for updates in the module config, file areas, completion, grades, comments and ratings.
4539   *
4540   * @param  cm_info $cm        course module data
4541   * @param  int $from          the time to check
4542   * @param  array $fileareas   additional file ares to check
4543   * @param  array $filter      if we need to filter and return only selected updates
4544   * @return stdClass object with the different updates
4545   * @since  Moodle 3.2
4546   */
4547  function course_check_module_updates_since($cm, $from, $fileareas = array(), $filter = array()) {
4548      global $DB, $CFG, $USER;
4549  
4550      $context = $cm->context;
4551      $mod = $DB->get_record($cm->modname, array('id' => $cm->instance), '*', MUST_EXIST);
4552  
4553      $updates = new stdClass();
4554      $course = get_course($cm->course);
4555      $component = 'mod_' . $cm->modname;
4556  
4557      // Check changes in the module configuration.
4558      if (isset($mod->timemodified) and (empty($filter) or in_array('configuration', $filter))) {
4559          $updates->configuration = (object) array('updated' => false);
4560          if ($updates->configuration->updated = $mod->timemodified > $from) {
4561              $updates->configuration->timeupdated = $mod->timemodified;
4562          }
4563      }
4564  
4565      // Check for updates in files.
4566      if (plugin_supports('mod', $cm->modname, FEATURE_MOD_INTRO)) {
4567          $fileareas[] = 'intro';
4568      }
4569      if (!empty($fileareas) and (empty($filter) or in_array('fileareas', $filter))) {
4570          $fs = get_file_storage();
4571          $files = $fs->get_area_files($context->id, $component, $fileareas, false, "filearea, timemodified DESC", false, $from);
4572          foreach ($fileareas as $filearea) {
4573              $updates->{$filearea . 'files'} = (object) array('updated' => false);
4574          }
4575          foreach ($files as $file) {
4576              $updates->{$file->get_filearea() . 'files'}->updated = true;
4577              $updates->{$file->get_filearea() . 'files'}->itemids[] = $file->get_id();
4578          }
4579      }
4580  
4581      // Check completion.
4582      $supportcompletion = plugin_supports('mod', $cm->modname, FEATURE_COMPLETION_HAS_RULES);
4583      $supportcompletion = $supportcompletion or plugin_supports('mod', $cm->modname, FEATURE_COMPLETION_TRACKS_VIEWS);
4584      if ($supportcompletion and (empty($filter) or in_array('completion', $filter))) {
4585          $updates->completion = (object) array('updated' => false);
4586          $completion = new completion_info($course);
4587          // Use wholecourse to cache all the modules the first time.
4588          $completiondata = $completion->get_data($cm, true);
4589          if ($updates->completion->updated = !empty($completiondata->timemodified) && $completiondata->timemodified > $from) {
4590              $updates->completion->timemodified = $completiondata->timemodified;
4591          }
4592      }
4593  
4594      // Check grades.
4595      $supportgrades = plugin_supports('mod', $cm->modname, FEATURE_GRADE_HAS_GRADE);
4596      $supportgrades = $supportgrades or plugin_supports('mod', $cm->modname, FEATURE_GRADE_OUTCOMES);
4597      if ($supportgrades and (empty($filter) or (in_array('gradeitems', $filter) or in_array('outcomes', $filter)))) {
4598          require_once($CFG->libdir . '/gradelib.php');
4599          $grades = grade_get_grades($course->id, 'mod', $cm->modname, $mod->id, $USER->id);
4600  
4601          if (empty($filter) or in_array('gradeitems', $filter)) {
4602              $updates->gradeitems = (object) array('updated' => false);
4603              foreach ($grades->items as $gradeitem) {
4604                  foreach ($gradeitem->grades as $grade) {
4605                      if ($grade->datesubmitted > $from or $grade->dategraded > $from) {
4606                          $updates->gradeitems->updated = true;
4607                          $updates->gradeitems->itemids[] = $gradeitem->id;
4608                      }
4609                  }
4610              }
4611          }
4612  
4613          if (empty($filter) or in_array('outcomes', $filter)) {
4614              $updates->outcomes = (object) array('updated' => false);
4615              foreach ($grades->outcomes as $outcome) {
4616                  foreach ($outcome->grades as $grade) {
4617                      if ($grade->datesubmitted > $from or $grade->dategraded > $from) {
4618                          $updates->outcomes->updated = true;
4619                          $updates->outcomes->itemids[] = $outcome->id;
4620                      }
4621                  }
4622              }
4623          }
4624      }
4625  
4626      // Check comments.
4627      if (plugin_supports('mod', $cm->modname, FEATURE_COMMENT) and (empty($filter) or in_array('comments', $filter))) {
4628          $updates->comments = (object) array('updated' => false);
4629          require_once($CFG->dirroot . '/comment/lib.php');
4630          require_once($CFG->dirroot . '/comment/locallib.php');
4631          $manager = new comment_manager();
4632          $comments = $manager->get_component_comments_since($course, $context, $component, $from, $cm);
4633          if (!empty($comments)) {
4634              $updates->comments->updated = true;
4635              $updates->comments->itemids = array_keys($comments);
4636          }
4637      }
4638  
4639      // Check ratings.
4640      if (plugin_supports('mod', $cm->modname, FEATURE_RATE) and (empty($filter) or in_array('ratings', $filter))) {
4641          $updates->ratings = (object) array('updated' => false);
4642          require_once($CFG->dirroot . '/rating/lib.php');
4643          $manager = new rating_manager();
4644          $ratings = $manager->get_component_ratings_since($context, $component, $from);
4645          if (!empty($ratings)) {
4646              $updates->ratings->updated = true;
4647              $updates->ratings->itemids = array_keys($ratings);
4648          }
4649      }
4650  
4651      return $updates;
4652  }
4653  
4654  /**
4655   * Returns true if the user can view the participant page, false otherwise,
4656   *
4657   * @param context $context The context we are checking.
4658   * @return bool
4659   */
4660  function course_can_view_participants($context) {
4661      $viewparticipantscap = 'moodle/course:viewparticipants';
4662      if ($context->contextlevel == CONTEXT_SYSTEM) {
4663          $viewparticipantscap = 'moodle/site:viewparticipants';
4664      }
4665  
4666      return has_any_capability([$viewparticipantscap, 'moodle/course:enrolreview'], $context);
4667  }
4668  
4669  /**
4670   * Checks if a user can view the participant page, if not throws an exception.
4671   *
4672   * @param context $context The context we are checking.
4673   * @throws required_capability_exception
4674   */
4675  function course_require_view_participants($context) {
4676      if (!course_can_view_participants($context)) {
4677          $viewparticipantscap = 'moodle/course:viewparticipants';
4678          if ($context->contextlevel == CONTEXT_SYSTEM) {
4679              $viewparticipantscap = 'moodle/site:viewparticipants';
4680          }
4681          throw new required_capability_exception($context, $viewparticipantscap, 'nopermissions', '');
4682      }
4683  }
4684  
4685  /**
4686   * Return whether the user can download from the specified backup file area in the given context.
4687   *
4688   * @param string $filearea the backup file area. E.g. 'course', 'backup' or 'automated'.
4689   * @param \context $context
4690   * @param stdClass $user the user object. If not provided, the current user will be checked.
4691   * @return bool true if the user is allowed to download in the context, false otherwise.
4692   */
4693  function can_download_from_backup_filearea($filearea, \context $context, stdClass $user = null) {
4694      $candownload = false;
4695      switch ($filearea) {
4696          case 'course':
4697          case 'backup':
4698              $candownload = has_capability('moodle/backup:downloadfile', $context, $user);
4699              break;
4700          case 'automated':
4701              // Given the automated backups may contain userinfo, we restrict access such that only users who are able to
4702              // restore with userinfo are able to download the file. Users can't create these backups, so checking 'backup:userinfo'
4703              // doesn't make sense here.
4704              $candownload = has_capability('moodle/backup:downloadfile', $context, $user) &&
4705                             has_capability('moodle/restore:userinfo', $context, $user);
4706              break;
4707          default:
4708              break;
4709  
4710      }
4711      return $candownload;
4712  }
4713  
4714  /**
4715   * Get a list of hidden courses
4716   *
4717   * @param int|object|null $user User override to get the filter from. Defaults to current user
4718   * @return array $ids List of hidden courses
4719   * @throws coding_exception
4720   */
4721  function get_hidden_courses_on_timeline($user = null) {
4722      global $USER;
4723  
4724      if (empty($user)) {
4725          $user = $USER->id;
4726      }
4727  
4728      $preferences = get_user_preferences(null, null, $user);
4729      $ids = [];
4730      foreach ($preferences as $key => $value) {
4731          if (preg_match('/block_myoverview_hidden_course_(\d)+/', $key)) {
4732              $id = preg_split('/block_myoverview_hidden_course_/', $key);
4733              $ids[] = $id[1];
4734          }
4735      }
4736  
4737      return $ids;
4738  }
4739  
4740  /**
4741   * Returns a list of the most recently courses accessed by a user
4742   *
4743   * @param int $userid User id from which the courses will be obtained
4744   * @param int $limit Restrict result set to this amount
4745   * @param int $offset Skip this number of records from the start of the result set
4746   * @param string|null $sort SQL string for sorting
4747   * @return array
4748   */
4749  function course_get_recent_courses(int $userid = null, int $limit = 0, int $offset = 0, string $sort = null) {
4750  
4751      global $CFG, $USER, $DB;
4752  
4753      if (empty($userid)) {
4754          $userid = $USER->id;
4755      }
4756  
4757      $basefields = [
4758          'id', 'idnumber', 'summary', 'summaryformat', 'startdate', 'enddate', 'category',
4759          'shortname', 'fullname', 'timeaccess', 'component', 'visible',
4760          'showactivitydates', 'showcompletionconditions',
4761      ];
4762  
4763      if (empty($sort)) {
4764          $sort = 'timeaccess DESC';
4765      } else {
4766          // The SQL string for sorting can define sorting by multiple columns.
4767          $rawsorts = explode(',', $sort);
4768          $sorts = array();
4769          // Validate and trim the sort parameters in the SQL string for sorting.
4770          foreach ($rawsorts as $rawsort) {
4771              $sort = trim($rawsort);
4772              $sortparams = explode(' ', $sort);
4773              // A valid sort statement can not have more than 2 params (ex. 'summary desc' or 'timeaccess').
4774              if (count($sortparams) > 2) {
4775                  throw new invalid_parameter_exception(
4776                      'Invalid structure of the sort parameter, allowed structure: fieldname [ASC|DESC].');
4777              }
4778              $sortfield = trim($sortparams[0]);
4779              // Validate the value which defines the field to sort by.
4780              if (!in_array($sortfield, $basefields)) {
4781                  throw new invalid_parameter_exception('Invalid field in the sort parameter, allowed fields: ' .
4782                      implode(', ', $basefields) . '.');
4783              }
4784              $sortdirection = isset($sortparams[1]) ? trim($sortparams[1]) : '';
4785              // Validate the value which defines the sort direction (if present).
4786              $allowedsortdirections = ['asc', 'desc'];
4787              if (!empty($sortdirection) && !in_array(strtolower($sortdirection), $allowedsortdirections)) {
4788                  throw new invalid_parameter_exception('Invalid sort direction in the sort parameter, allowed values: ' .
4789                      implode(', ', $allowedsortdirections) . '.');
4790              }
4791              $sorts[] = $sort;
4792          }
4793          $sort = implode(',', $sorts);
4794      }
4795  
4796      $ctxfields = context_helper::get_preload_record_columns_sql('ctx');
4797  
4798      $coursefields = 'c.' . join(',', $basefields);
4799  
4800      // Ask the favourites service to give us the join SQL for favourited courses,
4801      // so we can include favourite information in the query.
4802      $usercontext = \context_user::instance($userid);
4803      $favservice = \core_favourites\service_factory::get_service_for_user_context($usercontext);
4804      list($favsql, $favparams) = $favservice->get_join_sql_by_type('core_course', 'courses', 'fav', 'ul.courseid');
4805  
4806      $sql = "SELECT $coursefields, $ctxfields
4807                FROM {course} c
4808                JOIN {context} ctx
4809                     ON ctx.contextlevel = :contextlevel
4810                     AND ctx.instanceid = c.id
4811                JOIN {user_lastaccess} ul
4812                     ON ul.courseid = c.id
4813              $favsql
4814           LEFT JOIN {enrol} eg ON eg.courseid = c.id AND eg.status = :statusenrolg AND eg.enrol = :guestenrol
4815               WHERE ul.userid = :userid
4816                 AND c.visible = :visible
4817                 AND (eg.id IS NOT NULL
4818                      OR EXISTS (SELECT e.id
4819                               FROM {enrol} e
4820                               JOIN {user_enrolments} ue ON ue.enrolid = e.id
4821                              WHERE e.courseid = c.id
4822                                AND e.status = :statusenrol
4823                                AND ue.status = :status
4824                                AND ue.userid = :userid2
4825                                AND ue.timestart < :now1
4826                                AND (ue.timeend = 0 OR ue.timeend > :now2)
4827                            ))
4828            ORDER BY $sort";
4829  
4830      $now = round(time(), -2); // Improves db caching.
4831      $params = ['userid' => $userid, 'contextlevel' => CONTEXT_COURSE, 'visible' => 1, 'status' => ENROL_USER_ACTIVE,
4832                 'statusenrol' => ENROL_INSTANCE_ENABLED, 'guestenrol' => 'guest', 'now1' => $now, 'now2' => $now,
4833                 'userid2' => $userid, 'statusenrolg' => ENROL_INSTANCE_ENABLED] + $favparams;
4834  
4835      $recentcourses = $DB->get_records_sql($sql, $params, $offset, $limit);
4836  
4837      // Filter courses if last access field is hidden.
4838      $hiddenfields = array_flip(explode(',', $CFG->hiddenuserfields));
4839  
4840      if ($userid != $USER->id && isset($hiddenfields['lastaccess'])) {
4841          $recentcourses = array_filter($recentcourses, function($course) {
4842              context_helper::preload_from_record($course);
4843              $context = context_course::instance($course->id, IGNORE_MISSING);
4844              // If last access was a hidden field, a user requesting info about another user would need permission to view hidden
4845              // fields.
4846              return has_capability('moodle/course:viewhiddenuserfields', $context);
4847          });
4848      }
4849  
4850      return $recentcourses;
4851  }
4852  
4853  /**
4854   * Calculate the course start date and offset for the given user ids.
4855   *
4856   * If the course is a fixed date course then the course start date will be returned.
4857   * If the course is a relative date course then the course date will be calculated and
4858   * and offset provided.
4859   *
4860   * The dates are returned as an array with the index being the user id. The array
4861   * contains the start date and start offset values for the user.
4862   *
4863   * If the user is not enrolled in the course then the course start date will be returned.
4864   *
4865   * If we have a course which starts on 1563244000 and 2 users, id 123 and 456, where the
4866   * former is enrolled in the course at 1563244693 and the latter is not enrolled then the
4867   * return value would look like:
4868   * [
4869   *      '123' => [
4870   *          'start' => 1563244693,
4871   *          'startoffset' => 693
4872   *      ],
4873   *      '456' => [
4874   *          'start' => 1563244000,
4875   *          'startoffset' => 0
4876   *      ]
4877   * ]
4878   *
4879   * @param stdClass $course The course to fetch dates for.
4880   * @param array $userids The list of user ids to get dates for.
4881   * @return array
4882   */
4883  function course_get_course_dates_for_user_ids(stdClass $course, array $userids): array {
4884      if (empty($course->relativedatesmode)) {
4885          // This course isn't set to relative dates so we can early return with the course
4886          // start date.
4887          return array_reduce($userids, function($carry, $userid) use ($course) {
4888              $carry[$userid] = [
4889                  'start' => $course->startdate,
4890                  'startoffset' => 0
4891              ];
4892              return $carry;
4893          }, []);
4894      }
4895  
4896      // We're dealing with a relative dates course now so we need to calculate some dates.
4897      $cache = cache::make('core', 'course_user_dates');
4898      $dates = [];
4899      $uncacheduserids = [];
4900  
4901      // Try fetching the values from the cache so that we don't need to do a DB request.
4902      foreach ($userids as $userid) {
4903          $cachekey = "{$course->id}_{$userid}";
4904          $cachedvalue = $cache->get($cachekey);
4905  
4906          if ($cachedvalue === false) {
4907              // Looks like we haven't seen this user for this course before so we'll have
4908              // to fetch it.
4909              $uncacheduserids[] = $userid;
4910          } else {
4911              [$start, $startoffset] = $cachedvalue;
4912              $dates[$userid] = [
4913                  'start' => $start,
4914                  'startoffset' => $startoffset
4915              ];
4916          }
4917      }
4918  
4919      if (!empty($uncacheduserids)) {
4920          // Load the enrolments for any users we haven't seen yet. Set the "onlyactive" param
4921          // to false because it filters out users with enrolment start times in the future which
4922          // we don't want.
4923          $enrolments = enrol_get_course_users($course->id, false, $uncacheduserids);
4924  
4925          foreach ($uncacheduserids as $userid) {
4926              // Find the user enrolment that has the earliest start date.
4927              $enrolment = array_reduce(array_values($enrolments), function($carry, $enrolment) use ($userid) {
4928                  // Only consider enrolments for this user if the user enrolment is active and the
4929                  // enrolment method is enabled.
4930                  if (
4931                      $enrolment->uestatus == ENROL_USER_ACTIVE &&
4932                      $enrolment->estatus == ENROL_INSTANCE_ENABLED &&
4933                      $enrolment->id == $userid
4934                  ) {
4935                      if (is_null($carry)) {
4936                          // Haven't found an enrolment yet for this user so use the one we just found.
4937                          $carry = $enrolment;
4938                      } else {
4939                          // We've already found an enrolment for this user so let's use which ever one
4940                          // has the earliest start time.
4941                          $carry = $carry->uetimestart < $enrolment->uetimestart ? $carry : $enrolment;
4942                      }
4943                  }
4944  
4945                  return $carry;
4946              }, null);
4947  
4948              if ($enrolment) {
4949                  // The course is in relative dates mode so we calculate the student's start
4950                  // date based on their enrolment start date.
4951                  $start = $course->startdate > $enrolment->uetimestart ? $course->startdate : $enrolment->uetimestart;
4952                  $startoffset = $start - $course->startdate;
4953              } else {
4954                  // The user is not enrolled in the course so default back to the course start date.
4955                  $start = $course->startdate;
4956                  $startoffset = 0;
4957              }
4958  
4959              $dates[$userid] = [
4960                  'start' => $start,
4961                  'startoffset' => $startoffset
4962              ];
4963  
4964              $cachekey = "{$course->id}_{$userid}";
4965              $cache->set($cachekey, [$start, $startoffset]);
4966          }
4967      }
4968  
4969      return $dates;
4970  }
4971  
4972  /**
4973   * Calculate the course start date and offset for the given user id.
4974   *
4975   * If the course is a fixed date course then the course start date will be returned.
4976   * If the course is a relative date course then the course date will be calculated and
4977   * and offset provided.
4978   *
4979   * The return array contains the start date and start offset values for the user.
4980   *
4981   * If the user is not enrolled in the course then the course start date will be returned.
4982   *
4983   * If we have a course which starts on 1563244000. If a user's enrolment starts on 1563244693
4984   * then the return would be:
4985   * [
4986   *      'start' => 1563244693,
4987   *      'startoffset' => 693
4988   * ]
4989   *
4990   * If the use was not enrolled then the return would be:
4991   * [
4992   *      'start' => 1563244000,
4993   *      'startoffset' => 0
4994   * ]
4995   *
4996   * @param stdClass $course The course to fetch dates for.
4997   * @param int $userid The user id to get dates for.
4998   * @return array
4999   */
5000  function course_get_course_dates_for_user_id(stdClass $course, int $userid): array {
5001      return (course_get_course_dates_for_user_ids($course, [$userid]))[$userid];
5002  }
5003  
5004  /**
5005   * Renders the course copy form for the modal on the course management screen.
5006   *
5007   * @param array $args
5008   * @return string $o Form HTML.
5009   */
5010  function course_output_fragment_new_base_form($args) {
5011  
5012      $serialiseddata = json_decode($args['jsonformdata'], true);
5013      $formdata = [];
5014      if (!empty($serialiseddata)) {
5015          parse_str($serialiseddata, $formdata);
5016      }
5017  
5018      $context = context_course::instance($args['courseid']);
5019      $copycaps = \core_course\management\helper::get_course_copy_capabilities();
5020      require_all_capabilities($copycaps, $context);
5021  
5022      $course = get_course($args['courseid']);
5023      $mform = new \core_backup\output\copy_form(
5024          null,
5025          array('course' => $course, 'returnto' => '', 'returnurl' => ''),
5026          'post', '', ['class' => 'ignoredirty'], true, $formdata);
5027  
5028      if (!empty($serialiseddata)) {
5029          // If we were passed non-empty form data we want the mform to call validation functions and show errors.
5030          $mform->is_validated();
5031      }
5032  
5033      ob_start();
5034      $mform->display();
5035      $o = ob_get_contents();
5036      ob_end_clean();
5037  
5038      return $o;
5039  }