Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 4.2.x will end 22 April 2024 (12 months).
  • Bug fixes for security issues in 4.2.x will end 7 October 2024 (18 months).
  • PHP version: minimum PHP 8.0.0 Note: minimum PHP version has increased since Moodle 4.1. PHP 8.1.x is supported too.
/course/ -> lib.php (source)

Differences Between: [Versions 310 and 402] [Versions 311 and 402] [Versions 39 and 402] [Versions 400 and 402] [Versions 401 and 402] [Versions 402 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 ($hasmanageactivities && $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' => '-1']),
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, ['duplicate' => $mod->id]),
1841              new pix_icon('t/copy', '', 'moodle', array('class' => 'iconsmall')),
1842              $str->duplicate,
1843              [
1844                  'class' => 'editing_duplicate',
1845                  'data-action' => ($courseformat->supports_components()) ? 'cmDuplicate' : 'duplicate',
1846                  'data-sectionreturn' => $sr,
1847                  'data-id' => $mod->id,
1848              ]
1849          );
1850      }
1851  
1852      // Assign.
1853      if (has_capability('moodle/role:assign', $modcontext)){
1854          $actions['assign'] = new action_menu_link_secondary(
1855              new moodle_url('/admin/roles/assign.php', array('contextid' => $modcontext->id)),
1856              new pix_icon('t/assignroles', '', 'moodle', array('class' => 'iconsmall')),
1857              $str->assign,
1858              array('class' => 'editing_assign', 'data-action' => 'assignroles', 'data-sectionreturn' => $sr)
1859          );
1860      }
1861  
1862      // Delete.
1863      if ($hasmanageactivities) {
1864          $actions['delete'] = new action_menu_link_secondary(
1865              new moodle_url($baseurl, ['delete' => $mod->id]),
1866              new pix_icon('t/delete', '', 'moodle', ['class' => 'iconsmall']),
1867              $str->delete,
1868              [
1869                  'class' => 'editing_delete',
1870                  'data-action' => ($usecomponents) ? 'cmDelete' : 'delete',
1871                  'data-sectionreturn' => $sr,
1872                  'data-id' => $mod->id,
1873              ]
1874          );
1875      }
1876  
1877      return $actions;
1878  }
1879  
1880  /**
1881   * Returns the move action.
1882   *
1883   * @param cm_info $mod The module to produce a move button for
1884   * @param int $sr The section to link back to (used for creating the links)
1885   * @return The markup for the move action, or an empty string if not available.
1886   */
1887  function course_get_cm_move(cm_info $mod, $sr = null) {
1888      global $OUTPUT;
1889  
1890      static $str;
1891      static $baseurl;
1892  
1893      $modcontext = context_module::instance($mod->id);
1894      $hasmanageactivities = has_capability('moodle/course:manageactivities', $modcontext);
1895  
1896      if (!isset($str)) {
1897          $str = get_strings(array('move'));
1898      }
1899  
1900      if (!isset($baseurl)) {
1901          $baseurl = new moodle_url('/course/mod.php', array('sesskey' => sesskey()));
1902  
1903          if ($sr !== null) {
1904              $baseurl->param('sr', $sr);
1905          }
1906      }
1907  
1908      if ($hasmanageactivities) {
1909          $pixicon = 'i/dragdrop';
1910  
1911          if (!course_ajax_enabled($mod->get_course())) {
1912              // Override for course frontpage until we get drag/drop working there.
1913              $pixicon = 't/move';
1914          }
1915  
1916          $attributes = [
1917              'class' => 'editing_move',
1918              'data-action' => 'move',
1919              'data-sectionreturn' => $sr,
1920              'title' => $str->move,
1921              'aria-label' => $str->move,
1922          ];
1923          return html_writer::link(
1924              new moodle_url($baseurl, ['copy' => $mod->id]),
1925              $OUTPUT->pix_icon($pixicon, '', 'moodle', ['class' => 'iconsmall']),
1926              $attributes
1927          );
1928      }
1929      return '';
1930  }
1931  
1932  /**
1933   * given a course object with shortname & fullname, this function will
1934   * truncate the the number of chars allowed and add ... if it was too long
1935   */
1936  function course_format_name ($course,$max=100) {
1937  
1938      $context = context_course::instance($course->id);
1939      $shortname = format_string($course->shortname, true, array('context' => $context));
1940      $fullname = format_string($course->fullname, true, array('context' => context_course::instance($course->id)));
1941      $str = $shortname.': '. $fullname;
1942      if (core_text::strlen($str) <= $max) {
1943          return $str;
1944      }
1945      else {
1946          return core_text::substr($str,0,$max-3).'...';
1947      }
1948  }
1949  
1950  /**
1951   * Is the user allowed to add this type of module to this course?
1952   * @param object $course the course settings. Only $course->id is used.
1953   * @param string $modname the module name. E.g. 'forum' or 'quiz'.
1954   * @param \stdClass $user the user to check, defaults to the global user if not provided.
1955   * @return bool whether the current user is allowed to add this type of module to this course.
1956   */
1957  function course_allowed_module($course, $modname, \stdClass $user = null) {
1958      global $USER;
1959      $user = $user ?? $USER;
1960      if (is_numeric($modname)) {
1961          throw new coding_exception('Function course_allowed_module no longer
1962                  supports numeric module ids. Please update your code to pass the module name.');
1963      }
1964  
1965      $capability = 'mod/' . $modname . ':addinstance';
1966      if (!get_capability_info($capability)) {
1967          // Debug warning that the capability does not exist, but no more than once per page.
1968          static $warned = array();
1969          $archetype = plugin_supports('mod', $modname, FEATURE_MOD_ARCHETYPE, MOD_ARCHETYPE_OTHER);
1970          if (!isset($warned[$modname]) && $archetype !== MOD_ARCHETYPE_SYSTEM) {
1971              debugging('The module ' . $modname . ' does not define the standard capability ' .
1972                      $capability , DEBUG_DEVELOPER);
1973              $warned[$modname] = 1;
1974          }
1975  
1976          // If the capability does not exist, the module can always be added.
1977          return true;
1978      }
1979  
1980      $coursecontext = context_course::instance($course->id);
1981      return has_capability($capability, $coursecontext, $user);
1982  }
1983  
1984  /**
1985   * Efficiently moves many courses around while maintaining
1986   * sortorder in order.
1987   *
1988   * @param array $courseids is an array of course ids
1989   * @param int $categoryid
1990   * @return bool success
1991   */
1992  function move_courses($courseids, $categoryid) {
1993      global $DB;
1994  
1995      if (empty($courseids)) {
1996          // Nothing to do.
1997          return false;
1998      }
1999  
2000      if (!$category = $DB->get_record('course_categories', array('id' => $categoryid))) {
2001          return false;
2002      }
2003  
2004      $courseids = array_reverse($courseids);
2005      $newparent = context_coursecat::instance($category->id);
2006      $i = 1;
2007  
2008      list($where, $params) = $DB->get_in_or_equal($courseids);
2009      $dbcourses = $DB->get_records_select('course', 'id ' . $where, $params, '', 'id, category, shortname, fullname');
2010      foreach ($dbcourses as $dbcourse) {
2011          $course = new stdClass();
2012          $course->id = $dbcourse->id;
2013          $course->timemodified = time();
2014          $course->category  = $category->id;
2015          $course->sortorder = $category->sortorder + get_max_courses_in_category() - $i++;
2016          if ($category->visible == 0) {
2017              // Hide the course when moving into hidden category, do not update the visibleold flag - we want to get
2018              // to previous state if somebody unhides the category.
2019              $course->visible = 0;
2020          }
2021  
2022          $DB->update_record('course', $course);
2023  
2024          // Update context, so it can be passed to event.
2025          $context = context_course::instance($course->id);
2026          $context->update_moved($newparent);
2027  
2028          // Trigger a course updated event.
2029          $event = \core\event\course_updated::create(array(
2030              'objectid' => $course->id,
2031              'context' => context_course::instance($course->id),
2032              'other' => array('shortname' => $dbcourse->shortname,
2033                               'fullname' => $dbcourse->fullname,
2034                               'updatedfields' => array('category' => $category->id))
2035          ));
2036          $event->trigger();
2037      }
2038      fix_course_sortorder();
2039      cache_helper::purge_by_event('changesincourse');
2040  
2041      return true;
2042  }
2043  
2044  /**
2045   * Returns the display name of the given section that the course prefers
2046   *
2047   * Implementation of this function is provided by course format
2048   * @see core_courseformat\base::get_section_name()
2049   *
2050   * @param int|stdClass $courseorid The course to get the section name for (object or just course id)
2051   * @param int|stdClass $section Section object from database or just field course_sections.section
2052   * @return string Display name that the course format prefers, e.g. "Week 2"
2053   */
2054  function get_section_name($courseorid, $section) {
2055      return course_get_format($courseorid)->get_section_name($section);
2056  }
2057  
2058  /**
2059   * Tells if current course format uses sections
2060   *
2061   * @param string $format Course format ID e.g. 'weeks' $course->format
2062   * @return bool
2063   */
2064  function course_format_uses_sections($format) {
2065      $course = new stdClass();
2066      $course->format = $format;
2067      return course_get_format($course)->uses_sections();
2068  }
2069  
2070  /**
2071   * Returns the information about the ajax support in the given source format
2072   *
2073   * The returned object's property (boolean)capable indicates that
2074   * the course format supports Moodle course ajax features.
2075   *
2076   * @param string $format
2077   * @return stdClass
2078   */
2079  function course_format_ajax_support($format) {
2080      $course = new stdClass();
2081      $course->format = $format;
2082      return course_get_format($course)->supports_ajax();
2083  }
2084  
2085  /**
2086   * Can the current user delete this course?
2087   * Course creators have exception,
2088   * 1 day after the creation they can sill delete the course.
2089   * @param int $courseid
2090   * @return boolean
2091   */
2092  function can_delete_course($courseid) {
2093      global $USER;
2094  
2095      $context = context_course::instance($courseid);
2096  
2097      if (has_capability('moodle/course:delete', $context)) {
2098          return true;
2099      }
2100  
2101      // hack: now try to find out if creator created this course recently (1 day)
2102      if (!has_capability('moodle/course:create', $context)) {
2103          return false;
2104      }
2105  
2106      $since = time() - 60*60*24;
2107      $course = get_course($courseid);
2108  
2109      if ($course->timecreated < $since) {
2110          return false; // Return if the course was not created in last 24 hours.
2111      }
2112  
2113      $logmanger = get_log_manager();
2114      $readers = $logmanger->get_readers('\core\log\sql_reader');
2115      $reader = reset($readers);
2116  
2117      if (empty($reader)) {
2118          return false; // No log reader found.
2119      }
2120  
2121      // A proper reader.
2122      $select = "userid = :userid AND courseid = :courseid AND eventname = :eventname AND timecreated > :since";
2123      $params = array('userid' => $USER->id, 'since' => $since, 'courseid' => $course->id, 'eventname' => '\core\event\course_created');
2124  
2125      return (bool)$reader->get_events_select_count($select, $params);
2126  }
2127  
2128  /**
2129   * Save the Your name for 'Some role' strings.
2130   *
2131   * @param integer $courseid the id of this course.
2132   * @param array $data the data that came from the course settings form.
2133   */
2134  function save_local_role_names($courseid, $data) {
2135      global $DB;
2136      $context = context_course::instance($courseid);
2137  
2138      foreach ($data as $fieldname => $value) {
2139          if (strpos($fieldname, 'role_') !== 0) {
2140              continue;
2141          }
2142          list($ignored, $roleid) = explode('_', $fieldname);
2143  
2144          // make up our mind whether we want to delete, update or insert
2145          if (!$value) {
2146              $DB->delete_records('role_names', array('contextid' => $context->id, 'roleid' => $roleid));
2147  
2148          } else if ($rolename = $DB->get_record('role_names', array('contextid' => $context->id, 'roleid' => $roleid))) {
2149              $rolename->name = $value;
2150              $DB->update_record('role_names', $rolename);
2151  
2152          } else {
2153              $rolename = new stdClass;
2154              $rolename->contextid = $context->id;
2155              $rolename->roleid = $roleid;
2156              $rolename->name = $value;
2157              $DB->insert_record('role_names', $rolename);
2158          }
2159          // This will ensure the course contacts cache is purged..
2160          core_course_category::role_assignment_changed($roleid, $context);
2161      }
2162  }
2163  
2164  /**
2165   * Returns options to use in course overviewfiles filemanager
2166   *
2167   * @param null|stdClass|core_course_list_element|int $course either object that has 'id' property or just the course id;
2168   *     may be empty if course does not exist yet (course create form)
2169   * @return array|null array of options such as maxfiles, maxbytes, accepted_types, etc.
2170   *     or null if overviewfiles are disabled
2171   */
2172  function course_overviewfiles_options($course) {
2173      global $CFG;
2174      if (empty($CFG->courseoverviewfileslimit)) {
2175          return null;
2176      }
2177  
2178      // Create accepted file types based on config value, falling back to default all.
2179      $acceptedtypes = (new \core_form\filetypes_util)->normalize_file_types($CFG->courseoverviewfilesext);
2180      if (in_array('*', $acceptedtypes) || empty($acceptedtypes)) {
2181          $acceptedtypes = '*';
2182      }
2183  
2184      $options = array(
2185          'maxfiles' => $CFG->courseoverviewfileslimit,
2186          'maxbytes' => $CFG->maxbytes,
2187          'subdirs' => 0,
2188          'accepted_types' => $acceptedtypes
2189      );
2190      if (!empty($course->id)) {
2191          $options['context'] = context_course::instance($course->id);
2192      } else if (is_int($course) && $course > 0) {
2193          $options['context'] = context_course::instance($course);
2194      }
2195      return $options;
2196  }
2197  
2198  /**
2199   * Create a course and either return a $course object
2200   *
2201   * Please note this functions does not verify any access control,
2202   * the calling code is responsible for all validation (usually it is the form definition).
2203   *
2204   * @param array $editoroptions course description editor options
2205   * @param object $data  - all the data needed for an entry in the 'course' table
2206   * @return object new course instance
2207   */
2208  function create_course($data, $editoroptions = NULL) {
2209      global $DB, $CFG;
2210  
2211      //check the categoryid - must be given for all new courses
2212      $category = $DB->get_record('course_categories', array('id'=>$data->category), '*', MUST_EXIST);
2213  
2214      // Check if the shortname already exists.
2215      if (!empty($data->shortname)) {
2216          if ($DB->record_exists('course', array('shortname' => $data->shortname))) {
2217              throw new moodle_exception('shortnametaken', '', '', $data->shortname);
2218          }
2219      }
2220  
2221      // Check if the idnumber already exists.
2222      if (!empty($data->idnumber)) {
2223          if ($DB->record_exists('course', array('idnumber' => $data->idnumber))) {
2224              throw new moodle_exception('courseidnumbertaken', '', '', $data->idnumber);
2225          }
2226      }
2227  
2228      if (empty($CFG->enablecourserelativedates)) {
2229          // Make sure we're not setting the relative dates mode when the setting is disabled.
2230          unset($data->relativedatesmode);
2231      }
2232  
2233      if ($errorcode = course_validate_dates((array)$data)) {
2234          throw new moodle_exception($errorcode);
2235      }
2236  
2237      // Check if timecreated is given.
2238      $data->timecreated  = !empty($data->timecreated) ? $data->timecreated : time();
2239      $data->timemodified = $data->timecreated;
2240  
2241      // place at beginning of any category
2242      $data->sortorder = 0;
2243  
2244      if ($editoroptions) {
2245          // summary text is updated later, we need context to store the files first
2246          $data->summary = '';
2247          $data->summary_format = $data->summary_editor['format'];
2248      }
2249  
2250      // Get default completion settings as a fallback in case the enablecompletion field is not set.
2251      $courseconfig = get_config('moodlecourse');
2252      $defaultcompletion = !empty($CFG->enablecompletion) ? $courseconfig->enablecompletion : COMPLETION_DISABLED;
2253      $enablecompletion = $data->enablecompletion ?? $defaultcompletion;
2254      // Unset showcompletionconditions when completion tracking is not enabled for the course.
2255      if ($enablecompletion == COMPLETION_DISABLED) {
2256          unset($data->showcompletionconditions);
2257      } else if (!isset($data->showcompletionconditions)) {
2258          // Show completion conditions should have a default value when completion is enabled. Set it to the site defaults.
2259          // This scenario can happen when a course is created through data generators or through a web service.
2260          $data->showcompletionconditions = $courseconfig->showcompletionconditions;
2261      }
2262  
2263      if (!isset($data->visible)) {
2264          // data not from form, add missing visibility info
2265          $data->visible = $category->visible;
2266      }
2267      $data->visibleold = $data->visible;
2268  
2269      $newcourseid = $DB->insert_record('course', $data);
2270      $context = context_course::instance($newcourseid, MUST_EXIST);
2271  
2272      if ($editoroptions) {
2273          // Save the files used in the summary editor and store
2274          $data = file_postupdate_standard_editor($data, 'summary', $editoroptions, $context, 'course', 'summary', 0);
2275          $DB->set_field('course', 'summary', $data->summary, array('id'=>$newcourseid));
2276          $DB->set_field('course', 'summaryformat', $data->summary_format, array('id'=>$newcourseid));
2277      }
2278      if ($overviewfilesoptions = course_overviewfiles_options($newcourseid)) {
2279          // Save the course overviewfiles
2280          $data = file_postupdate_standard_filemanager($data, 'overviewfiles', $overviewfilesoptions, $context, 'course', 'overviewfiles', 0);
2281      }
2282  
2283      // update course format options
2284      course_get_format($newcourseid)->update_course_format_options($data);
2285  
2286      $course = course_get_format($newcourseid)->get_course();
2287  
2288      fix_course_sortorder();
2289      // purge appropriate caches in case fix_course_sortorder() did not change anything
2290      cache_helper::purge_by_event('changesincourse');
2291  
2292      // Trigger a course created event.
2293      $event = \core\event\course_created::create(array(
2294          'objectid' => $course->id,
2295          'context' => context_course::instance($course->id),
2296          'other' => array('shortname' => $course->shortname,
2297              'fullname' => $course->fullname)
2298      ));
2299  
2300      $event->trigger();
2301  
2302      // Setup the blocks
2303      blocks_add_default_course_blocks($course);
2304  
2305      // Create default section and initial sections if specified (unless they've already been created earlier).
2306      // We do not want to call course_create_sections_if_missing() because to avoid creating course cache.
2307      $numsections = isset($data->numsections) ? $data->numsections : 0;
2308      $existingsections = $DB->get_fieldset_sql('SELECT section from {course_sections} WHERE course = ?', [$newcourseid]);
2309      $newsections = array_diff(range(0, $numsections), $existingsections);
2310      foreach ($newsections as $sectionnum) {
2311          course_create_section($newcourseid, $sectionnum, true);
2312      }
2313  
2314      // Save any custom role names.
2315      save_local_role_names($course->id, (array)$data);
2316  
2317      // set up enrolments
2318      enrol_course_updated(true, $course, $data);
2319  
2320      // Update course tags.
2321      if (isset($data->tags)) {
2322          core_tag_tag::set_item_tags('core', 'course', $course->id, context_course::instance($course->id), $data->tags);
2323      }
2324  
2325      // Save custom fields if there are any of them in the form.
2326      $handler = core_course\customfield\course_handler::create();
2327      // Make sure to set the handler's parent context first.
2328      $coursecatcontext = context_coursecat::instance($category->id);
2329      $handler->set_parent_context($coursecatcontext);
2330      // Save the custom field data.
2331      $data->id = $course->id;
2332      $handler->instance_form_save($data, true);
2333  
2334      return $course;
2335  }
2336  
2337  /**
2338   * Update a course.
2339   *
2340   * Please note this functions does not verify any access control,
2341   * the calling code is responsible for all validation (usually it is the form definition).
2342   *
2343   * @param object $data  - all the data needed for an entry in the 'course' table
2344   * @param array $editoroptions course description editor options
2345   * @return void
2346   */
2347  function update_course($data, $editoroptions = NULL) {
2348      global $DB, $CFG;
2349  
2350      // Prevent changes on front page course.
2351      if ($data->id == SITEID) {
2352          throw new moodle_exception('invalidcourse', 'error');
2353      }
2354  
2355      $oldcourse = course_get_format($data->id)->get_course();
2356      $context   = context_course::instance($oldcourse->id);
2357  
2358      // Make sure we're not changing whatever the course's relativedatesmode setting is.
2359      unset($data->relativedatesmode);
2360  
2361      // Capture the updated fields for the log data.
2362      $updatedfields = [];
2363      foreach (get_object_vars($oldcourse) as $field => $value) {
2364          if ($field == 'summary_editor') {
2365              if (($data->$field)['text'] !== $value['text']) {
2366                  // The summary might be very long, we don't wan't to fill up the log record with the full text.
2367                  $updatedfields[$field] = '(updated)';
2368              }
2369          } else if ($field == 'tags' && isset($data->tags)) {
2370              // Tags might not have the same array keys, just check the values.
2371              if (array_values($data->$field) !== array_values($value)) {
2372                  $updatedfields[$field] = $data->$field;
2373              }
2374          } else {
2375              if (isset($data->$field) && $data->$field != $value) {
2376                  $updatedfields[$field] = $data->$field;
2377              }
2378          }
2379      }
2380  
2381      $data->timemodified = time();
2382  
2383      if ($editoroptions) {
2384          $data = file_postupdate_standard_editor($data, 'summary', $editoroptions, $context, 'course', 'summary', 0);
2385      }
2386      if ($overviewfilesoptions = course_overviewfiles_options($data->id)) {
2387          $data = file_postupdate_standard_filemanager($data, 'overviewfiles', $overviewfilesoptions, $context, 'course', 'overviewfiles', 0);
2388      }
2389  
2390      // Check we don't have a duplicate shortname.
2391      if (!empty($data->shortname) && $oldcourse->shortname != $data->shortname) {
2392          if ($DB->record_exists_sql('SELECT id from {course} WHERE shortname = ? AND id <> ?', array($data->shortname, $data->id))) {
2393              throw new moodle_exception('shortnametaken', '', '', $data->shortname);
2394          }
2395      }
2396  
2397      // Check we don't have a duplicate idnumber.
2398      if (!empty($data->idnumber) && $oldcourse->idnumber != $data->idnumber) {
2399          if ($DB->record_exists_sql('SELECT id from {course} WHERE idnumber = ? AND id <> ?', array($data->idnumber, $data->id))) {
2400              throw new moodle_exception('courseidnumbertaken', '', '', $data->idnumber);
2401          }
2402      }
2403  
2404      if ($errorcode = course_validate_dates((array)$data)) {
2405          throw new moodle_exception($errorcode);
2406      }
2407  
2408      if (!isset($data->category) or empty($data->category)) {
2409          // prevent nulls and 0 in category field
2410          unset($data->category);
2411      }
2412      $changesincoursecat = $movecat = (isset($data->category) and $oldcourse->category != $data->category);
2413  
2414      if (!isset($data->visible)) {
2415          // data not from form, add missing visibility info
2416          $data->visible = $oldcourse->visible;
2417      }
2418  
2419      if ($data->visible != $oldcourse->visible) {
2420          // reset the visibleold flag when manually hiding/unhiding course
2421          $data->visibleold = $data->visible;
2422          $changesincoursecat = true;
2423      } else {
2424          if ($movecat) {
2425              $newcategory = $DB->get_record('course_categories', array('id'=>$data->category));
2426              if (empty($newcategory->visible)) {
2427                  // make sure when moving into hidden category the course is hidden automatically
2428                  $data->visible = 0;
2429              }
2430          }
2431      }
2432  
2433      // Set newsitems to 0 if format does not support announcements.
2434      if (isset($data->format)) {
2435          $newcourseformat = course_get_format((object)['format' => $data->format]);
2436          if (!$newcourseformat->supports_news()) {
2437              $data->newsitems = 0;
2438          }
2439      }
2440  
2441      // Set showcompletionconditions to null when completion tracking has been disabled for the course.
2442      if (isset($data->enablecompletion) && $data->enablecompletion == COMPLETION_DISABLED) {
2443          $data->showcompletionconditions = null;
2444      }
2445  
2446      // Update custom fields if there are any of them in the form.
2447      $handler = core_course\customfield\course_handler::create();
2448      $handler->instance_form_save($data);
2449  
2450      // Update with the new data
2451      $DB->update_record('course', $data);
2452      // make sure the modinfo cache is reset
2453      rebuild_course_cache($data->id);
2454  
2455      // Purge course image cache in case if course image has been updated.
2456      \cache::make('core', 'course_image')->delete($data->id);
2457  
2458      // update course format options with full course data
2459      course_get_format($data->id)->update_course_format_options($data, $oldcourse);
2460  
2461      $course = $DB->get_record('course', array('id'=>$data->id));
2462  
2463      if ($movecat) {
2464          $newparent = context_coursecat::instance($course->category);
2465          $context->update_moved($newparent);
2466      }
2467      $fixcoursesortorder = $movecat || (isset($data->sortorder) && ($oldcourse->sortorder != $data->sortorder));
2468      if ($fixcoursesortorder) {
2469          fix_course_sortorder();
2470      }
2471  
2472      // purge appropriate caches in case fix_course_sortorder() did not change anything
2473      cache_helper::purge_by_event('changesincourse');
2474      if ($changesincoursecat) {
2475          cache_helper::purge_by_event('changesincoursecat');
2476      }
2477  
2478      // Test for and remove blocks which aren't appropriate anymore
2479      blocks_remove_inappropriate($course);
2480  
2481      // Save any custom role names.
2482      save_local_role_names($course->id, $data);
2483  
2484      // update enrol settings
2485      enrol_course_updated(false, $course, $data);
2486  
2487      // Update course tags.
2488      if (isset($data->tags)) {
2489          core_tag_tag::set_item_tags('core', 'course', $course->id, context_course::instance($course->id), $data->tags);
2490      }
2491  
2492      // Trigger a course updated event.
2493      $event = \core\event\course_updated::create(array(
2494          'objectid' => $course->id,
2495          'context' => context_course::instance($course->id),
2496          'other' => array('shortname' => $course->shortname,
2497                           'fullname' => $course->fullname,
2498                           'updatedfields' => $updatedfields)
2499      ));
2500  
2501      $event->trigger();
2502  
2503      if ($oldcourse->format !== $course->format) {
2504          // Remove all options stored for the previous format
2505          // We assume that new course format migrated everything it needed watching trigger
2506          // 'course_updated' and in method format_XXX::update_course_format_options()
2507          $DB->delete_records('course_format_options',
2508                  array('courseid' => $course->id, 'format' => $oldcourse->format));
2509      }
2510  }
2511  
2512  /**
2513   * Calculate the average number of enrolled participants per course.
2514   *
2515   * This is intended for statistics purposes during the site registration. Only visible courses are taken into account.
2516   * Front page enrolments are excluded.
2517   *
2518   * @param bool $onlyactive Consider only active enrolments in enabled plugins and obey the enrolment time restrictions.
2519   * @param int $lastloginsince If specified, count only users who logged in after this timestamp.
2520   * @return float
2521   */
2522  function average_number_of_participants(bool $onlyactive = false, int $lastloginsince = null): float {
2523      global $DB;
2524  
2525      $params = [];
2526  
2527      $sql = "SELECT DISTINCT ue.userid, e.courseid
2528                FROM {user_enrolments} ue
2529                JOIN {enrol} e ON e.id = ue.enrolid
2530                JOIN {course} c ON c.id = e.courseid ";
2531  
2532      if ($onlyactive || $lastloginsince) {
2533          $sql .= "JOIN {user} u ON u.id = ue.userid ";
2534      }
2535  
2536      $sql .= "WHERE e.courseid <> " . SITEID . " AND c.visible = 1 ";
2537  
2538      if ($onlyactive) {
2539          $sql .= "AND ue.status = :active
2540                   AND e.status = :enabled
2541                   AND ue.timestart < :now1
2542                   AND (ue.timeend = 0 OR ue.timeend > :now2) ";
2543  
2544          // Same as in the enrollib - the rounding should help caching in the database.
2545          $now = round(time(), -2);
2546  
2547          $params += [
2548              'active' => ENROL_USER_ACTIVE,
2549              'enabled' => ENROL_INSTANCE_ENABLED,
2550              'now1' => $now,
2551              'now2' => $now,
2552          ];
2553      }
2554  
2555      if ($lastloginsince) {
2556          $sql .= "AND u.lastlogin > :lastlogin ";
2557          $params['lastlogin'] = $lastloginsince;
2558      }
2559  
2560      $sql = "SELECT COUNT(*)
2561                FROM ($sql) total";
2562  
2563      $enrolmenttotal = $DB->count_records_sql($sql, $params);
2564  
2565      // Get the number of visible courses (exclude the front page).
2566      $coursetotal = $DB->count_records('course', ['visible' => 1]);
2567      $coursetotal = $coursetotal - 1;
2568  
2569      if (empty($coursetotal)) {
2570          $participantaverage = 0;
2571  
2572      } else {
2573          $participantaverage = $enrolmenttotal / $coursetotal;
2574      }
2575  
2576      return $participantaverage;
2577  }
2578  
2579  /**
2580   * Average number of course modules
2581   * @return integer
2582   */
2583  function average_number_of_courses_modules() {
2584      global $DB, $SITE;
2585  
2586      //count total of visible course module (except front page)
2587      $sql = 'SELECT COUNT(*) FROM (
2588          SELECT cm.course, cm.module
2589          FROM {course} c, {course_modules} cm
2590          WHERE c.id = cm.course
2591              AND c.id <> :siteid
2592              AND cm.visible = 1
2593              AND c.visible = 1) total';
2594      $params = array('siteid' => $SITE->id);
2595      $moduletotal = $DB->count_records_sql($sql, $params);
2596  
2597  
2598      //count total of visible courses (minus front page)
2599      $coursetotal = $DB->count_records('course', array('visible' => 1));
2600      $coursetotal = $coursetotal - 1 ;
2601  
2602      //average of course module
2603      if (empty($coursetotal)) {
2604          $coursemoduleaverage = 0;
2605      } else {
2606          $coursemoduleaverage = $moduletotal / $coursetotal;
2607      }
2608  
2609      return $coursemoduleaverage;
2610  }
2611  
2612  /**
2613   * This class pertains to course requests and contains methods associated with
2614   * create, approving, and removing course requests.
2615   *
2616   * Please note we do not allow embedded images here because there is no context
2617   * to store them with proper access control.
2618   *
2619   * @copyright 2009 Sam Hemelryk
2620   * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
2621   * @since Moodle 2.0
2622   *
2623   * @property-read int $id
2624   * @property-read string $fullname
2625   * @property-read string $shortname
2626   * @property-read string $summary
2627   * @property-read int $summaryformat
2628   * @property-read int $summarytrust
2629   * @property-read string $reason
2630   * @property-read int $requester
2631   */
2632  class course_request {
2633  
2634      /**
2635       * This is the stdClass that stores the properties for the course request
2636       * and is externally accessed through the __get magic method
2637       * @var stdClass
2638       */
2639      protected $properties;
2640  
2641      /**
2642       * An array of options for the summary editor used by course request forms.
2643       * This is initially set by {@link summary_editor_options()}
2644       * @var array
2645       * @static
2646       */
2647      protected static $summaryeditoroptions;
2648  
2649      /**
2650       * Static function to prepare the summary editor for working with a course
2651       * request.
2652       *
2653       * @static
2654       * @param null|stdClass $data Optional, an object containing the default values
2655       *                       for the form, these may be modified when preparing the
2656       *                       editor so this should be called before creating the form
2657       * @return stdClass An object that can be used to set the default values for
2658       *                   an mforms form
2659       */
2660      public static function prepare($data=null) {
2661          if ($data === null) {
2662              $data = new stdClass;
2663          }
2664          $data = file_prepare_standard_editor($data, 'summary', self::summary_editor_options());
2665          return $data;
2666      }
2667  
2668      /**
2669       * Static function to create a new course request when passed an array of properties
2670       * for it.
2671       *
2672       * This function also handles saving any files that may have been used in the editor
2673       *
2674       * @static
2675       * @param stdClass $data
2676       * @return course_request The newly created course request
2677       */
2678      public static function create($data) {
2679          global $USER, $DB, $CFG;
2680          $data->requester = $USER->id;
2681  
2682          // Setting the default category if none set.
2683          if (empty($data->category) || !empty($CFG->lockrequestcategory)) {
2684              $data->category = $CFG->defaultrequestcategory;
2685          }
2686  
2687          // Summary is a required field so copy the text over
2688          $data->summary       = $data->summary_editor['text'];
2689          $data->summaryformat = $data->summary_editor['format'];
2690  
2691          $data->id = $DB->insert_record('course_request', $data);
2692  
2693          // Create a new course_request object and return it
2694          $request = new course_request($data);
2695  
2696          // Notify the admin if required.
2697          if ($users = get_users_from_config($CFG->courserequestnotify, 'moodle/site:approvecourse')) {
2698  
2699              $a = new stdClass;
2700              $a->link = "$CFG->wwwroot/course/pending.php";
2701              $a->user = fullname($USER);
2702              $subject = get_string('courserequest');
2703              $message = get_string('courserequestnotifyemail', 'admin', $a);
2704              foreach ($users as $user) {
2705                  $request->notify($user, $USER, 'courserequested', $subject, $message);
2706              }
2707          }
2708  
2709          return $request;
2710      }
2711  
2712      /**
2713       * Returns an array of options to use with a summary editor
2714       *
2715       * @uses course_request::$summaryeditoroptions
2716       * @return array An array of options to use with the editor
2717       */
2718      public static function summary_editor_options() {
2719          global $CFG;
2720          if (self::$summaryeditoroptions === null) {
2721              self::$summaryeditoroptions = array('maxfiles' => 0, 'maxbytes'=>0);
2722          }
2723          return self::$summaryeditoroptions;
2724      }
2725  
2726      /**
2727       * Loads the properties for this course request object. Id is required and if
2728       * only id is provided then we load the rest of the properties from the database
2729       *
2730       * @param stdClass|int $properties Either an object containing properties
2731       *                      or the course_request id to load
2732       */
2733      public function __construct($properties) {
2734          global $DB;
2735          if (empty($properties->id)) {
2736              if (empty($properties)) {
2737                  throw new coding_exception('You must provide a course request id when creating a course_request object');
2738              }
2739              $id = $properties;
2740              $properties = new stdClass;
2741              $properties->id = (int)$id;
2742              unset($id);
2743          }
2744          if (empty($properties->requester)) {
2745              if (!($this->properties = $DB->get_record('course_request', array('id' => $properties->id)))) {
2746                  throw new \moodle_exception('unknowncourserequest');
2747              }
2748          } else {
2749              $this->properties = $properties;
2750          }
2751          $this->properties->collision = null;
2752      }
2753  
2754      /**
2755       * Returns the requested property
2756       *
2757       * @param string $key
2758       * @return mixed
2759       */
2760      public function __get($key) {
2761          return $this->properties->$key;
2762      }
2763  
2764      /**
2765       * Override this to ensure empty($request->blah) calls return a reliable answer...
2766       *
2767       * This is required because we define the __get method
2768       *
2769       * @param mixed $key
2770       * @return bool True is it not empty, false otherwise
2771       */
2772      public function __isset($key) {
2773          return (!empty($this->properties->$key));
2774      }
2775  
2776      /**
2777       * Returns the user who requested this course
2778       *
2779       * Uses a static var to cache the results and cut down the number of db queries
2780       *
2781       * @staticvar array $requesters An array of cached users
2782       * @return stdClass The user who requested the course
2783       */
2784      public function get_requester() {
2785          global $DB;
2786          static $requesters= array();
2787          if (!array_key_exists($this->properties->requester, $requesters)) {
2788              $requesters[$this->properties->requester] = $DB->get_record('user', array('id'=>$this->properties->requester));
2789          }
2790          return $requesters[$this->properties->requester];
2791      }
2792  
2793      /**
2794       * Checks that the shortname used by the course does not conflict with any other
2795       * courses that exist
2796       *
2797       * @param string|null $shortnamemark The string to append to the requests shortname
2798       *                     should a conflict be found
2799       * @return bool true is there is a conflict, false otherwise
2800       */
2801      public function check_shortname_collision($shortnamemark = '[*]') {
2802          global $DB;
2803  
2804          if ($this->properties->collision !== null) {
2805              return $this->properties->collision;
2806          }
2807  
2808          if (empty($this->properties->shortname)) {
2809              debugging('Attempting to check a course request shortname before it has been set', DEBUG_DEVELOPER);
2810              $this->properties->collision = false;
2811          } else if ($DB->record_exists('course', array('shortname' => $this->properties->shortname))) {
2812              if (!empty($shortnamemark)) {
2813                  $this->properties->shortname .= ' '.$shortnamemark;
2814              }
2815              $this->properties->collision = true;
2816          } else {
2817              $this->properties->collision = false;
2818          }
2819          return $this->properties->collision;
2820      }
2821  
2822      /**
2823       * Checks user capability to approve a requested course
2824       *
2825       * If course was requested without category for some reason (might happen if $CFG->defaultrequestcategory is
2826       * misconfigured), we check capabilities 'moodle/site:approvecourse' and 'moodle/course:changecategory'.
2827       *
2828       * @return bool
2829       */
2830      public function can_approve() {
2831          global $CFG;
2832          $category = null;
2833          if ($this->properties->category) {
2834              $category = core_course_category::get($this->properties->category, IGNORE_MISSING);
2835          } else if ($CFG->defaultrequestcategory) {
2836              $category = core_course_category::get($CFG->defaultrequestcategory, IGNORE_MISSING);
2837          }
2838          if ($category) {
2839              return has_capability('moodle/site:approvecourse', $category->get_context());
2840          }
2841  
2842          // We can not determine the context where the course should be created. The approver should have
2843          // both capabilities to approve courses and change course category in the system context.
2844          return has_all_capabilities(['moodle/site:approvecourse', 'moodle/course:changecategory'], context_system::instance());
2845      }
2846  
2847      /**
2848       * Returns the category where this course request should be created
2849       *
2850       * Note that we don't check here that user has a capability to view
2851       * hidden categories if he has capabilities 'moodle/site:approvecourse' and
2852       * 'moodle/course:changecategory'
2853       *
2854       * @return core_course_category
2855       */
2856      public function get_category() {
2857          global $CFG;
2858          if ($this->properties->category && ($category = core_course_category::get($this->properties->category, IGNORE_MISSING))) {
2859              return $category;
2860          } else if ($CFG->defaultrequestcategory &&
2861                  ($category = core_course_category::get($CFG->defaultrequestcategory, IGNORE_MISSING))) {
2862              return $category;
2863          } else {
2864              return core_course_category::get_default();
2865          }
2866      }
2867  
2868      /**
2869       * This function approves the request turning it into a course
2870       *
2871       * This function converts the course request into a course, at the same time
2872       * transferring any files used in the summary to the new course and then removing
2873       * the course request and the files associated with it.
2874       *
2875       * @return int The id of the course that was created from this request
2876       */
2877      public function approve() {
2878          global $CFG, $DB, $USER;
2879  
2880          require_once($CFG->dirroot . '/backup/util/includes/restore_includes.php');
2881  
2882          $user = $DB->get_record('user', array('id' => $this->properties->requester, 'deleted'=>0), '*', MUST_EXIST);
2883  
2884          $courseconfig = get_config('moodlecourse');
2885  
2886          // Transfer appropriate settings
2887          $data = clone($this->properties);
2888          unset($data->id);
2889          unset($data->reason);
2890          unset($data->requester);
2891  
2892          // Set category
2893          $category = $this->get_category();
2894          $data->category = $category->id;
2895          // Set misc settings
2896          $data->requested = 1;
2897  
2898          // Apply course default settings
2899          $data->format             = $courseconfig->format;
2900          $data->newsitems          = $courseconfig->newsitems;
2901          $data->showgrades         = $courseconfig->showgrades;
2902          $data->showreports        = $courseconfig->showreports;
2903          $data->maxbytes           = $courseconfig->maxbytes;
2904          $data->groupmode          = $courseconfig->groupmode;
2905          $data->groupmodeforce     = $courseconfig->groupmodeforce;
2906          $data->visible            = $courseconfig->visible;
2907          $data->visibleold         = $data->visible;
2908          $data->lang               = $courseconfig->lang;
2909          $data->enablecompletion   = $courseconfig->enablecompletion;
2910          $data->numsections        = $courseconfig->numsections;
2911          $data->startdate          = usergetmidnight(time());
2912          if ($courseconfig->courseenddateenabled) {
2913              $data->enddate        = usergetmidnight(time()) + $courseconfig->courseduration;
2914          }
2915  
2916          list($data->fullname, $data->shortname) = restore_dbops::calculate_course_names(0, $data->fullname, $data->shortname);
2917  
2918          $course = create_course($data);
2919          $context = context_course::instance($course->id, MUST_EXIST);
2920  
2921          // add enrol instances
2922          if (!$DB->record_exists('enrol', array('courseid'=>$course->id, 'enrol'=>'manual'))) {
2923              if ($manual = enrol_get_plugin('manual')) {
2924                  $manual->add_default_instance($course);
2925              }
2926          }
2927  
2928          // enrol the requester as teacher if necessary
2929          if (!empty($CFG->creatornewroleid) and !is_viewing($context, $user, 'moodle/role:assign') and !is_enrolled($context, $user, 'moodle/role:assign')) {
2930              enrol_try_internal_enrol($course->id, $user->id, $CFG->creatornewroleid);
2931          }
2932  
2933          $this->delete();
2934  
2935          $a = new stdClass();
2936          $a->name = format_string($course->fullname, true, array('context' => context_course::instance($course->id)));
2937          $a->url = $CFG->wwwroot.'/course/view.php?id=' . $course->id;
2938          $this->notify($user, $USER, 'courserequestapproved', get_string('courseapprovedsubject'), get_string('courseapprovedemail2', 'moodle', $a), $course->id);
2939  
2940          return $course->id;
2941      }
2942  
2943      /**
2944       * Reject a course request
2945       *
2946       * This function rejects a course request, emailing the requesting user the
2947       * provided notice and then removing the request from the database
2948       *
2949       * @param string $notice The message to display to the user
2950       */
2951      public function reject($notice) {
2952          global $USER, $DB;
2953          $user = $DB->get_record('user', array('id' => $this->properties->requester), '*', MUST_EXIST);
2954          $this->notify($user, $USER, 'courserequestrejected', get_string('courserejectsubject'), get_string('courserejectemail', 'moodle', $notice));
2955          $this->delete();
2956      }
2957  
2958      /**
2959       * Deletes the course request and any associated files
2960       */
2961      public function delete() {
2962          global $DB;
2963          $DB->delete_records('course_request', array('id' => $this->properties->id));
2964      }
2965  
2966      /**
2967       * Send a message from one user to another using events_trigger
2968       *
2969       * @param object $touser
2970       * @param object $fromuser
2971       * @param string $name
2972       * @param string $subject
2973       * @param string $message
2974       * @param int|null $courseid
2975       */
2976      protected function notify($touser, $fromuser, $name, $subject, $message, $courseid = null) {
2977          $eventdata = new \core\message\message();
2978          $eventdata->courseid          = empty($courseid) ? SITEID : $courseid;
2979          $eventdata->component         = 'moodle';
2980          $eventdata->name              = $name;
2981          $eventdata->userfrom          = $fromuser;
2982          $eventdata->userto            = $touser;
2983          $eventdata->subject           = $subject;
2984          $eventdata->fullmessage       = $message;
2985          $eventdata->fullmessageformat = FORMAT_PLAIN;
2986          $eventdata->fullmessagehtml   = '';
2987          $eventdata->smallmessage      = '';
2988          $eventdata->notification      = 1;
2989          message_send($eventdata);
2990      }
2991  
2992      /**
2993       * Checks if current user can request a course in this context
2994       *
2995       * @param context $context
2996       * @return bool
2997       */
2998      public static function can_request(context $context) {
2999          global $CFG;
3000          if (empty($CFG->enablecourserequests)) {
3001              return false;
3002          }
3003          if (has_capability('moodle/course:create', $context)) {
3004              return false;
3005          }
3006  
3007          if ($context instanceof context_system) {
3008              $defaultcontext = context_coursecat::instance($CFG->defaultrequestcategory, IGNORE_MISSING);
3009              return $defaultcontext &&
3010                  has_capability('moodle/course:request', $defaultcontext);
3011          } else if ($context instanceof context_coursecat) {
3012              if (!$CFG->lockrequestcategory || $CFG->defaultrequestcategory == $context->instanceid) {
3013                  return has_capability('moodle/course:request', $context);
3014              }
3015          }
3016          return false;
3017      }
3018  }
3019  
3020  /**
3021   * Return a list of page types
3022   * @param string $pagetype current page type
3023   * @param context $parentcontext Block's parent context
3024   * @param context $currentcontext Current context of block
3025   * @return array array of page types
3026   */
3027  function course_page_type_list($pagetype, $parentcontext, $currentcontext) {
3028      if ($pagetype === 'course-index' || $pagetype === 'course-index-category') {
3029          // For courses and categories browsing pages (/course/index.php) add option to show on ANY category page
3030          $pagetypes = array('*' => get_string('page-x', 'pagetype'),
3031              'course-index-*' => get_string('page-course-index-x', 'pagetype'),
3032          );
3033      } else if ($currentcontext && (!($coursecontext = $currentcontext->get_course_context(false)) || $coursecontext->instanceid == SITEID)) {
3034          // We know for sure that despite pagetype starts with 'course-' this is not a page in course context (i.e. /course/search.php, etc.)
3035          $pagetypes = array('*' => get_string('page-x', 'pagetype'));
3036      } else {
3037          // Otherwise consider it a page inside a course even if $currentcontext is null
3038          $pagetypes = array('*' => get_string('page-x', 'pagetype'),
3039              'course-*' => get_string('page-course-x', 'pagetype'),
3040              'course-view-*' => get_string('page-course-view-x', 'pagetype')
3041          );
3042      }
3043      return $pagetypes;
3044  }
3045  
3046  /**
3047   * Determine whether course ajax should be enabled for the specified course
3048   *
3049   * @param stdClass $course The course to test against
3050   * @return boolean Whether course ajax is enabled or note
3051   */
3052  function course_ajax_enabled($course) {
3053      global $CFG, $PAGE, $SITE;
3054  
3055      // The user must be editing for AJAX to be included
3056      if (!$PAGE->user_is_editing()) {
3057          return false;
3058      }
3059  
3060      // Check that the theme suports
3061      if (!$PAGE->theme->enablecourseajax) {
3062          return false;
3063      }
3064  
3065      // Check that the course format supports ajax functionality
3066      // The site 'format' doesn't have information on course format support
3067      if ($SITE->id !== $course->id) {
3068          $courseformatajaxsupport = course_format_ajax_support($course->format);
3069          if (!$courseformatajaxsupport->capable) {
3070              return false;
3071          }
3072      }
3073  
3074      // All conditions have been met so course ajax should be enabled
3075      return true;
3076  }
3077  
3078  /**
3079   * Include the relevant javascript and language strings for the resource
3080   * toolbox YUI module
3081   *
3082   * @param integer $id The ID of the course being applied to
3083   * @param array $usedmodules An array containing the names of the modules in use on the page
3084   * @param array $enabledmodules An array containing the names of the enabled (visible) modules on this site
3085   * @param stdClass $config An object containing configuration parameters for ajax modules including:
3086   *          * resourceurl   The URL to post changes to for resource changes
3087   *          * sectionurl    The URL to post changes to for section changes
3088   *          * pageparams    Additional parameters to pass through in the post
3089   * @return bool
3090   */
3091  function include_course_ajax($course, $usedmodules = array(), $enabledmodules = null, $config = null) {
3092      global $CFG, $PAGE, $SITE;
3093  
3094      // Init the course editor module to support UI components.
3095      $format = course_get_format($course);
3096      include_course_editor($format);
3097  
3098      // Ensure that ajax should be included
3099      if (!course_ajax_enabled($course)) {
3100          return false;
3101      }
3102  
3103      // Component based formats don't use YUI drag and drop anymore.
3104      if (!$format->supports_components() && course_format_uses_sections($course->format)) {
3105  
3106          if (!$config) {
3107              $config = new stdClass();
3108          }
3109  
3110          // The URL to use for resource changes.
3111          if (!isset($config->resourceurl)) {
3112              $config->resourceurl = '/course/rest.php';
3113          }
3114  
3115          // The URL to use for section changes.
3116          if (!isset($config->sectionurl)) {
3117              $config->sectionurl = '/course/rest.php';
3118          }
3119  
3120          // Any additional parameters which need to be included on page submission.
3121          if (!isset($config->pageparams)) {
3122              $config->pageparams = array();
3123          }
3124  
3125          $PAGE->requires->yui_module('moodle-course-dragdrop', 'M.course.init_section_dragdrop',
3126              array(array(
3127                  'courseid' => $course->id,
3128                  'ajaxurl' => $config->sectionurl,
3129                  'config' => $config,
3130              )), null, true);
3131  
3132          $PAGE->requires->yui_module('moodle-course-dragdrop', 'M.course.init_resource_dragdrop',
3133              array(array(
3134                  'courseid' => $course->id,
3135                  'ajaxurl' => $config->resourceurl,
3136                  'config' => $config,
3137              )), null, true);
3138  
3139          // Require various strings for the command toolbox.
3140          $PAGE->requires->strings_for_js(array(
3141              'moveleft',
3142              'deletechecktype',
3143              'deletechecktypename',
3144              'edittitle',
3145              'edittitleinstructions',
3146              'show',
3147              'hide',
3148              'highlight',
3149              'highlightoff',
3150              'groupsnone',
3151              'groupsvisible',
3152              'groupsseparate',
3153              'clicktochangeinbrackets',
3154              'markthistopic',
3155              'markedthistopic',
3156              'movesection',
3157              'movecoursemodule',
3158              'movecoursesection',
3159              'movecontent',
3160              'tocontent',
3161              'emptydragdropregion',
3162              'afterresource',
3163              'aftersection',
3164              'totopofsection',
3165          ), 'moodle');
3166  
3167          // Include section-specific strings for formats which support sections.
3168          if (course_format_uses_sections($course->format)) {
3169              $PAGE->requires->strings_for_js(array(
3170                      'showfromothers',
3171                      'hidefromothers',
3172                  ), 'format_' . $course->format);
3173          }
3174  
3175          // For confirming resource deletion we need the name of the module in question.
3176          foreach ($usedmodules as $module => $modname) {
3177              $PAGE->requires->string_for_js('pluginname', $module);
3178          }
3179  
3180          // Load drag and drop upload AJAX.
3181          require_once($CFG->dirroot.'/course/dnduploadlib.php');
3182          dndupload_add_to_course($course, $enabledmodules);
3183      }
3184  
3185      $PAGE->requires->js_call_amd('core_course/actions', 'initCoursePage', array($course->format));
3186  
3187      return true;
3188  }
3189  
3190  /**
3191   * Include and configure the course editor modules.
3192   *
3193   * @param course_format $format the course format instance.
3194   */
3195  function include_course_editor(course_format $format) {
3196      global $PAGE, $SITE;
3197  
3198      $course = $format->get_course();
3199  
3200      if ($SITE->id === $course->id) {
3201          return;
3202      }
3203  
3204      $statekey = course_format::session_cache($course);
3205  
3206      // Edition mode and some format specs must be passed to the init method.
3207      $setup = (object)[
3208          'editing' => $format->show_editor(),
3209          'supportscomponents' => $format->supports_components(),
3210          'statekey' => $statekey,
3211          'overriddenStrings' => $format->get_editor_custom_strings(),
3212      ];
3213      // All the new editor elements will be loaded after the course is presented and
3214      // the initial course state will be generated using core_course_get_state webservice.
3215      $PAGE->requires->js_call_amd('core_courseformat/courseeditor', 'setViewFormat', [$course->id, $setup]);
3216  }
3217  
3218  /**
3219   * Returns the sorted list of available course formats, filtered by enabled if necessary
3220   *
3221   * @param bool $enabledonly return only formats that are enabled
3222   * @return array array of sorted format names
3223   */
3224  function get_sorted_course_formats($enabledonly = false) {
3225      global $CFG;
3226      $formats = core_component::get_plugin_list('format');
3227  
3228      if (!empty($CFG->format_plugins_sortorder)) {
3229          $order = explode(',', $CFG->format_plugins_sortorder);
3230          $order = array_merge(array_intersect($order, array_keys($formats)),
3231                      array_diff(array_keys($formats), $order));
3232      } else {
3233          $order = array_keys($formats);
3234      }
3235      if (!$enabledonly) {
3236          return $order;
3237      }
3238      $sortedformats = array();
3239      foreach ($order as $formatname) {
3240          if (!get_config('format_'.$formatname, 'disabled')) {
3241              $sortedformats[] = $formatname;
3242          }
3243      }
3244      return $sortedformats;
3245  }
3246  
3247  /**
3248   * The URL to use for the specified course (with section)
3249   *
3250   * @param int|stdClass $courseorid The course to get the section name for (either object or just course id)
3251   * @param int|stdClass $section Section object from database or just field course_sections.section
3252   *     if omitted the course view page is returned
3253   * @param array $options options for view URL. At the moment core uses:
3254   *     'navigation' (bool) if true and section has no separate page, the function returns null
3255   *     'sr' (int) used by multipage formats to specify to which section to return
3256   * @return moodle_url The url of course
3257   */
3258  function course_get_url($courseorid, $section = null, $options = array()) {
3259      return course_get_format($courseorid)->get_view_url($section, $options);
3260  }
3261  
3262  /**
3263   * Create a module.
3264   *
3265   * It includes:
3266   *      - capability checks and other checks
3267   *      - create the module from the module info
3268   *
3269   * @param object $module
3270   * @return object the created module info
3271   * @throws moodle_exception if user is not allowed to perform the action or module is not allowed in this course
3272   */
3273  function create_module($moduleinfo) {
3274      global $DB, $CFG;
3275  
3276      require_once($CFG->dirroot . '/course/modlib.php');
3277  
3278      // Check manadatory attributs.
3279      $mandatoryfields = array('modulename', 'course', 'section', 'visible');
3280      if (plugin_supports('mod', $moduleinfo->modulename, FEATURE_MOD_INTRO, true)) {
3281          $mandatoryfields[] = 'introeditor';
3282      }
3283      foreach($mandatoryfields as $mandatoryfield) {
3284          if (!isset($moduleinfo->{$mandatoryfield})) {
3285              throw new moodle_exception('createmodulemissingattribut', '', '', $mandatoryfield);
3286          }
3287      }
3288  
3289      // Some additional checks (capability / existing instances).
3290      $course = $DB->get_record('course', array('id'=>$moduleinfo->course), '*', MUST_EXIST);
3291      list($module, $context, $cw) = can_add_moduleinfo($course, $moduleinfo->modulename, $moduleinfo->section);
3292  
3293      // Add the module.
3294      $moduleinfo->module = $module->id;
3295      $moduleinfo = add_moduleinfo($moduleinfo, $course, null);
3296  
3297      return $moduleinfo;
3298  }
3299  
3300  /**
3301   * Update a module.
3302   *
3303   * It includes:
3304   *      - capability and other checks
3305   *      - update the module
3306   *
3307   * @param object $module
3308   * @return object the updated module info
3309   * @throws moodle_exception if current user is not allowed to update the module
3310   */
3311  function update_module($moduleinfo) {
3312      global $DB, $CFG;
3313  
3314      require_once($CFG->dirroot . '/course/modlib.php');
3315  
3316      // Check the course module exists.
3317      $cm = get_coursemodule_from_id('', $moduleinfo->coursemodule, 0, false, MUST_EXIST);
3318  
3319      // Check the course exists.
3320      $course = $DB->get_record('course', array('id'=>$cm->course), '*', MUST_EXIST);
3321  
3322      // Some checks (capaibility / existing instances).
3323      list($cm, $context, $module, $data, $cw) = can_update_moduleinfo($cm);
3324  
3325      // Retrieve few information needed by update_moduleinfo.
3326      $moduleinfo->modulename = $cm->modname;
3327      if (!isset($moduleinfo->scale)) {
3328          $moduleinfo->scale = 0;
3329      }
3330      $moduleinfo->type = 'mod';
3331  
3332      // Update the module.
3333      list($cm, $moduleinfo) = update_moduleinfo($cm, $moduleinfo, $course, null);
3334  
3335      return $moduleinfo;
3336  }
3337  
3338  /**
3339   * Duplicate a module on the course for ajax.
3340   *
3341   * @see mod_duplicate_module()
3342   * @param object $course The course
3343   * @param object $cm The course module to duplicate
3344   * @param int $sr The section to link back to (used for creating the links)
3345   * @throws moodle_exception if the plugin doesn't support duplication
3346   * @return Object containing:
3347   * - fullcontent: The HTML markup for the created CM
3348   * - cmid: The CMID of the newly created CM
3349   * - redirect: Whether to trigger a redirect following this change
3350   */
3351  function mod_duplicate_activity($course, $cm, $sr = null) {
3352      global $PAGE;
3353  
3354      $newcm = duplicate_module($course, $cm);
3355  
3356      $resp = new stdClass();
3357      if ($newcm) {
3358  
3359          $format = course_get_format($course);
3360          $renderer = $format->get_renderer($PAGE);
3361          $modinfo = $format->get_modinfo();
3362          $section = $modinfo->get_section_info($newcm->sectionnum);
3363  
3364          // Get the new element html content.
3365          $resp->fullcontent = $renderer->course_section_updated_cm_item($format, $section, $newcm);
3366  
3367          $resp->cmid = $newcm->id;
3368      } else {
3369          // Trigger a redirect.
3370          $resp->redirect = true;
3371      }
3372      return $resp;
3373  }
3374  
3375  /**
3376   * Api to duplicate a module.
3377   *
3378   * @param object $course course object.
3379   * @param object $cm course module object to be duplicated.
3380   * @param int $sectionid section ID new course module will be placed in.
3381   * @param bool $changename updates module name with text from duplicatedmodule lang string.
3382   * @since Moodle 2.8
3383   *
3384   * @throws Exception
3385   * @throws coding_exception
3386   * @throws moodle_exception
3387   * @throws restore_controller_exception
3388   *
3389   * @return cm_info|null cminfo object if we sucessfully duplicated the mod and found the new cm.
3390   */
3391  function duplicate_module($course, $cm, int $sectionid = null, bool $changename = true): ?cm_info {
3392      global $CFG, $DB, $USER;
3393      require_once($CFG->dirroot . '/backup/util/includes/backup_includes.php');
3394      require_once($CFG->dirroot . '/backup/util/includes/restore_includes.php');
3395      require_once($CFG->libdir . '/filelib.php');
3396  
3397      $a          = new stdClass();
3398      $a->modtype = get_string('modulename', $cm->modname);
3399      $a->modname = format_string($cm->name);
3400  
3401      if (!plugin_supports('mod', $cm->modname, FEATURE_BACKUP_MOODLE2)) {
3402          throw new moodle_exception('duplicatenosupport', 'error', '', $a);
3403      }
3404  
3405      // Backup the activity.
3406  
3407      $bc = new backup_controller(backup::TYPE_1ACTIVITY, $cm->id, backup::FORMAT_MOODLE,
3408              backup::INTERACTIVE_NO, backup::MODE_IMPORT, $USER->id);
3409  
3410      $backupid       = $bc->get_backupid();
3411      $backupbasepath = $bc->get_plan()->get_basepath();
3412  
3413      $bc->execute_plan();
3414  
3415      $bc->destroy();
3416  
3417      // Restore the backup immediately.
3418  
3419      $rc = new restore_controller($backupid, $course->id,
3420              backup::INTERACTIVE_NO, backup::MODE_IMPORT, $USER->id, backup::TARGET_CURRENT_ADDING);
3421  
3422      // Make sure that the restore_general_groups setting is always enabled when duplicating an activity.
3423      $plan = $rc->get_plan();
3424      $groupsetting = $plan->get_setting('groups');
3425      if (empty($groupsetting->get_value())) {
3426          $groupsetting->set_value(true);
3427      }
3428  
3429      $cmcontext = context_module::instance($cm->id);
3430      if (!$rc->execute_precheck()) {
3431          $precheckresults = $rc->get_precheck_results();
3432          if (is_array($precheckresults) && !empty($precheckresults['errors'])) {
3433              if (empty($CFG->keeptempdirectoriesonbackup)) {
3434                  fulldelete($backupbasepath);
3435              }
3436          }
3437      }
3438  
3439      $rc->execute_plan();
3440  
3441      // Now a bit hacky part follows - we try to get the cmid of the newly
3442      // restored copy of the module.
3443      $newcmid = null;
3444      $tasks = $rc->get_plan()->get_tasks();
3445      foreach ($tasks as $task) {
3446          if (is_subclass_of($task, 'restore_activity_task')) {
3447              if ($task->get_old_contextid() == $cmcontext->id) {
3448                  $newcmid = $task->get_moduleid();
3449                  break;
3450              }
3451          }
3452      }
3453  
3454      $rc->destroy();
3455  
3456      if (empty($CFG->keeptempdirectoriesonbackup)) {
3457          fulldelete($backupbasepath);
3458      }
3459  
3460      // If we know the cmid of the new course module, let us move it
3461      // right below the original one. otherwise it will stay at the
3462      // end of the section.
3463      if ($newcmid) {
3464          // Proceed with activity renaming before everything else. We don't use APIs here to avoid
3465          // triggering a lot of create/update duplicated events.
3466          $newcm = get_coursemodule_from_id($cm->modname, $newcmid, $cm->course);
3467          if ($changename) {
3468              // Add ' (copy)' language string postfix to duplicated module.
3469              $newname = get_string('duplicatedmodule', 'moodle', $newcm->name);
3470              set_coursemodule_name($newcm->id, $newname);
3471          }
3472  
3473          $section = $DB->get_record('course_sections', ['id' => $sectionid ?? $cm->section, 'course' => $cm->course]);
3474          if (isset($sectionid)) {
3475              moveto_module($newcm, $section);
3476          } else {
3477              $modarray = explode(",", trim($section->sequence));
3478              $cmindex = array_search($cm->id, $modarray);
3479              if ($cmindex !== false && $cmindex < count($modarray) - 1) {
3480                  moveto_module($newcm, $section, $modarray[$cmindex + 1]);
3481              }
3482          }
3483  
3484          // Update calendar events with the duplicated module.
3485          // The following line is to be removed in MDL-58906.
3486          course_module_update_calendar_events($newcm->modname, null, $newcm);
3487  
3488          // Trigger course module created event. We can trigger the event only if we know the newcmid.
3489          $newcm = get_fast_modinfo($cm->course)->get_cm($newcmid);
3490          $event = \core\event\course_module_created::create_from_cm($newcm);
3491          $event->trigger();
3492      }
3493  
3494      return isset($newcm) ? $newcm : null;
3495  }
3496  
3497  /**
3498   * Compare two objects to find out their correct order based on timestamp (to be used by usort).
3499   * Sorts by descending order of time.
3500   *
3501   * @param stdClass $a First object
3502   * @param stdClass $b Second object
3503   * @return int 0,1,-1 representing the order
3504   */
3505  function compare_activities_by_time_desc($a, $b) {
3506      // Make sure the activities actually have a timestamp property.
3507      if ((!property_exists($a, 'timestamp')) && (!property_exists($b, 'timestamp'))) {
3508          return 0;
3509      }
3510      // We treat instances without timestamp as if they have a timestamp of 0.
3511      if ((!property_exists($a, 'timestamp')) && (property_exists($b,'timestamp'))) {
3512          return 1;
3513      }
3514      if ((property_exists($a, 'timestamp')) && (!property_exists($b, 'timestamp'))) {
3515          return -1;
3516      }
3517      if ($a->timestamp == $b->timestamp) {
3518          return 0;
3519      }
3520      return ($a->timestamp > $b->timestamp) ? -1 : 1;
3521  }
3522  
3523  /**
3524   * Compare two objects to find out their correct order based on timestamp (to be used by usort).
3525   * Sorts by ascending order of time.
3526   *
3527   * @param stdClass $a First object
3528   * @param stdClass $b Second object
3529   * @return int 0,1,-1 representing the order
3530   */
3531  function compare_activities_by_time_asc($a, $b) {
3532      // Make sure the activities actually have a timestamp property.
3533      if ((!property_exists($a, 'timestamp')) && (!property_exists($b, 'timestamp'))) {
3534        return 0;
3535      }
3536      // We treat instances without timestamp as if they have a timestamp of 0.
3537      if ((!property_exists($a, 'timestamp')) && (property_exists($b, 'timestamp'))) {
3538          return -1;
3539      }
3540      if ((property_exists($a, 'timestamp')) && (!property_exists($b, 'timestamp'))) {
3541          return 1;
3542      }
3543      if ($a->timestamp == $b->timestamp) {
3544          return 0;
3545      }
3546      return ($a->timestamp < $b->timestamp) ? -1 : 1;
3547  }
3548  
3549  /**
3550   * Changes the visibility of a course.
3551   *
3552   * @param int $courseid The course to change.
3553   * @param bool $show True to make it visible, false otherwise.
3554   * @return bool
3555   */
3556  function course_change_visibility($courseid, $show = true) {
3557      $course = new stdClass;
3558      $course->id = $courseid;
3559      $course->visible = ($show) ? '1' : '0';
3560      $course->visibleold = $course->visible;
3561      update_course($course);
3562      return true;
3563  }
3564  
3565  /**
3566   * Changes the course sortorder by one, moving it up or down one in respect to sort order.
3567   *
3568   * @param stdClass|core_course_list_element $course
3569   * @param bool $up If set to true the course will be moved up one. Otherwise down one.
3570   * @return bool
3571   */
3572  function course_change_sortorder_by_one($course, $up) {
3573      global $DB;
3574      $params = array($course->sortorder, $course->category);
3575      if ($up) {
3576          $select = 'sortorder < ? AND category = ?';
3577          $sort = 'sortorder DESC';
3578      } else {
3579          $select = 'sortorder > ? AND category = ?';
3580          $sort = 'sortorder ASC';
3581      }
3582      fix_course_sortorder();
3583      $swapcourse = $DB->get_records_select('course', $select, $params, $sort, '*', 0, 1);
3584      if ($swapcourse) {
3585          $swapcourse = reset($swapcourse);
3586          $DB->set_field('course', 'sortorder', $swapcourse->sortorder, array('id' => $course->id));
3587          $DB->set_field('course', 'sortorder', $course->sortorder, array('id' => $swapcourse->id));
3588          // Finally reorder courses.
3589          fix_course_sortorder();
3590          cache_helper::purge_by_event('changesincourse');
3591          return true;
3592      }
3593      return false;
3594  }
3595  
3596  /**
3597   * Changes the sort order of courses in a category so that the first course appears after the second.
3598   *
3599   * @param int|stdClass $courseorid The course to focus on.
3600   * @param int $moveaftercourseid The course to shifter after or 0 if you want it to be the first course in the category.
3601   * @return bool
3602   */
3603  function course_change_sortorder_after_course($courseorid, $moveaftercourseid) {
3604      global $DB;
3605  
3606      if (!is_object($courseorid)) {
3607          $course = get_course($courseorid);
3608      } else {
3609          $course = $courseorid;
3610      }
3611  
3612      if ((int)$moveaftercourseid === 0) {
3613          // We've moving the course to the start of the queue.
3614          $sql = 'SELECT sortorder
3615                        FROM {course}
3616                       WHERE category = :categoryid
3617                    ORDER BY sortorder';
3618          $params = array(
3619              'categoryid' => $course->category
3620          );
3621          $sortorder = $DB->get_field_sql($sql, $params, IGNORE_MULTIPLE);
3622  
3623          $sql = 'UPDATE {course}
3624                     SET sortorder = sortorder + 1
3625                   WHERE category = :categoryid
3626                     AND id <> :id';
3627          $params = array(
3628              'categoryid' => $course->category,
3629              'id' => $course->id,
3630          );
3631          $DB->execute($sql, $params);
3632          $DB->set_field('course', 'sortorder', $sortorder, array('id' => $course->id));
3633      } else if ($course->id === $moveaftercourseid) {
3634          // They're the same - moronic.
3635          debugging("Invalid move after course given.", DEBUG_DEVELOPER);
3636          return false;
3637      } else {
3638          // Moving this course after the given course. It could be before it could be after.
3639          $moveaftercourse = get_course($moveaftercourseid);
3640          if ($course->category !== $moveaftercourse->category) {
3641              debugging("Cannot re-order courses. The given courses do not belong to the same category.", DEBUG_DEVELOPER);
3642              return false;
3643          }
3644          // Increment all courses in the same category that are ordered after the moveafter course.
3645          // This makes a space for the course we're moving.
3646          $sql = 'UPDATE {course}
3647                         SET sortorder = sortorder + 1
3648                       WHERE category = :categoryid
3649                         AND sortorder > :sortorder';
3650          $params = array(
3651              'categoryid' => $moveaftercourse->category,
3652              'sortorder' => $moveaftercourse->sortorder
3653          );
3654          $DB->execute($sql, $params);
3655          $DB->set_field('course', 'sortorder', $moveaftercourse->sortorder + 1, array('id' => $course->id));
3656      }
3657      fix_course_sortorder();
3658      cache_helper::purge_by_event('changesincourse');
3659      return true;
3660  }
3661  
3662  /**
3663   * Trigger course viewed event. This API function is used when course view actions happens,
3664   * usually in course/view.php but also in external functions.
3665   *
3666   * @param stdClass  $context course context object
3667   * @param int $sectionnumber section number
3668   * @since Moodle 2.9
3669   */
3670  function course_view($context, $sectionnumber = 0) {
3671  
3672      $eventdata = array('context' => $context);
3673  
3674      if (!empty($sectionnumber)) {
3675          $eventdata['other']['coursesectionnumber'] = $sectionnumber;
3676      }
3677  
3678      $event = \core\event\course_viewed::create($eventdata);
3679      $event->trigger();
3680  
3681      user_accesstime_log($context->instanceid);
3682  }
3683  
3684  /**
3685   * Returns courses tagged with a specified tag.
3686   *
3687   * @param core_tag_tag $tag
3688   * @param bool $exclusivemode if set to true it means that no other entities tagged with this tag
3689   *             are displayed on the page and the per-page limit may be bigger
3690   * @param int $fromctx context id where the link was displayed, may be used by callbacks
3691   *            to display items in the same context first
3692   * @param int $ctx context id where to search for records
3693   * @param bool $rec search in subcontexts as well
3694   * @param int $page 0-based number of page being displayed
3695   * @return \core_tag\output\tagindex
3696   */
3697  function course_get_tagged_courses($tag, $exclusivemode = false, $fromctx = 0, $ctx = 0, $rec = 1, $page = 0) {
3698      global $CFG, $PAGE;
3699  
3700      $perpage = $exclusivemode ? $CFG->coursesperpage : 5;
3701      $displayoptions = array(
3702          'limit' => $perpage,
3703          'offset' => $page * $perpage,
3704          'viewmoreurl' => null,
3705      );
3706  
3707      $courserenderer = $PAGE->get_renderer('core', 'course');
3708      $totalcount = core_course_category::search_courses_count(array('tagid' => $tag->id, 'ctx' => $ctx, 'rec' => $rec));
3709      $content = $courserenderer->tagged_courses($tag->id, $exclusivemode, $ctx, $rec, $displayoptions);
3710      $totalpages = ceil($totalcount / $perpage);
3711  
3712      return new core_tag\output\tagindex($tag, 'core', 'course', $content,
3713              $exclusivemode, $fromctx, $ctx, $rec, $page, $totalpages);
3714  }
3715  
3716  /**
3717   * Implements callback inplace_editable() allowing to edit values in-place
3718   *
3719   * @param string $itemtype
3720   * @param int $itemid
3721   * @param mixed $newvalue
3722   * @return \core\output\inplace_editable
3723   */
3724  function core_course_inplace_editable($itemtype, $itemid, $newvalue) {
3725      if ($itemtype === 'activityname') {
3726          return \core_courseformat\output\local\content\cm\title::update($itemid, $newvalue);
3727      }
3728  }
3729  
3730  /**
3731   * This function calculates the minimum and maximum cutoff values for the timestart of
3732   * the given event.
3733   *
3734   * It will return an array with two values, the first being the minimum cutoff value and
3735   * the second being the maximum cutoff value. Either or both values can be null, which
3736   * indicates there is no minimum or maximum, respectively.
3737   *
3738   * If a cutoff is required then the function must return an array containing the cutoff
3739   * timestamp and error string to display to the user if the cutoff value is violated.
3740   *
3741   * A minimum and maximum cutoff return value will look like:
3742   * [
3743   *     [1505704373, 'The date must be after this date'],
3744   *     [1506741172, 'The date must be before this date']
3745   * ]
3746   *
3747   * @param calendar_event $event The calendar event to get the time range for
3748   * @param stdClass $course The course object to get the range from
3749   * @return array Returns an array with min and max date.
3750   */
3751  function core_course_core_calendar_get_valid_event_timestart_range(\calendar_event $event, $course) {
3752      $mindate = null;
3753      $maxdate = null;
3754  
3755      if ($course->startdate) {
3756          $mindate = [
3757              $course->startdate,
3758              get_string('errorbeforecoursestart', 'calendar')
3759          ];
3760      }
3761  
3762      return [$mindate, $maxdate];
3763  }
3764  
3765  /**
3766   * Render the message drawer to be included in the top of the body of each page.
3767   *
3768   * @return string HTML
3769   */
3770  function core_course_drawer(): string {
3771      global $PAGE;
3772  
3773      // Only add course index on non-site course pages.
3774      if (!$PAGE->course || $PAGE->course->id == SITEID) {
3775          return '';
3776      }
3777  
3778      // Show course index to users can access the course only.
3779      if (!can_access_course($PAGE->course, null, '', true)) {
3780          return '';
3781      }
3782  
3783      $format = course_get_format($PAGE->course);
3784      $renderer = $format->get_renderer($PAGE);
3785      if (method_exists($renderer, 'course_index_drawer')) {
3786          return $renderer->course_index_drawer($format);
3787      }
3788  
3789      return '';
3790  }
3791  
3792  /**
3793   * Returns course modules tagged with a specified tag ready for output on tag/index.php page
3794   *
3795   * This is a callback used by the tag area core/course_modules to search for course modules
3796   * tagged with a specific tag.
3797   *
3798   * @param core_tag_tag $tag
3799   * @param bool $exclusivemode if set to true it means that no other entities tagged with this tag
3800   *             are displayed on the page and the per-page limit may be bigger
3801   * @param int $fromcontextid context id where the link was displayed, may be used by callbacks
3802   *            to display items in the same context first
3803   * @param int $contextid context id where to search for records
3804   * @param bool $recursivecontext search in subcontexts as well
3805   * @param int $page 0-based number of page being displayed
3806   * @return \core_tag\output\tagindex
3807   */
3808  function course_get_tagged_course_modules($tag, $exclusivemode = false, $fromcontextid = 0, $contextid = 0,
3809                                            $recursivecontext = 1, $page = 0) {
3810      global $OUTPUT;
3811      $perpage = $exclusivemode ? 20 : 5;
3812  
3813      // Build select query.
3814      $ctxselect = context_helper::get_preload_record_columns_sql('ctx');
3815      $query = "SELECT cm.id AS cmid, c.id AS courseid, $ctxselect
3816                  FROM {course_modules} cm
3817                  JOIN {tag_instance} tt ON cm.id = tt.itemid
3818                  JOIN {course} c ON cm.course = c.id
3819                  JOIN {context} ctx ON ctx.instanceid = cm.id AND ctx.contextlevel = :coursemodulecontextlevel
3820                 WHERE tt.itemtype = :itemtype AND tt.tagid = :tagid AND tt.component = :component
3821                  AND cm.deletioninprogress = 0
3822                  AND c.id %COURSEFILTER% AND cm.id %ITEMFILTER%";
3823  
3824      $params = array('itemtype' => 'course_modules', 'tagid' => $tag->id, 'component' => 'core',
3825          'coursemodulecontextlevel' => CONTEXT_MODULE);
3826      if ($contextid) {
3827          $context = context::instance_by_id($contextid);
3828          $query .= $recursivecontext ? ' AND (ctx.id = :contextid OR ctx.path LIKE :path)' : ' AND ctx.id = :contextid';
3829          $params['contextid'] = $context->id;
3830          $params['path'] = $context->path.'/%';
3831      }
3832  
3833      $query .= ' ORDER BY';
3834      if ($fromcontextid) {
3835          // In order-clause specify that modules from inside "fromctx" context should be returned first.
3836          $fromcontext = context::instance_by_id($fromcontextid);
3837          $query .= ' (CASE WHEN ctx.id = :fromcontextid OR ctx.path LIKE :frompath THEN 0 ELSE 1 END),';
3838          $params['fromcontextid'] = $fromcontext->id;
3839          $params['frompath'] = $fromcontext->path.'/%';
3840      }
3841      $query .= ' c.sortorder, cm.id';
3842      $totalpages = $page + 1;
3843  
3844      // Use core_tag_index_builder to build and filter the list of items.
3845      // Request one item more than we need so we know if next page exists.
3846      $builder = new core_tag_index_builder('core', 'course_modules', $query, $params, $page * $perpage, $perpage + 1);
3847      while ($item = $builder->has_item_that_needs_access_check()) {
3848          context_helper::preload_from_record($item);
3849          $courseid = $item->courseid;
3850          if (!$builder->can_access_course($courseid)) {
3851              $builder->set_accessible($item, false);
3852              continue;
3853          }
3854          $modinfo = get_fast_modinfo($builder->get_course($courseid));
3855          // Set accessibility of this item and all other items in the same course.
3856          $builder->walk(function ($taggeditem) use ($courseid, $modinfo, $builder) {
3857              if ($taggeditem->courseid == $courseid) {
3858                  $cm = $modinfo->get_cm($taggeditem->cmid);
3859                  $builder->set_accessible($taggeditem, $cm->uservisible);
3860              }
3861          });
3862      }
3863  
3864      $items = $builder->get_items();
3865      if (count($items) > $perpage) {
3866          $totalpages = $page + 2; // We don't need exact page count, just indicate that the next page exists.
3867          array_pop($items);
3868      }
3869  
3870      // Build the display contents.
3871      if ($items) {
3872          $tagfeed = new core_tag\output\tagfeed();
3873          foreach ($items as $item) {
3874              context_helper::preload_from_record($item);
3875              $course = $builder->get_course($item->courseid);
3876              $modinfo = get_fast_modinfo($course);
3877              $cm = $modinfo->get_cm($item->cmid);
3878              $courseurl = course_get_url($item->courseid, $cm->sectionnum);
3879              $cmname = $cm->get_formatted_name();
3880              if (!$exclusivemode) {
3881                  $cmname = shorten_text($cmname, 100);
3882              }
3883              $cmname = html_writer::link($cm->url?:$courseurl, $cmname);
3884              $coursename = format_string($course->fullname, true,
3885                      array('context' => context_course::instance($item->courseid)));
3886              $coursename = html_writer::link($courseurl, $coursename);
3887              $icon = html_writer::empty_tag('img', array('src' => $cm->get_icon_url()));
3888              $tagfeed->add($icon, $cmname, $coursename);
3889          }
3890  
3891          $content = $OUTPUT->render_from_template('core_tag/tagfeed',
3892                  $tagfeed->export_for_template($OUTPUT));
3893  
3894          return new core_tag\output\tagindex($tag, 'core', 'course_modules', $content,
3895                  $exclusivemode, $fromcontextid, $contextid, $recursivecontext, $page, $totalpages);
3896      }
3897  }
3898  
3899  /**
3900   * Return an object with the list of navigation options in a course that are avaialable or not for the current user.
3901   * This function also handles the frontpage course.
3902   *
3903   * @param  stdClass $context context object (it can be a course context or the system context for frontpage settings)
3904   * @param  stdClass $course  the course where the settings are being rendered
3905   * @return stdClass          the navigation options in a course and their availability status
3906   * @since  Moodle 3.2
3907   */
3908  function course_get_user_navigation_options($context, $course = null) {
3909      global $CFG, $USER;
3910  
3911      $isloggedin = isloggedin();
3912      $isguestuser = isguestuser();
3913      $isfrontpage = $context->contextlevel == CONTEXT_SYSTEM;
3914  
3915      if ($isfrontpage) {
3916          $sitecontext = $context;
3917      } else {
3918          $sitecontext = context_system::instance();
3919      }
3920  
3921      // Sets defaults for all options.
3922      $options = (object) [
3923          'badges' => false,
3924          'blogs' => false,
3925          'competencies' => false,
3926          'grades' => false,
3927          'notes' => false,
3928          'participants' => false,
3929          'search' => false,
3930          'tags' => false,
3931      ];
3932  
3933      $options->blogs = !empty($CFG->enableblogs) &&
3934                          ($CFG->bloglevel == BLOG_GLOBAL_LEVEL ||
3935                          ($CFG->bloglevel == BLOG_SITE_LEVEL and ($isloggedin and !$isguestuser)))
3936                          && has_capability('moodle/blog:view', $sitecontext);
3937  
3938      $options->notes = !empty($CFG->enablenotes) && has_any_capability(array('moodle/notes:manage', 'moodle/notes:view'), $context);
3939  
3940      // Frontpage settings?
3941      if ($isfrontpage) {
3942          // We are on the front page, so make sure we use the proper capability (site:viewparticipants).
3943          $options->participants = course_can_view_participants($sitecontext);
3944          $options->badges = !empty($CFG->enablebadges) && has_capability('moodle/badges:viewbadges', $sitecontext);
3945          $options->tags = !empty($CFG->usetags) && $isloggedin;
3946          $options->search = !empty($CFG->enableglobalsearch) && has_capability('moodle/search:query', $sitecontext);
3947      } else {
3948          // We are in a course, so make sure we use the proper capability (course:viewparticipants).
3949          $options->participants = course_can_view_participants($context);
3950  
3951          // Only display badges if they are enabled and the current user can manage them or if they can view them and have,
3952          // at least, one available badge.
3953          if (!empty($CFG->enablebadges) && !empty($CFG->badges_allowcoursebadges)) {
3954              $canmanage = has_any_capability([
3955                      'moodle/badges:createbadge',
3956                      'moodle/badges:awardbadge',
3957                      'moodle/badges:configurecriteria',
3958                      'moodle/badges:configuremessages',
3959                      'moodle/badges:configuredetails',
3960                      'moodle/badges:deletebadge',
3961                  ],
3962                  $context
3963              );
3964              $totalbadges = [];
3965              $canview = false;
3966              if (!$canmanage) {
3967                  // This only needs to be calculated if the user can't manage badges (to improve performance).
3968                  $canview = has_capability('moodle/badges:viewbadges', $context);
3969                  if ($canview) {
3970                      require_once($CFG->dirroot.'/lib/badgeslib.php');
3971                      if (is_null($course)) {
3972                          $totalbadges = count(badges_get_badges(BADGE_TYPE_SITE, 0, '', '', 0, 0, $USER->id));
3973                      } else {
3974                          $totalbadges = count(badges_get_badges(BADGE_TYPE_COURSE, $course->id, '', '', 0, 0, $USER->id));
3975                      }
3976                  }
3977              }
3978  
3979              $options->badges = ($canmanage || ($canview && $totalbadges > 0));
3980          }
3981          // Add view grade report is permitted.
3982          $grades = false;
3983  
3984          if (has_capability('moodle/grade:viewall', $context)) {
3985              $grades = true;
3986          } else if (!empty($course->showgrades)) {
3987              $reports = core_component::get_plugin_list('gradereport');
3988              if (is_array($reports) && count($reports) > 0) {  // Get all installed reports.
3989                  arsort($reports);   // User is last, we want to test it first.
3990                  foreach ($reports as $plugin => $plugindir) {
3991                      if (has_capability('gradereport/'.$plugin.':view', $context)) {
3992                          // Stop when the first visible plugin is found.
3993                          $grades = true;
3994                          break;
3995                      }
3996                  }
3997              }
3998          }
3999          $options->grades = $grades;
4000      }
4001  
4002      if (\core_competency\api::is_enabled()) {
4003          $capabilities = array('moodle/competency:coursecompetencyview', 'moodle/competency:coursecompetencymanage');
4004          $options->competencies = has_any_capability($capabilities, $context);
4005      }
4006      return $options;
4007  }
4008  
4009  /**
4010   * Return an object with the list of administration options in a course that are available or not for the current user.
4011   * This function also handles the frontpage settings.
4012   *
4013   * @param  stdClass $course  course object (for frontpage it should be a clone of $SITE)
4014   * @param  stdClass $context context object (course context)
4015   * @return stdClass          the administration options in a course and their availability status
4016   * @since  Moodle 3.2
4017   */
4018  function course_get_user_administration_options($course, $context) {
4019      global $CFG;
4020      $isfrontpage = $course->id == SITEID;
4021      $completionenabled = $CFG->enablecompletion && $course->enablecompletion;
4022      $hascompletionoptions = count(core_completion\manager::get_available_completion_options($course->id)) > 0;
4023      $options = new stdClass;
4024      $options->update = has_capability('moodle/course:update', $context);
4025      $options->editcompletion = $CFG->enablecompletion && $course->enablecompletion &&
4026          ($options->update || $hascompletionoptions);
4027      $options->filters = has_capability('moodle/filter:manage', $context) &&
4028                          count(filter_get_available_in_context($context)) > 0;
4029      $options->reports = has_capability('moodle/site:viewreports', $context);
4030      $options->backup = has_capability('moodle/backup:backupcourse', $context);
4031      $options->restore = has_capability('moodle/restore:restorecourse', $context);
4032      $options->copy = \core_course\management\helper::can_copy_course($course->id);
4033      $options->files = ($course->legacyfiles == 2 && has_capability('moodle/course:managefiles', $context));
4034  
4035      if (!$isfrontpage) {
4036          $options->tags = has_capability('moodle/course:tag', $context);
4037          $options->gradebook = has_capability('moodle/grade:manage', $context);
4038          $options->outcomes = !empty($CFG->enableoutcomes) && has_capability('moodle/course:update', $context);
4039          $options->badges = !empty($CFG->enablebadges);
4040          $options->import = has_capability('moodle/restore:restoretargetimport', $context);
4041          $options->reset = has_capability('moodle/course:reset', $context);
4042          $options->roles = has_capability('moodle/role:switchroles', $context);
4043      } else {
4044          // Set default options to false.
4045          $listofoptions = array('tags', 'gradebook', 'outcomes', 'badges', 'import', 'publish', 'reset', 'roles', 'grades');
4046  
4047          foreach ($listofoptions as $option) {
4048              $options->$option = false;
4049          }
4050      }
4051  
4052      return $options;
4053  }
4054  
4055  /**
4056   * Validates course start and end dates.
4057   *
4058   * Checks that the end course date is not greater than the start course date.
4059   *
4060   * $coursedata['startdate'] or $coursedata['enddate'] may not be set, it depends on the form and user input.
4061   *
4062   * @param array $coursedata May contain startdate and enddate timestamps, depends on the user input.
4063   * @return mixed False if everything alright, error codes otherwise.
4064   */
4065  function course_validate_dates($coursedata) {
4066  
4067      // If both start and end dates are set end date should be later than the start date.
4068      if (!empty($coursedata['startdate']) && !empty($coursedata['enddate']) &&
4069              ($coursedata['enddate'] < $coursedata['startdate'])) {
4070          return 'enddatebeforestartdate';
4071      }
4072  
4073      // If start date is not set end date can not be set.
4074      if (empty($coursedata['startdate']) && !empty($coursedata['enddate'])) {
4075          return 'nostartdatenoenddate';
4076      }
4077  
4078      return false;
4079  }
4080  
4081  /**
4082   * Check for course updates in the given context level instances (only modules supported right Now)
4083   *
4084   * @param  stdClass $course  course object
4085   * @param  array $tocheck    instances to check for updates
4086   * @param  array $filter check only for updates in these areas
4087   * @return array list of warnings and instances with updates information
4088   * @since  Moodle 3.2
4089   */
4090  function course_check_updates($course, $tocheck, $filter = array()) {
4091      global $CFG, $DB;
4092  
4093      $instances = array();
4094      $warnings = array();
4095      $modulescallbacksupport = array();
4096      $modinfo = get_fast_modinfo($course);
4097  
4098      $supportedplugins = get_plugin_list_with_function('mod', 'check_updates_since');
4099  
4100      // Check instances.
4101      foreach ($tocheck as $instance) {
4102          if ($instance['contextlevel'] == 'module') {
4103              // Check module visibility.
4104              try {
4105                  $cm = $modinfo->get_cm($instance['id']);
4106              } catch (Exception $e) {
4107                  $warnings[] = array(
4108                      'item' => 'module',
4109                      'itemid' => $instance['id'],
4110                      'warningcode' => 'cmidnotincourse',
4111                      'message' => 'This module id does not belong to this course.'
4112                  );
4113                  continue;
4114              }
4115  
4116              if (!$cm->uservisible) {
4117                  $warnings[] = array(
4118                      'item' => 'module',
4119                      'itemid' => $instance['id'],
4120                      'warningcode' => 'nonuservisible',
4121                      'message' => 'You don\'t have access to this module.'
4122                  );
4123                  continue;
4124              }
4125              if (empty($supportedplugins['mod_' . $cm->modname])) {
4126                  $warnings[] = array(
4127                      'item' => 'module',
4128                      'itemid' => $instance['id'],
4129                      'warningcode' => 'missingcallback',
4130                      'message' => 'This module does not implement the check_updates_since callback: ' . $instance['contextlevel'],
4131                  );
4132                  continue;
4133              }
4134              // Retrieve the module instance.
4135              $instances[] = array(
4136                  'contextlevel' => $instance['contextlevel'],
4137                  'id' => $instance['id'],
4138                  'updates' => call_user_func($cm->modname . '_check_updates_since', $cm, $instance['since'], $filter)
4139              );
4140  
4141          } else {
4142              $warnings[] = array(
4143                  'item' => 'contextlevel',
4144                  'itemid' => $instance['id'],
4145                  'warningcode' => 'contextlevelnotsupported',
4146                  'message' => 'Context level not yet supported ' . $instance['contextlevel'],
4147              );
4148          }
4149      }
4150      return array($instances, $warnings);
4151  }
4152  
4153  /**
4154   * This function classifies a course as past, in progress or future.
4155   *
4156   * This function may incur a DB hit to calculate course completion.
4157   * @param stdClass $course Course record
4158   * @param stdClass $user User record (optional - defaults to $USER).
4159   * @param completion_info $completioninfo Completion record for the user (optional - will be fetched if required).
4160   * @return string (one of COURSE_TIMELINE_FUTURE, COURSE_TIMELINE_INPROGRESS or COURSE_TIMELINE_PAST)
4161   */
4162  function course_classify_for_timeline($course, $user = null, $completioninfo = null) {
4163      global $USER;
4164  
4165      if ($user == null) {
4166          $user = $USER;
4167      }
4168  
4169      if ($completioninfo == null) {
4170          $completioninfo = new completion_info($course);
4171      }
4172  
4173      // Let plugins override data for timeline classification.
4174      $pluginsfunction = get_plugins_with_function('extend_course_classify_for_timeline', 'lib.php');
4175      foreach ($pluginsfunction as $plugintype => $plugins) {
4176          foreach ($plugins as $pluginfunction) {
4177              $pluginfunction($course, $user, $completioninfo);
4178          }
4179      }
4180  
4181      $today = time();
4182      // End date past.
4183      if (!empty($course->enddate) && (course_classify_end_date($course) < $today)) {
4184          return COURSE_TIMELINE_PAST;
4185      }
4186  
4187      // Course was completed.
4188      if ($completioninfo->is_enabled() && $completioninfo->is_course_complete($user->id)) {
4189          return COURSE_TIMELINE_PAST;
4190      }
4191  
4192      // Start date not reached.
4193      if (!empty($course->startdate) && (course_classify_start_date($course) > $today)) {
4194          return COURSE_TIMELINE_FUTURE;
4195      }
4196  
4197      // Everything else is in progress.
4198      return COURSE_TIMELINE_INPROGRESS;
4199  }
4200  
4201  /**
4202   * This function calculates the end date to use for display classification purposes,
4203   * incorporating the grace period, if any.
4204   *
4205   * @param stdClass $course The course record.
4206   * @return int The new enddate.
4207   */
4208  function course_classify_end_date($course) {
4209      global $CFG;
4210      $coursegraceperiodafter = (empty($CFG->coursegraceperiodafter)) ? 0 : $CFG->coursegraceperiodafter;
4211      $enddate = (new \DateTimeImmutable())->setTimestamp($course->enddate)->modify("+{$coursegraceperiodafter} days");
4212      return $enddate->getTimestamp();
4213  }
4214  
4215  /**
4216   * This function calculates the start date to use for display classification purposes,
4217   * incorporating the grace period, if any.
4218   *
4219   * @param stdClass $course The course record.
4220   * @return int The new startdate.
4221   */
4222  function course_classify_start_date($course) {
4223      global $CFG;
4224      $coursegraceperiodbefore = (empty($CFG->coursegraceperiodbefore)) ? 0 : $CFG->coursegraceperiodbefore;
4225      $startdate = (new \DateTimeImmutable())->setTimestamp($course->startdate)->modify("-{$coursegraceperiodbefore} days");
4226      return $startdate->getTimestamp();
4227  }
4228  
4229  /**
4230   * Group a list of courses into either past, future, or in progress.
4231   *
4232   * The return value will be an array indexed by the COURSE_TIMELINE_* constants
4233   * with each value being an array of courses in that group.
4234   * E.g.
4235   * [
4236   *      COURSE_TIMELINE_PAST => [... list of past courses ...],
4237   *      COURSE_TIMELINE_FUTURE => [],
4238   *      COURSE_TIMELINE_INPROGRESS => []
4239   * ]
4240   *
4241   * @param array $courses List of courses to be grouped.
4242   * @return array
4243   */
4244  function course_classify_courses_for_timeline(array $courses) {
4245      return array_reduce($courses, function($carry, $course) {
4246          $classification = course_classify_for_timeline($course);
4247          array_push($carry[$classification], $course);
4248  
4249          return $carry;
4250      }, [
4251          COURSE_TIMELINE_PAST => [],
4252          COURSE_TIMELINE_FUTURE => [],
4253          COURSE_TIMELINE_INPROGRESS => []
4254      ]);
4255  }
4256  
4257  /**
4258   * Get the list of enrolled courses for the current user.
4259   *
4260   * This function returns a Generator. The courses will be loaded from the database
4261   * in chunks rather than a single query.
4262   *
4263   * @param int $limit Restrict result set to this amount
4264   * @param int $offset Skip this number of records from the start of the result set
4265   * @param string|null $sort SQL string for sorting
4266   * @param string|null $fields SQL string for fields to be returned
4267   * @param int $dbquerylimit The number of records to load per DB request
4268   * @param array $includecourses courses ids to be restricted
4269   * @param array $hiddencourses courses ids to be excluded
4270   * @return Generator
4271   */
4272  function course_get_enrolled_courses_for_logged_in_user(
4273      int $limit = 0,
4274      int $offset = 0,
4275      string $sort = null,
4276      string $fields = null,
4277      int $dbquerylimit = COURSE_DB_QUERY_LIMIT,
4278      array $includecourses = [],
4279      array $hiddencourses = []
4280  ) : Generator {
4281  
4282      $haslimit = !empty($limit);
4283      $recordsloaded = 0;
4284      $querylimit = (!$haslimit || $limit > $dbquerylimit) ? $dbquerylimit : $limit;
4285  
4286      while ($courses = enrol_get_my_courses($fields, $sort, $querylimit, $includecourses, false, $offset, $hiddencourses)) {
4287          yield from $courses;
4288  
4289          $recordsloaded += $querylimit;
4290  
4291          if (count($courses) < $querylimit) {
4292              break;
4293          }
4294          if ($haslimit && $recordsloaded >= $limit) {
4295              break;
4296          }
4297  
4298          $offset += $querylimit;
4299      }
4300  }
4301  
4302  /**
4303   * Get the list of enrolled courses the current user searched for.
4304   *
4305   * This function returns a Generator. The courses will be loaded from the database
4306   * in chunks rather than a single query.
4307   *
4308   * @param int $limit Restrict result set to this amount
4309   * @param int $offset Skip this number of records from the start of the result set
4310   * @param string|null $sort SQL string for sorting
4311   * @param string|null $fields SQL string for fields to be returned
4312   * @param int $dbquerylimit The number of records to load per DB request
4313   * @param array $searchcriteria contains search criteria
4314   * @param array $options display options, same as in get_courses() except 'recursive' is ignored -
4315   *                       search is always category-independent
4316   * @return Generator
4317   */
4318  function course_get_enrolled_courses_for_logged_in_user_from_search(
4319      int $limit = 0,
4320      int $offset = 0,
4321      string $sort = null,
4322      string $fields = null,
4323      int $dbquerylimit = COURSE_DB_QUERY_LIMIT,
4324      array $searchcriteria = [],
4325      array $options = []
4326  ) : Generator {
4327  
4328      $haslimit = !empty($limit);
4329      $recordsloaded = 0;
4330      $querylimit = (!$haslimit || $limit > $dbquerylimit) ? $dbquerylimit : $limit;
4331      $ids = core_course_category::search_courses($searchcriteria, $options);
4332  
4333      // If no courses were found matching the criteria return back.
4334      if (empty($ids)) {
4335          return;
4336      }
4337  
4338      while ($courses = enrol_get_my_courses($fields, $sort, $querylimit, $ids, false, $offset)) {
4339          yield from $courses;
4340  
4341          $recordsloaded += $querylimit;
4342  
4343          if (count($courses) < $querylimit) {
4344              break;
4345          }
4346          if ($haslimit && $recordsloaded >= $limit) {
4347              break;
4348          }
4349  
4350          $offset += $querylimit;
4351      }
4352  }
4353  
4354  /**
4355   * Search the given $courses for any that match the given $classification up to the specified
4356   * $limit.
4357   *
4358   * This function will return the subset of courses that match the classification as well as the
4359   * number of courses it had to process to build that subset.
4360   *
4361   * It is recommended that for larger sets of courses this function is given a Generator that loads
4362   * the courses from the database in chunks.
4363   *
4364   * @param array|Traversable $courses List of courses to process
4365   * @param string $classification One of the COURSE_TIMELINE_* constants
4366   * @param int $limit Limit the number of results to this amount
4367   * @return array First value is the filtered courses, second value is the number of courses processed
4368   */
4369  function course_filter_courses_by_timeline_classification(
4370      $courses,
4371      string $classification,
4372      int $limit = 0
4373  ) : array {
4374  
4375      if (!in_array($classification,
4376              [COURSE_TIMELINE_ALLINCLUDINGHIDDEN, COURSE_TIMELINE_ALL, COURSE_TIMELINE_PAST, COURSE_TIMELINE_INPROGRESS,
4377                  COURSE_TIMELINE_FUTURE, COURSE_TIMELINE_HIDDEN, COURSE_TIMELINE_SEARCH])) {
4378          $message = 'Classification must be one of COURSE_TIMELINE_ALLINCLUDINGHIDDEN, COURSE_TIMELINE_ALL, COURSE_TIMELINE_PAST, '
4379              . 'COURSE_TIMELINE_INPROGRESS, COURSE_TIMELINE_SEARCH or COURSE_TIMELINE_FUTURE';
4380          throw new moodle_exception($message);
4381      }
4382  
4383      $filteredcourses = [];
4384      $numberofcoursesprocessed = 0;
4385      $filtermatches = 0;
4386  
4387      foreach ($courses as $course) {
4388          $numberofcoursesprocessed++;
4389          $pref = get_user_preferences('block_myoverview_hidden_course_' . $course->id, 0);
4390  
4391          // Added as of MDL-63457 toggle viewability for each user.
4392          if ($classification == COURSE_TIMELINE_ALLINCLUDINGHIDDEN || ($classification == COURSE_TIMELINE_HIDDEN && $pref) ||
4393              $classification == COURSE_TIMELINE_SEARCH||
4394              (($classification == COURSE_TIMELINE_ALL || $classification == course_classify_for_timeline($course)) && !$pref)) {
4395              $filteredcourses[] = $course;
4396              $filtermatches++;
4397          }
4398  
4399          if ($limit && $filtermatches >= $limit) {
4400              // We've found the number of requested courses. No need to continue searching.
4401              break;
4402          }
4403      }
4404  
4405      // Return the number of filtered courses as well as the number of courses that were searched
4406      // in order to find the matching courses. This allows the calling code to do some kind of
4407      // pagination.
4408      return [$filteredcourses, $numberofcoursesprocessed];
4409  }
4410  
4411  /**
4412   * Search the given $courses for any that match the given $classification up to the specified
4413   * $limit.
4414   *
4415   * This function will return the subset of courses that are favourites as well as the
4416   * number of courses it had to process to build that subset.
4417   *
4418   * It is recommended that for larger sets of courses this function is given a Generator that loads
4419   * the courses from the database in chunks.
4420   *
4421   * @param array|Traversable $courses List of courses to process
4422   * @param array $favouritecourseids Array of favourite courses.
4423   * @param int $limit Limit the number of results to this amount
4424   * @return array First value is the filtered courses, second value is the number of courses processed
4425   */
4426  function course_filter_courses_by_favourites(
4427      $courses,
4428      $favouritecourseids,
4429      int $limit = 0
4430  ) : array {
4431  
4432      $filteredcourses = [];
4433      $numberofcoursesprocessed = 0;
4434      $filtermatches = 0;
4435  
4436      foreach ($courses as $course) {
4437          $numberofcoursesprocessed++;
4438  
4439          if (in_array($course->id, $favouritecourseids)) {
4440              $filteredcourses[] = $course;
4441              $filtermatches++;
4442          }
4443  
4444          if ($limit && $filtermatches >= $limit) {
4445              // We've found the number of requested courses. No need to continue searching.
4446              break;
4447          }
4448      }
4449  
4450      // Return the number of filtered courses as well as the number of courses that were searched
4451      // in order to find the matching courses. This allows the calling code to do some kind of
4452      // pagination.
4453      return [$filteredcourses, $numberofcoursesprocessed];
4454  }
4455  
4456  /**
4457   * Search the given $courses for any that have a $customfieldname value that matches the given
4458   * $customfieldvalue, up to the specified $limit.
4459   *
4460   * This function will return the subset of courses that matches the value as well as the
4461   * number of courses it had to process to build that subset.
4462   *
4463   * It is recommended that for larger sets of courses this function is given a Generator that loads
4464   * the courses from the database in chunks.
4465   *
4466   * @param array|Traversable $courses List of courses to process
4467   * @param string $customfieldname the shortname of the custom field to match against
4468   * @param string $customfieldvalue the value this custom field needs to match
4469   * @param int $limit Limit the number of results to this amount
4470   * @return array First value is the filtered courses, second value is the number of courses processed
4471   */
4472  function course_filter_courses_by_customfield(
4473      $courses,
4474      $customfieldname,
4475      $customfieldvalue,
4476      int $limit = 0
4477  ) : array {
4478      global $DB;
4479  
4480      if (!$courses) {
4481          return [[], 0];
4482      }
4483  
4484      // Prepare the list of courses to search through.
4485      $coursesbyid = [];
4486      foreach ($courses as $course) {
4487          $coursesbyid[$course->id] = $course;
4488      }
4489      if (!$coursesbyid) {
4490          return [[], 0];
4491      }
4492      list($csql, $params) = $DB->get_in_or_equal(array_keys($coursesbyid), SQL_PARAMS_NAMED);
4493  
4494      // Get the id of the custom field.
4495      $sql = "
4496         SELECT f.id
4497           FROM {customfield_field} f
4498           JOIN {customfield_category} cat ON cat.id = f.categoryid
4499          WHERE f.shortname = ?
4500            AND cat.component = 'core_course'
4501            AND cat.area = 'course'
4502      ";
4503      $fieldid = $DB->get_field_sql($sql, [$customfieldname]);
4504      if (!$fieldid) {
4505          return [[], 0];
4506      }
4507  
4508      // Get a list of courseids that match that custom field value.
4509      if ($customfieldvalue == COURSE_CUSTOMFIELD_EMPTY) {
4510          $comparevalue = $DB->sql_compare_text('cd.value');
4511          $sql = "
4512             SELECT c.id
4513               FROM {course} c
4514          LEFT JOIN {customfield_data} cd ON cd.instanceid = c.id AND cd.fieldid = :fieldid
4515              WHERE c.id $csql
4516                AND (cd.value IS NULL OR $comparevalue = '' OR $comparevalue = '0')
4517          ";
4518          $params['fieldid'] = $fieldid;
4519          $matchcourseids = $DB->get_fieldset_sql($sql, $params);
4520      } else {
4521          $comparevalue = $DB->sql_compare_text('value');
4522          $select = "fieldid = :fieldid AND $comparevalue = :customfieldvalue AND instanceid $csql";
4523          $params['fieldid'] = $fieldid;
4524          $params['customfieldvalue'] = $customfieldvalue;
4525          $matchcourseids = $DB->get_fieldset_select('customfield_data', 'instanceid', $select, $params);
4526      }
4527  
4528      // Prepare the list of courses to return.
4529      $filteredcourses = [];
4530      $numberofcoursesprocessed = 0;
4531      $filtermatches = 0;
4532  
4533      foreach ($coursesbyid as $course) {
4534          $numberofcoursesprocessed++;
4535  
4536          if (in_array($course->id, $matchcourseids)) {
4537              $filteredcourses[] = $course;
4538              $filtermatches++;
4539          }
4540  
4541          if ($limit && $filtermatches >= $limit) {
4542              // We've found the number of requested courses. No need to continue searching.
4543              break;
4544          }
4545      }
4546  
4547      // Return the number of filtered courses as well as the number of courses that were searched
4548      // in order to find the matching courses. This allows the calling code to do some kind of
4549      // pagination.
4550      return [$filteredcourses, $numberofcoursesprocessed];
4551  }
4552  
4553  /**
4554   * Check module updates since a given time.
4555   * This function checks for updates in the module config, file areas, completion, grades, comments and ratings.
4556   *
4557   * @param  cm_info $cm        course module data
4558   * @param  int $from          the time to check
4559   * @param  array $fileareas   additional file ares to check
4560   * @param  array $filter      if we need to filter and return only selected updates
4561   * @return stdClass object with the different updates
4562   * @since  Moodle 3.2
4563   */
4564  function course_check_module_updates_since($cm, $from, $fileareas = array(), $filter = array()) {
4565      global $DB, $CFG, $USER;
4566  
4567      $context = $cm->context;
4568      $mod = $DB->get_record($cm->modname, array('id' => $cm->instance), '*', MUST_EXIST);
4569  
4570      $updates = new stdClass();
4571      $course = get_course($cm->course);
4572      $component = 'mod_' . $cm->modname;
4573  
4574      // Check changes in the module configuration.
4575      if (isset($mod->timemodified) and (empty($filter) or in_array('configuration', $filter))) {
4576          $updates->configuration = (object) array('updated' => false);
4577          if ($updates->configuration->updated = $mod->timemodified > $from) {
4578              $updates->configuration->timeupdated = $mod->timemodified;
4579          }
4580      }
4581  
4582      // Check for updates in files.
4583      if (plugin_supports('mod', $cm->modname, FEATURE_MOD_INTRO)) {
4584          $fileareas[] = 'intro';
4585      }
4586      if (!empty($fileareas) and (empty($filter) or in_array('fileareas', $filter))) {
4587          $fs = get_file_storage();
4588          $files = $fs->get_area_files($context->id, $component, $fileareas, false, "filearea, timemodified DESC", false, $from);
4589          foreach ($fileareas as $filearea) {
4590              $updates->{$filearea . 'files'} = (object) array('updated' => false);
4591          }
4592          foreach ($files as $file) {
4593              $updates->{$file->get_filearea() . 'files'}->updated = true;
4594              $updates->{$file->get_filearea() . 'files'}->itemids[] = $file->get_id();
4595          }
4596      }
4597  
4598      // Check completion.
4599      $supportcompletion = plugin_supports('mod', $cm->modname, FEATURE_COMPLETION_HAS_RULES);
4600      $supportcompletion = $supportcompletion or plugin_supports('mod', $cm->modname, FEATURE_COMPLETION_TRACKS_VIEWS);
4601      if ($supportcompletion and (empty($filter) or in_array('completion', $filter))) {
4602          $updates->completion = (object) array('updated' => false);
4603          $completion = new completion_info($course);
4604          // Use wholecourse to cache all the modules the first time.
4605          $completiondata = $completion->get_data($cm, true);
4606          if ($updates->completion->updated = !empty($completiondata->timemodified) && $completiondata->timemodified > $from) {
4607              $updates->completion->timemodified = $completiondata->timemodified;
4608          }
4609      }
4610  
4611      // Check grades.
4612      $supportgrades = plugin_supports('mod', $cm->modname, FEATURE_GRADE_HAS_GRADE);
4613      $supportgrades = $supportgrades or plugin_supports('mod', $cm->modname, FEATURE_GRADE_OUTCOMES);
4614      if ($supportgrades and (empty($filter) or (in_array('gradeitems', $filter) or in_array('outcomes', $filter)))) {
4615          require_once($CFG->libdir . '/gradelib.php');
4616          $grades = grade_get_grades($course->id, 'mod', $cm->modname, $mod->id, $USER->id);
4617  
4618          if (empty($filter) or in_array('gradeitems', $filter)) {
4619              $updates->gradeitems = (object) array('updated' => false);
4620              foreach ($grades->items as $gradeitem) {
4621                  foreach ($gradeitem->grades as $grade) {
4622                      if ($grade->datesubmitted > $from or $grade->dategraded > $from) {
4623                          $updates->gradeitems->updated = true;
4624                          $updates->gradeitems->itemids[] = $gradeitem->id;
4625                      }
4626                  }
4627              }
4628          }
4629  
4630          if (empty($filter) or in_array('outcomes', $filter)) {
4631              $updates->outcomes = (object) array('updated' => false);
4632              foreach ($grades->outcomes as $outcome) {
4633                  foreach ($outcome->grades as $grade) {
4634                      if ($grade->datesubmitted > $from or $grade->dategraded > $from) {
4635                          $updates->outcomes->updated = true;
4636                          $updates->outcomes->itemids[] = $outcome->id;
4637                      }
4638                  }
4639              }
4640          }
4641      }
4642  
4643      // Check comments.
4644      if (plugin_supports('mod', $cm->modname, FEATURE_COMMENT) and (empty($filter) or in_array('comments', $filter))) {
4645          $updates->comments = (object) array('updated' => false);
4646          require_once($CFG->dirroot . '/comment/lib.php');
4647          require_once($CFG->dirroot . '/comment/locallib.php');
4648          $manager = new comment_manager();
4649          $comments = $manager->get_component_comments_since($course, $context, $component, $from, $cm);
4650          if (!empty($comments)) {
4651              $updates->comments->updated = true;
4652              $updates->comments->itemids = array_keys($comments);
4653          }
4654      }
4655  
4656      // Check ratings.
4657      if (plugin_supports('mod', $cm->modname, FEATURE_RATE) and (empty($filter) or in_array('ratings', $filter))) {
4658          $updates->ratings = (object) array('updated' => false);
4659          require_once($CFG->dirroot . '/rating/lib.php');
4660          $manager = new rating_manager();
4661          $ratings = $manager->get_component_ratings_since($context, $component, $from);
4662          if (!empty($ratings)) {
4663              $updates->ratings->updated = true;
4664              $updates->ratings->itemids = array_keys($ratings);
4665          }
4666      }
4667  
4668      return $updates;
4669  }
4670  
4671  /**
4672   * Returns true if the user can view the participant page, false otherwise,
4673   *
4674   * @param context $context The context we are checking.
4675   * @return bool
4676   */
4677  function course_can_view_participants($context) {
4678      $viewparticipantscap = 'moodle/course:viewparticipants';
4679      if ($context->contextlevel == CONTEXT_SYSTEM) {
4680          $viewparticipantscap = 'moodle/site:viewparticipants';
4681      }
4682  
4683      return has_any_capability([$viewparticipantscap, 'moodle/course:enrolreview'], $context);
4684  }
4685  
4686  /**
4687   * Checks if a user can view the participant page, if not throws an exception.
4688   *
4689   * @param context $context The context we are checking.
4690   * @throws required_capability_exception
4691   */
4692  function course_require_view_participants($context) {
4693      if (!course_can_view_participants($context)) {
4694          $viewparticipantscap = 'moodle/course:viewparticipants';
4695          if ($context->contextlevel == CONTEXT_SYSTEM) {
4696              $viewparticipantscap = 'moodle/site:viewparticipants';
4697          }
4698          throw new required_capability_exception($context, $viewparticipantscap, 'nopermissions', '');
4699      }
4700  }
4701  
4702  /**
4703   * Return whether the user can download from the specified backup file area in the given context.
4704   *
4705   * @param string $filearea the backup file area. E.g. 'course', 'backup' or 'automated'.
4706   * @param \context $context
4707   * @param stdClass $user the user object. If not provided, the current user will be checked.
4708   * @return bool true if the user is allowed to download in the context, false otherwise.
4709   */
4710  function can_download_from_backup_filearea($filearea, \context $context, stdClass $user = null) {
4711      $candownload = false;
4712      switch ($filearea) {
4713          case 'course':
4714          case 'backup':
4715              $candownload = has_capability('moodle/backup:downloadfile', $context, $user);
4716              break;
4717          case 'automated':
4718              // Given the automated backups may contain userinfo, we restrict access such that only users who are able to
4719              // restore with userinfo are able to download the file. Users can't create these backups, so checking 'backup:userinfo'
4720              // doesn't make sense here.
4721              $candownload = has_capability('moodle/backup:downloadfile', $context, $user) &&
4722                             has_capability('moodle/restore:userinfo', $context, $user);
4723              break;
4724          default:
4725              break;
4726  
4727      }
4728      return $candownload;
4729  }
4730  
4731  /**
4732   * Get a list of hidden courses
4733   *
4734   * @param int|object|null $user User override to get the filter from. Defaults to current user
4735   * @return array $ids List of hidden courses
4736   * @throws coding_exception
4737   */
4738  function get_hidden_courses_on_timeline($user = null) {
4739      global $USER;
4740  
4741      if (empty($user)) {
4742          $user = $USER->id;
4743      }
4744  
4745      $preferences = get_user_preferences(null, null, $user);
4746      $ids = [];
4747      foreach ($preferences as $key => $value) {
4748          if (preg_match('/block_myoverview_hidden_course_(\d)+/', $key)) {
4749              $id = preg_split('/block_myoverview_hidden_course_/', $key);
4750              $ids[] = $id[1];
4751          }
4752      }
4753  
4754      return $ids;
4755  }
4756  
4757  /**
4758   * Returns a list of the most recently courses accessed by a user
4759   *
4760   * @param int $userid User id from which the courses will be obtained
4761   * @param int $limit Restrict result set to this amount
4762   * @param int $offset Skip this number of records from the start of the result set
4763   * @param string|null $sort SQL string for sorting
4764   * @return array
4765   */
4766  function course_get_recent_courses(int $userid = null, int $limit = 0, int $offset = 0, string $sort = null) {
4767  
4768      global $CFG, $USER, $DB;
4769  
4770      if (empty($userid)) {
4771          $userid = $USER->id;
4772      }
4773  
4774      $basefields = [
4775          'id', 'idnumber', 'summary', 'summaryformat', 'startdate', 'enddate', 'category',
4776          'shortname', 'fullname', 'timeaccess', 'component', 'visible',
4777          'showactivitydates', 'showcompletionconditions', 'pdfexportfont'
4778      ];
4779  
4780      if (empty($sort)) {
4781          $sort = 'timeaccess DESC';
4782      } else {
4783          // The SQL string for sorting can define sorting by multiple columns.
4784          $rawsorts = explode(',', $sort);
4785          $sorts = array();
4786          // Validate and trim the sort parameters in the SQL string for sorting.
4787          foreach ($rawsorts as $rawsort) {
4788              $sort = trim($rawsort);
4789              $sortparams = explode(' ', $sort);
4790              // A valid sort statement can not have more than 2 params (ex. 'summary desc' or 'timeaccess').
4791              if (count($sortparams) > 2) {
4792                  throw new invalid_parameter_exception(
4793                      'Invalid structure of the sort parameter, allowed structure: fieldname [ASC|DESC].');
4794              }
4795              $sortfield = trim($sortparams[0]);
4796              // Validate the value which defines the field to sort by.
4797              if (!in_array($sortfield, $basefields)) {
4798                  throw new invalid_parameter_exception('Invalid field in the sort parameter, allowed fields: ' .
4799                      implode(', ', $basefields) . '.');
4800              }
4801              $sortdirection = isset($sortparams[1]) ? trim($sortparams[1]) : '';
4802              // Validate the value which defines the sort direction (if present).
4803              $allowedsortdirections = ['asc', 'desc'];
4804              if (!empty($sortdirection) && !in_array(strtolower($sortdirection), $allowedsortdirections)) {
4805                  throw new invalid_parameter_exception('Invalid sort direction in the sort parameter, allowed values: ' .
4806                      implode(', ', $allowedsortdirections) . '.');
4807              }
4808              $sorts[] = $sort;
4809          }
4810          $sort = implode(',', $sorts);
4811      }
4812  
4813      $ctxfields = context_helper::get_preload_record_columns_sql('ctx');
4814  
4815      $coursefields = 'c.' . join(',', $basefields);
4816  
4817      // Ask the favourites service to give us the join SQL for favourited courses,
4818      // so we can include favourite information in the query.
4819      $usercontext = \context_user::instance($userid);
4820      $favservice = \core_favourites\service_factory::get_service_for_user_context($usercontext);
4821      list($favsql, $favparams) = $favservice->get_join_sql_by_type('core_course', 'courses', 'fav', 'ul.courseid');
4822  
4823      $sql = "SELECT $coursefields, $ctxfields
4824                FROM {course} c
4825                JOIN {context} ctx
4826                     ON ctx.contextlevel = :contextlevel
4827                     AND ctx.instanceid = c.id
4828                JOIN {user_lastaccess} ul
4829                     ON ul.courseid = c.id
4830              $favsql
4831           LEFT JOIN {enrol} eg ON eg.courseid = c.id AND eg.status = :statusenrolg AND eg.enrol = :guestenrol
4832               WHERE ul.userid = :userid
4833                 AND c.visible = :visible
4834                 AND (eg.id IS NOT NULL
4835                      OR EXISTS (SELECT e.id
4836                               FROM {enrol} e
4837                               JOIN {user_enrolments} ue ON ue.enrolid = e.id
4838                              WHERE e.courseid = c.id
4839                                AND e.status = :statusenrol
4840                                AND ue.status = :status
4841                                AND ue.userid = :userid2
4842                                AND ue.timestart < :now1
4843                                AND (ue.timeend = 0 OR ue.timeend > :now2)
4844                            ))
4845            ORDER BY $sort";
4846  
4847      $now = round(time(), -2); // Improves db caching.
4848      $params = ['userid' => $userid, 'contextlevel' => CONTEXT_COURSE, 'visible' => 1, 'status' => ENROL_USER_ACTIVE,
4849                 'statusenrol' => ENROL_INSTANCE_ENABLED, 'guestenrol' => 'guest', 'now1' => $now, 'now2' => $now,
4850                 'userid2' => $userid, 'statusenrolg' => ENROL_INSTANCE_ENABLED] + $favparams;
4851  
4852      $recentcourses = $DB->get_records_sql($sql, $params, $offset, $limit);
4853  
4854      // Filter courses if last access field is hidden.
4855      $hiddenfields = array_flip(explode(',', $CFG->hiddenuserfields));
4856  
4857      if ($userid != $USER->id && isset($hiddenfields['lastaccess'])) {
4858          $recentcourses = array_filter($recentcourses, function($course) {
4859              context_helper::preload_from_record($course);
4860              $context = context_course::instance($course->id, IGNORE_MISSING);
4861              // If last access was a hidden field, a user requesting info about another user would need permission to view hidden
4862              // fields.
4863              return has_capability('moodle/course:viewhiddenuserfields', $context);
4864          });
4865      }
4866  
4867      return $recentcourses;
4868  }
4869  
4870  /**
4871   * Calculate the course start date and offset for the given user ids.
4872   *
4873   * If the course is a fixed date course then the course start date will be returned.
4874   * If the course is a relative date course then the course date will be calculated and
4875   * and offset provided.
4876   *
4877   * The dates are returned as an array with the index being the user id. The array
4878   * contains the start date and start offset values for the user.
4879   *
4880   * If the user is not enrolled in the course then the course start date will be returned.
4881   *
4882   * If we have a course which starts on 1563244000 and 2 users, id 123 and 456, where the
4883   * former is enrolled in the course at 1563244693 and the latter is not enrolled then the
4884   * return value would look like:
4885   * [
4886   *      '123' => [
4887   *          'start' => 1563244693,
4888   *          'startoffset' => 693
4889   *      ],
4890   *      '456' => [
4891   *          'start' => 1563244000,
4892   *          'startoffset' => 0
4893   *      ]
4894   * ]
4895   *
4896   * @param stdClass $course The course to fetch dates for.
4897   * @param array $userids The list of user ids to get dates for.
4898   * @return array
4899   */
4900  function course_get_course_dates_for_user_ids(stdClass $course, array $userids): array {
4901      if (empty($course->relativedatesmode)) {
4902          // This course isn't set to relative dates so we can early return with the course
4903          // start date.
4904          return array_reduce($userids, function($carry, $userid) use ($course) {
4905              $carry[$userid] = [
4906                  'start' => $course->startdate,
4907                  'startoffset' => 0
4908              ];
4909              return $carry;
4910          }, []);
4911      }
4912  
4913      // We're dealing with a relative dates course now so we need to calculate some dates.
4914      $cache = cache::make('core', 'course_user_dates');
4915      $dates = [];
4916      $uncacheduserids = [];
4917  
4918      // Try fetching the values from the cache so that we don't need to do a DB request.
4919      foreach ($userids as $userid) {
4920          $cachekey = "{$course->id}_{$userid}";
4921          $cachedvalue = $cache->get($cachekey);
4922  
4923          if ($cachedvalue === false) {
4924              // Looks like we haven't seen this user for this course before so we'll have
4925              // to fetch it.
4926              $uncacheduserids[] = $userid;
4927          } else {
4928              [$start, $startoffset] = $cachedvalue;
4929              $dates[$userid] = [
4930                  'start' => $start,
4931                  'startoffset' => $startoffset
4932              ];
4933          }
4934      }
4935  
4936      if (!empty($uncacheduserids)) {
4937          // Load the enrolments for any users we haven't seen yet. Set the "onlyactive" param
4938          // to false because it filters out users with enrolment start times in the future which
4939          // we don't want.
4940          $enrolments = enrol_get_course_users($course->id, false, $uncacheduserids);
4941  
4942          foreach ($uncacheduserids as $userid) {
4943              // Find the user enrolment that has the earliest start date.
4944              $enrolment = array_reduce(array_values($enrolments), function($carry, $enrolment) use ($userid) {
4945                  // Only consider enrolments for this user if the user enrolment is active and the
4946                  // enrolment method is enabled.
4947                  if (
4948                      $enrolment->uestatus == ENROL_USER_ACTIVE &&
4949                      $enrolment->estatus == ENROL_INSTANCE_ENABLED &&
4950                      $enrolment->id == $userid
4951                  ) {
4952                      if (is_null($carry)) {
4953                          // Haven't found an enrolment yet for this user so use the one we just found.
4954                          $carry = $enrolment;
4955                      } else {
4956                          // We've already found an enrolment for this user so let's use which ever one
4957                          // has the earliest start time.
4958                          $carry = $carry->uetimestart < $enrolment->uetimestart ? $carry : $enrolment;
4959                      }
4960                  }
4961  
4962                  return $carry;
4963              }, null);
4964  
4965              if ($enrolment) {
4966                  // The course is in relative dates mode so we calculate the student's start
4967                  // date based on their enrolment start date.
4968                  $start = $course->startdate > $enrolment->uetimestart ? $course->startdate : $enrolment->uetimestart;
4969                  $startoffset = $start - $course->startdate;
4970              } else {
4971                  // The user is not enrolled in the course so default back to the course start date.
4972                  $start = $course->startdate;
4973                  $startoffset = 0;
4974              }
4975  
4976              $dates[$userid] = [
4977                  'start' => $start,
4978                  'startoffset' => $startoffset
4979              ];
4980  
4981              $cachekey = "{$course->id}_{$userid}";
4982              $cache->set($cachekey, [$start, $startoffset]);
4983          }
4984      }
4985  
4986      return $dates;
4987  }
4988  
4989  /**
4990   * Calculate the course start date and offset for the given user id.
4991   *
4992   * If the course is a fixed date course then the course start date will be returned.
4993   * If the course is a relative date course then the course date will be calculated and
4994   * and offset provided.
4995   *
4996   * The return array contains the start date and start offset values for the user.
4997   *
4998   * If the user is not enrolled in the course then the course start date will be returned.
4999   *
5000   * If we have a course which starts on 1563244000. If a user's enrolment starts on 1563244693
5001   * then the return would be:
5002   * [
5003   *      'start' => 1563244693,
5004   *      'startoffset' => 693
5005   * ]
5006   *
5007   * If the use was not enrolled then the return would be:
5008   * [
5009   *      'start' => 1563244000,
5010   *      'startoffset' => 0
5011   * ]
5012   *
5013   * @param stdClass $course The course to fetch dates for.
5014   * @param int $userid The user id to get dates for.
5015   * @return array
5016   */
5017  function course_get_course_dates_for_user_id(stdClass $course, int $userid): array {
5018      return (course_get_course_dates_for_user_ids($course, [$userid]))[$userid];
5019  }
5020  
5021  /**
5022   * Renders the course copy form for the modal on the course management screen.
5023   *
5024   * @param array $args
5025   * @return string $o Form HTML.
5026   */
5027  function course_output_fragment_new_base_form($args) {
5028  
5029      $serialiseddata = json_decode($args['jsonformdata'], true);
5030      $formdata = [];
5031      if (!empty($serialiseddata)) {
5032          parse_str($serialiseddata, $formdata);
5033      }
5034  
5035      $context = context_course::instance($args['courseid']);
5036      $copycaps = \core_course\management\helper::get_course_copy_capabilities();
5037      require_all_capabilities($copycaps, $context);
5038  
5039      $course = get_course($args['courseid']);
5040      $mform = new \core_backup\output\copy_form(
5041          null,
5042          array('course' => $course, 'returnto' => '', 'returnurl' => ''),
5043          'post', '', ['class' => 'ignoredirty'], true, $formdata);
5044  
5045      if (!empty($serialiseddata)) {
5046          // If we were passed non-empty form data we want the mform to call validation functions and show errors.
5047          $mform->is_validated();
5048      }
5049  
5050      ob_start();
5051      $mform->display();
5052      $o = ob_get_contents();
5053      ob_end_clean();
5054  
5055      return $o;
5056  }