Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 4.0.x will end 8 May 2023 (12 months).
  • Bug fixes for security issues in 4.0.x will end 13 November 2023 (18 months).
  • PHP version: minimum PHP 7.3.0 Note: the minimum PHP version has increased since Moodle 3.10. PHP 7.4.x is also supported.
/course/ -> lib.php (source)

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