Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 3.11.x will end 14 Nov 2022 (12 months plus 6 months extension).
  • Bug fixes for security issues in 3.11.x will end 13 Nov 2023 (18 months plus 12 months extension).
  • PHP version: minimum PHP 7.3.0 Note: minimum PHP version has increased since Moodle 3.10. PHP 7.4.x is supported too.
/course/ -> lib.php (source)

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

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