Search moodle.org's
Developer Documentation

See Release Notes

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

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

   1  <?php
   2  
   3  // This file is part of Moodle - http://moodle.org/
   4  //
   5  // Moodle is free software: you can redistribute it and/or modify
   6  // it under the terms of the GNU General Public License as published by
   7  // the Free Software Foundation, either version 3 of the License, or
   8  // (at your option) any later version.
   9  //
  10  // Moodle is distributed in the hope that it will be useful,
  11  // but WITHOUT ANY WARRANTY; without even the implied warranty of
  12  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  13  // GNU General Public License for more details.
  14  //
  15  // You should have received a copy of the GNU General Public License
  16  // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
  17  
  18  /**
  19   * This library includes the basic parts of enrol api.
  20   * It is available on each page.
  21   *
  22   * @package    core
  23   * @subpackage enrol
  24   * @copyright  2010 Petr Skoda {@link http://skodak.org}
  25   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  26   */
  27  
  28  defined('MOODLE_INTERNAL') || die();
  29  
  30  /** Course enrol instance enabled. (used in enrol->status) */
  31  define('ENROL_INSTANCE_ENABLED', 0);
  32  
  33  /** Course enrol instance disabled, user may enter course if other enrol instance enabled. (used in enrol->status)*/
  34  define('ENROL_INSTANCE_DISABLED', 1);
  35  
  36  /** User is active participant (used in user_enrolments->status)*/
  37  define('ENROL_USER_ACTIVE', 0);
  38  
  39  /** User participation in course is suspended (used in user_enrolments->status) */
  40  define('ENROL_USER_SUSPENDED', 1);
  41  
  42  /** @deprecated - enrol caching was reworked, use ENROL_MAX_TIMESTAMP instead */
  43  define('ENROL_REQUIRE_LOGIN_CACHE_PERIOD', 1800);
  44  
  45  /** The timestamp indicating forever */
  46  define('ENROL_MAX_TIMESTAMP', 2147483647);
  47  
  48  /** When user disappears from external source, the enrolment is completely removed */
  49  define('ENROL_EXT_REMOVED_UNENROL', 0);
  50  
  51  /** When user disappears from external source, the enrolment is kept as is - one way sync */
  52  define('ENROL_EXT_REMOVED_KEEP', 1);
  53  
  54  /** @deprecated since 2.4 not used any more, migrate plugin to new restore methods */
  55  define('ENROL_RESTORE_TYPE', 'enrolrestore');
  56  
  57  /**
  58   * When user disappears from external source, user enrolment is suspended, roles are kept as is.
  59   * In some cases user needs a role with some capability to be visible in UI - suc has in gradebook,
  60   * assignments, etc.
  61   */
  62  define('ENROL_EXT_REMOVED_SUSPEND', 2);
  63  
  64  /**
  65   * When user disappears from external source, the enrolment is suspended and roles assigned
  66   * by enrol instance are removed. Please note that user may "disappear" from gradebook and other areas.
  67   * */
  68  define('ENROL_EXT_REMOVED_SUSPENDNOROLES', 3);
  69  
  70  /**
  71   * Do not send email.
  72   */
  73  define('ENROL_DO_NOT_SEND_EMAIL', 0);
  74  
  75  /**
  76   * Send email from course contact.
  77   */
  78  define('ENROL_SEND_EMAIL_FROM_COURSE_CONTACT', 1);
  79  
  80  /**
  81   * Send email from enrolment key holder.
  82   */
  83  define('ENROL_SEND_EMAIL_FROM_KEY_HOLDER', 2);
  84  
  85  /**
  86   * Send email from no reply address.
  87   */
  88  define('ENROL_SEND_EMAIL_FROM_NOREPLY', 3);
  89  
  90  /** Edit enrolment action. */
  91  define('ENROL_ACTION_EDIT', 'editenrolment');
  92  
  93  /** Unenrol action. */
  94  define('ENROL_ACTION_UNENROL', 'unenrol');
  95  
  96  /**
  97   * Returns instances of enrol plugins
  98   * @param bool $enabled return enabled only
  99   * @return array of enrol plugins name=>instance
 100   */
 101  function enrol_get_plugins($enabled) {
 102      global $CFG;
 103  
 104      $result = array();
 105  
 106      if ($enabled) {
 107          // sorted by enabled plugin order
 108          $enabled = explode(',', $CFG->enrol_plugins_enabled);
 109          $plugins = array();
 110          foreach ($enabled as $plugin) {
 111              $plugins[$plugin] = "$CFG->dirroot/enrol/$plugin";
 112          }
 113      } else {
 114          // sorted alphabetically
 115          $plugins = core_component::get_plugin_list('enrol');
 116          ksort($plugins);
 117      }
 118  
 119      foreach ($plugins as $plugin=>$location) {
 120          $class = "enrol_{$plugin}_plugin";
 121          if (!class_exists($class)) {
 122              if (!file_exists("$location/lib.php")) {
 123                  continue;
 124              }
 125              include_once("$location/lib.php");
 126              if (!class_exists($class)) {
 127                  continue;
 128              }
 129          }
 130  
 131          $result[$plugin] = new $class();
 132      }
 133  
 134      return $result;
 135  }
 136  
 137  /**
 138   * Returns instance of enrol plugin
 139   * @param  string $name name of enrol plugin ('manual', 'guest', ...)
 140   * @return enrol_plugin
 141   */
 142  function enrol_get_plugin($name) {
 143      global $CFG;
 144  
 145      $name = clean_param($name, PARAM_PLUGIN);
 146  
 147      if (empty($name)) {
 148          // ignore malformed or missing plugin names completely
 149          return null;
 150      }
 151  
 152      $location = "$CFG->dirroot/enrol/$name";
 153  
 154      $class = "enrol_{$name}_plugin";
 155      if (!class_exists($class)) {
 156          if (!file_exists("$location/lib.php")) {
 157              return null;
 158          }
 159          include_once("$location/lib.php");
 160          if (!class_exists($class)) {
 161              return null;
 162          }
 163      }
 164  
 165      return new $class();
 166  }
 167  
 168  /**
 169   * Returns enrolment instances in given course.
 170   * @param int $courseid
 171   * @param bool $enabled
 172   * @return array of enrol instances
 173   */
 174  function enrol_get_instances($courseid, $enabled) {
 175      global $DB, $CFG;
 176  
 177      if (!$enabled) {
 178          return $DB->get_records('enrol', array('courseid'=>$courseid), 'sortorder,id');
 179      }
 180  
 181      $result = $DB->get_records('enrol', array('courseid'=>$courseid, 'status'=>ENROL_INSTANCE_ENABLED), 'sortorder,id');
 182  
 183      $enabled = explode(',', $CFG->enrol_plugins_enabled);
 184      foreach ($result as $key=>$instance) {
 185          if (!in_array($instance->enrol, $enabled)) {
 186              unset($result[$key]);
 187              continue;
 188          }
 189          if (!file_exists("$CFG->dirroot/enrol/$instance->enrol/lib.php")) {
 190              // broken plugin
 191              unset($result[$key]);
 192              continue;
 193          }
 194      }
 195  
 196      return $result;
 197  }
 198  
 199  /**
 200   * Checks if a given plugin is in the list of enabled enrolment plugins.
 201   *
 202   * @param string $enrol Enrolment plugin name
 203   * @return boolean Whether the plugin is enabled
 204   */
 205  function enrol_is_enabled($enrol) {
 206      global $CFG;
 207  
 208      if (empty($CFG->enrol_plugins_enabled)) {
 209          return false;
 210      }
 211      return in_array($enrol, explode(',', $CFG->enrol_plugins_enabled));
 212  }
 213  
 214  /**
 215   * Check all the login enrolment information for the given user object
 216   * by querying the enrolment plugins
 217   * This function may be very slow, use only once after log-in or login-as.
 218   *
 219   * @param stdClass $user User object.
 220   * @param bool $ignoreintervalcheck Force to ignore checking configured sync intervals.
 221   *
 222   * @return void
 223   */
 224  function enrol_check_plugins($user, bool $ignoreintervalcheck = true) {
 225      global $CFG;
 226  
 227      if (empty($user->id) or isguestuser($user)) {
 228          // shortcut - there is no enrolment work for guests and not-logged-in users
 229          return;
 230      }
 231  
 232      // originally there was a broken admin test, but accidentally it was non-functional in 2.2,
 233      // which proved it was actually not necessary.
 234  
 235      static $inprogress = array();  // To prevent this function being called more than once in an invocation
 236  
 237      if (!empty($inprogress[$user->id])) {
 238          return;
 239      }
 240  
 241      $syncinterval = isset($CFG->enrolments_sync_interval) ? (int)$CFG->enrolments_sync_interval : HOURSECS;
 242      $needintervalchecking = !$ignoreintervalcheck && !empty($syncinterval);
 243  
 244      if ($needintervalchecking) {
 245          $lastsync = get_user_preferences('last_time_enrolments_synced', 0, $user);
 246          if (time() - $lastsync < $syncinterval) {
 247              return;
 248          }
 249      }
 250  
 251      $inprogress[$user->id] = true;  // Set the flag
 252  
 253      $enabled = enrol_get_plugins(true);
 254  
 255      foreach($enabled as $enrol) {
 256          $enrol->sync_user_enrolments($user);
 257      }
 258  
 259      if ($needintervalchecking) {
 260          set_user_preference('last_time_enrolments_synced', time(), $user);
 261      }
 262  
 263      unset($inprogress[$user->id]);  // Unset the flag
 264  }
 265  
 266  /**
 267   * Do these two students share any course?
 268   *
 269   * The courses has to be visible and enrolments has to be active,
 270   * timestart and timeend restrictions are ignored.
 271   *
 272   * This function calls {@see enrol_get_shared_courses()} setting checkexistsonly
 273   * to true.
 274   *
 275   * @param stdClass|int $user1
 276   * @param stdClass|int $user2
 277   * @return bool
 278   */
 279  function enrol_sharing_course($user1, $user2) {
 280      return enrol_get_shared_courses($user1, $user2, false, true);
 281  }
 282  
 283  /**
 284   * Returns any courses shared by the two users
 285   *
 286   * The courses has to be visible and enrolments has to be active,
 287   * timestart and timeend restrictions are ignored.
 288   *
 289   * @global moodle_database $DB
 290   * @param stdClass|int $user1
 291   * @param stdClass|int $user2
 292   * @param bool $preloadcontexts If set to true contexts for the returned courses
 293   *              will be preloaded.
 294   * @param bool $checkexistsonly If set to true then this function will return true
 295   *              if the users share any courses and false if not.
 296   * @return array|bool An array of courses that both users are enrolled in OR if
 297   *              $checkexistsonly set returns true if the users share any courses
 298   *              and false if not.
 299   */
 300  function enrol_get_shared_courses($user1, $user2, $preloadcontexts = false, $checkexistsonly = false) {
 301      global $DB, $CFG;
 302  
 303      $user1 = isset($user1->id) ? $user1->id : $user1;
 304      $user2 = isset($user2->id) ? $user2->id : $user2;
 305  
 306      if (empty($user1) or empty($user2)) {
 307          return false;
 308      }
 309  
 310      if (!$plugins = explode(',', $CFG->enrol_plugins_enabled)) {
 311          return false;
 312      }
 313  
 314      list($plugins1, $params1) = $DB->get_in_or_equal($plugins, SQL_PARAMS_NAMED, 'ee1');
 315      list($plugins2, $params2) = $DB->get_in_or_equal($plugins, SQL_PARAMS_NAMED, 'ee2');
 316      $params = array_merge($params1, $params2);
 317      $params['enabled1'] = ENROL_INSTANCE_ENABLED;
 318      $params['enabled2'] = ENROL_INSTANCE_ENABLED;
 319      $params['active1'] = ENROL_USER_ACTIVE;
 320      $params['active2'] = ENROL_USER_ACTIVE;
 321      $params['user1']   = $user1;
 322      $params['user2']   = $user2;
 323  
 324      $ctxselect = '';
 325      $ctxjoin = '';
 326      if ($preloadcontexts) {
 327          $ctxselect = ', ' . context_helper::get_preload_record_columns_sql('ctx');
 328          $ctxjoin = "LEFT JOIN {context} ctx ON (ctx.instanceid = c.id AND ctx.contextlevel = :contextlevel)";
 329          $params['contextlevel'] = CONTEXT_COURSE;
 330      }
 331  
 332      $sql = "SELECT c.* $ctxselect
 333                FROM {course} c
 334                JOIN (
 335                  SELECT DISTINCT c.id
 336                    FROM {course} c
 337                    JOIN {enrol} e1 ON (c.id = e1.courseid AND e1.status = :enabled1 AND e1.enrol $plugins1)
 338                    JOIN {user_enrolments} ue1 ON (ue1.enrolid = e1.id AND ue1.status = :active1 AND ue1.userid = :user1)
 339                    JOIN {enrol} e2 ON (c.id = e2.courseid AND e2.status = :enabled2 AND e2.enrol $plugins2)
 340                    JOIN {user_enrolments} ue2 ON (ue2.enrolid = e2.id AND ue2.status = :active2 AND ue2.userid = :user2)
 341                   WHERE c.visible = 1
 342                ) ec ON ec.id = c.id
 343                $ctxjoin";
 344  
 345      if ($checkexistsonly) {
 346          return $DB->record_exists_sql($sql, $params);
 347      } else {
 348          $courses = $DB->get_records_sql($sql, $params);
 349          if ($preloadcontexts) {
 350              array_map('context_helper::preload_from_record', $courses);
 351          }
 352          return $courses;
 353      }
 354  }
 355  
 356  /**
 357   * This function adds necessary enrol plugins UI into the course edit form.
 358   *
 359   * @param MoodleQuickForm $mform
 360   * @param object $data course edit form data
 361   * @param object $context context of existing course or parent category if course does not exist
 362   * @return void
 363   */
 364  function enrol_course_edit_form(MoodleQuickForm $mform, $data, $context) {
 365      $plugins = enrol_get_plugins(true);
 366      if (!empty($data->id)) {
 367          $instances = enrol_get_instances($data->id, false);
 368          foreach ($instances as $instance) {
 369              if (!isset($plugins[$instance->enrol])) {
 370                  continue;
 371              }
 372              $plugin = $plugins[$instance->enrol];
 373              $plugin->course_edit_form($instance, $mform, $data, $context);
 374          }
 375      } else {
 376          foreach ($plugins as $plugin) {
 377              $plugin->course_edit_form(NULL, $mform, $data, $context);
 378          }
 379      }
 380  }
 381  
 382  /**
 383   * Validate course edit form data
 384   *
 385   * @param array $data raw form data
 386   * @param object $context context of existing course or parent category if course does not exist
 387   * @return array errors array
 388   */
 389  function enrol_course_edit_validation(array $data, $context) {
 390      $errors = array();
 391      $plugins = enrol_get_plugins(true);
 392  
 393      if (!empty($data['id'])) {
 394          $instances = enrol_get_instances($data['id'], false);
 395          foreach ($instances as $instance) {
 396              if (!isset($plugins[$instance->enrol])) {
 397                  continue;
 398              }
 399              $plugin = $plugins[$instance->enrol];
 400              $errors = array_merge($errors, $plugin->course_edit_validation($instance, $data, $context));
 401          }
 402      } else {
 403          foreach ($plugins as $plugin) {
 404              $errors = array_merge($errors, $plugin->course_edit_validation(NULL, $data, $context));
 405          }
 406      }
 407  
 408      return $errors;
 409  }
 410  
 411  /**
 412   * Update enrol instances after course edit form submission
 413   * @param bool $inserted true means new course added, false course already existed
 414   * @param object $course
 415   * @param object $data form data
 416   * @return void
 417   */
 418  function enrol_course_updated($inserted, $course, $data) {
 419      global $DB, $CFG;
 420  
 421      $plugins = enrol_get_plugins(true);
 422  
 423      foreach ($plugins as $plugin) {
 424          $plugin->course_updated($inserted, $course, $data);
 425      }
 426  }
 427  
 428  /**
 429   * Add navigation nodes
 430   * @param navigation_node $coursenode
 431   * @param object $course
 432   * @return void
 433   */
 434  function enrol_add_course_navigation(navigation_node $coursenode, $course) {
 435      global $CFG;
 436  
 437      $coursecontext = context_course::instance($course->id);
 438  
 439      $instances = enrol_get_instances($course->id, true);
 440      $plugins   = enrol_get_plugins(true);
 441  
 442      // we do not want to break all course pages if there is some borked enrol plugin, right?
 443      foreach ($instances as $k=>$instance) {
 444          if (!isset($plugins[$instance->enrol])) {
 445              unset($instances[$k]);
 446          }
 447      }
 448  
 449      $usersnode = $coursenode->add(get_string('users'), null, navigation_node::TYPE_CONTAINER, null, 'users');
 450  
 451      // List all participants - allows assigning roles, groups, etc.
 452      // Have this available even in the site context as the page is still accessible from the frontpage.
 453      if (has_capability('moodle/course:enrolreview', $coursecontext)) {
 454          $url = new moodle_url('/user/index.php', array('id' => $course->id));
 455          $usersnode->add(get_string('enrolledusers', 'enrol'), $url, navigation_node::TYPE_SETTING,
 456              null, 'review', new pix_icon('i/enrolusers', ''));
 457      }
 458  
 459      if ($course->id != SITEID) {
 460          // manage enrol plugin instances
 461          if (has_capability('moodle/course:enrolconfig', $coursecontext) or has_capability('moodle/course:enrolreview', $coursecontext)) {
 462              $url = new moodle_url('/enrol/instances.php', array('id'=>$course->id));
 463          } else {
 464              $url = NULL;
 465          }
 466          $instancesnode = $usersnode->add(get_string('enrolmentinstances', 'enrol'), $url, navigation_node::TYPE_SETTING, null, 'manageinstances');
 467  
 468          // each instance decides how to configure itself or how many other nav items are exposed
 469          foreach ($instances as $instance) {
 470              if (!isset($plugins[$instance->enrol])) {
 471                  continue;
 472              }
 473              $plugins[$instance->enrol]->add_course_navigation($instancesnode, $instance);
 474          }
 475  
 476          if (!$url) {
 477              $instancesnode->trim_if_empty();
 478          }
 479  
 480          if (has_capability('moodle/course:renameroles', $coursecontext)) {
 481              $url = new moodle_url('/enrol/renameroles.php', array('id' => $course->id));
 482              $instancesnode->add(
 483                  get_string('rolerenaming'),
 484                  $url,
 485                  navigation_node::TYPE_SETTING,
 486                  null,
 487                  'renameroles'
 488              );
 489          }
 490      }
 491  
 492      // Manage groups in this course or even frontpage
 493      if (($course->groupmode || !$course->groupmodeforce) && has_capability('moodle/course:managegroups', $coursecontext)) {
 494          $url = new moodle_url('/group/index.php', array('id'=>$course->id));
 495          $usersnode->add(get_string('groups'), $url, navigation_node::TYPE_SETTING, null, 'groups', new pix_icon('i/group', ''));
 496      }
 497  
 498      if (has_any_capability(
 499          [ 'moodle/role:assign', 'moodle/role:safeoverride', 'moodle/role:override', 'moodle/role:review'],
 500          $coursecontext
 501      )) {
 502          // Override roles
 503          if (has_capability('moodle/role:review', $coursecontext)) {
 504              $url = new moodle_url('/admin/roles/permissions.php', array('contextid'=>$coursecontext->id));
 505          } else {
 506              $url = NULL;
 507          }
 508          $permissionsnode = $usersnode->add(get_string('permissions', 'role'), $url, navigation_node::TYPE_SETTING, null, 'override');
 509  
 510          // Add assign or override roles if allowed
 511          if ($course->id == SITEID or (!empty($CFG->adminsassignrolesincourse) and is_siteadmin())) {
 512              if (has_capability('moodle/role:assign', $coursecontext)) {
 513                  $url = new moodle_url('/admin/roles/assign.php', array('contextid'=>$coursecontext->id));
 514                  $permissionsnode->add(get_string('assignedroles', 'role'), $url, navigation_node::TYPE_SETTING, null, 'roles', new pix_icon('i/assignroles', ''));
 515              }
 516          }
 517          // Check role permissions
 518          if (has_any_capability(array('moodle/role:assign', 'moodle/role:safeoverride', 'moodle/role:override'), $coursecontext)) {
 519              $url = new moodle_url('/admin/roles/check.php', array('contextid'=>$coursecontext->id));
 520              $permissionsnode->add(get_string('checkpermissions', 'role'), $url, navigation_node::TYPE_SETTING, null, 'permissions', new pix_icon('i/checkpermissions', ''));
 521          }
 522      }
 523  
 524       // Deal somehow with users that are not enrolled but still got a role somehow
 525      if ($course->id != SITEID) {
 526          //TODO, create some new UI for role assignments at course level
 527          if (has_capability('moodle/course:reviewotherusers', $coursecontext)) {
 528              $url = new moodle_url('/enrol/otherusers.php', array('id'=>$course->id));
 529              $usersnode->add(get_string('notenrolledusers', 'enrol'), $url, navigation_node::TYPE_SETTING, null, 'otherusers', new pix_icon('i/assignroles', ''));
 530          }
 531      }
 532  
 533      // just in case nothing was actually added
 534      $usersnode->trim_if_empty();
 535  
 536      if ($course->id != SITEID) {
 537          if (isguestuser() or !isloggedin()) {
 538              // guest account can not be enrolled - no links for them
 539          } else if (is_enrolled($coursecontext)) {
 540              // unenrol link if possible
 541              foreach ($instances as $instance) {
 542                  if (!isset($plugins[$instance->enrol])) {
 543                      continue;
 544                  }
 545                  $plugin = $plugins[$instance->enrol];
 546                  if ($unenrollink = $plugin->get_unenrolself_link($instance)) {
 547                      $coursenode->add(get_string('unenrolme', 'core_enrol'), $unenrollink,
 548                          navigation_node::TYPE_SETTING, null, 'unenrolself', new pix_icon('i/user', ''));
 549                      $coursenode->get('unenrolself')->set_force_into_more_menu(true);
 550                      break;
 551                      //TODO. deal with multiple unenrol links - not likely case, but still...
 552                  }
 553              }
 554          } else {
 555              // enrol link if possible
 556              if (is_viewing($coursecontext)) {
 557                  // better not show any enrol link, this is intended for managers and inspectors
 558              } else {
 559                  foreach ($instances as $instance) {
 560                      if (!isset($plugins[$instance->enrol])) {
 561                          continue;
 562                      }
 563                      $plugin = $plugins[$instance->enrol];
 564                      if ($plugin->show_enrolme_link($instance)) {
 565                          $url = new moodle_url('/enrol/index.php', array('id'=>$course->id));
 566                          $shortname = format_string($course->shortname, true, array('context' => $coursecontext));
 567                          $coursenode->add(get_string('enrolme', 'core_enrol', $shortname), $url, navigation_node::TYPE_SETTING, null, 'enrolself', new pix_icon('i/user', ''));
 568                          break;
 569                      }
 570                  }
 571              }
 572          }
 573      }
 574  }
 575  
 576  /**
 577   * Returns list of courses current $USER is enrolled in and can access
 578   *
 579   * The $fields param is a list of field names to ADD so name just the fields you really need,
 580   * which will be added and uniq'd.
 581   *
 582   * If $allaccessible is true, this will additionally return courses that the current user is not
 583   * enrolled in, but can access because they are open to the user for other reasons (course view
 584   * permission, currently viewing course as a guest, or course allows guest access without
 585   * password).
 586   *
 587   * @param string|array $fields Extra fields to be returned (array or comma-separated list).
 588   * @param string|null $sort Comma separated list of fields to sort by, defaults to respecting navsortmycoursessort.
 589   * Allowed prefixes for sort fields are: "ul" for the user_lastaccess table, "c" for the courses table,
 590   * "ue" for the user_enrolments table.
 591   * @param int $limit max number of courses
 592   * @param array $courseids the list of course ids to filter by
 593   * @param bool $allaccessible Include courses user is not enrolled in, but can access
 594   * @param int $offset Offset the result set by this number
 595   * @param array $excludecourses IDs of hidden courses to exclude from search
 596   * @return array
 597   */
 598  function enrol_get_my_courses($fields = null, $sort = null, $limit = 0, $courseids = [], $allaccessible = false,
 599      $offset = 0, $excludecourses = []) {
 600      global $DB, $USER, $CFG;
 601  
 602      // Allowed prefixes and field names.
 603      $allowedprefixesandfields = ['c' => array_keys($DB->get_columns('course')),
 604                                  'ul' => array_keys($DB->get_columns('user_lastaccess')),
 605                                  'ue' => array_keys($DB->get_columns('user_enrolments'))];
 606  
 607      // Re-Arrange the course sorting according to the admin settings.
 608      $sort = enrol_get_courses_sortingsql($sort);
 609  
 610      // Guest account does not have any enrolled courses.
 611      if (!$allaccessible && (isguestuser() or !isloggedin())) {
 612          return array();
 613      }
 614  
 615      $basefields = [
 616          'id', 'category', 'sortorder',
 617          'shortname', 'fullname', 'idnumber',
 618          'startdate', 'visible',
 619          'groupmode', 'groupmodeforce', 'cacherev',
 620          'showactivitydates', 'showcompletionconditions',
 621      ];
 622  
 623      if (empty($fields)) {
 624          $fields = $basefields;
 625      } else if (is_string($fields)) {
 626          // turn the fields from a string to an array
 627          $fields = explode(',', $fields);
 628          $fields = array_map('trim', $fields);
 629          $fields = array_unique(array_merge($basefields, $fields));
 630      } else if (is_array($fields)) {
 631          $fields = array_unique(array_merge($basefields, $fields));
 632      } else {
 633          throw new coding_exception('Invalid $fields parameter in enrol_get_my_courses()');
 634      }
 635      if (in_array('*', $fields)) {
 636          $fields = array('*');
 637      }
 638  
 639      $orderby = "";
 640      $sort    = trim($sort);
 641      $sorttimeaccess = false;
 642      if (!empty($sort)) {
 643          $rawsorts = explode(',', $sort);
 644          $sorts = array();
 645          foreach ($rawsorts as $rawsort) {
 646              $rawsort = trim($rawsort);
 647              // Make sure that there are no more white spaces in sortparams after explode.
 648              $sortparams = array_values(array_filter(explode(' ', $rawsort)));
 649              // If more than 2 values present then throw coding_exception.
 650              if (isset($sortparams[2])) {
 651                  throw new coding_exception('Invalid $sort parameter in enrol_get_my_courses()');
 652              }
 653              // Check the sort ordering if present, at the beginning.
 654              if (isset($sortparams[1]) && (preg_match("/^(asc|desc)$/i", $sortparams[1]) === 0)) {
 655                  throw new coding_exception('Invalid sort direction in $sort parameter in enrol_get_my_courses()');
 656              }
 657  
 658              $sortfield = $sortparams[0];
 659              $sortdirection = $sortparams[1] ?? 'asc';
 660              if (strpos($sortfield, '.') !== false) {
 661                  $sortfieldparams = explode('.', $sortfield);
 662                  // Check if more than one dots present in the prefix field.
 663                  if (isset($sortfieldparams[2])) {
 664                      throw new coding_exception('Invalid $sort parameter in enrol_get_my_courses()');
 665                  }
 666                  list($prefix, $fieldname) = [$sortfieldparams[0], $sortfieldparams[1]];
 667                  // Check if the field name matches with the allowed prefix.
 668                  if (array_key_exists($prefix, $allowedprefixesandfields) &&
 669                      (in_array($fieldname, $allowedprefixesandfields[$prefix]))) {
 670                      if ($prefix === 'ul') {
 671                          $sorts[] = "COALESCE({$prefix}.{$fieldname}, 0) {$sortdirection}";
 672                          $sorttimeaccess = true;
 673                      } else {
 674                          // Check if the field name that matches with the prefix and just append to sorts.
 675                          $sorts[] = $rawsort;
 676                      }
 677                  } else {
 678                      throw new coding_exception('Invalid $sort parameter in enrol_get_my_courses()');
 679                  }
 680              } else {
 681                  // Check if the field name matches with $allowedprefixesandfields.
 682                  $found = false;
 683                  foreach (array_keys($allowedprefixesandfields) as $prefix) {
 684                      if (in_array($sortfield, $allowedprefixesandfields[$prefix])) {
 685                          if ($prefix === 'ul') {
 686                              $sorts[] = "COALESCE({$prefix}.{$sortfield}, 0) {$sortdirection}";
 687                              $sorttimeaccess = true;
 688                          } else {
 689                              $sorts[] = "{$prefix}.{$sortfield} {$sortdirection}";
 690                          }
 691                          $found = true;
 692                          break;
 693                      }
 694                  }
 695                  if (!$found) {
 696                      // The param is not found in $allowedprefixesandfields.
 697                      throw new coding_exception('Invalid $sort parameter in enrol_get_my_courses()');
 698                  }
 699              }
 700          }
 701          $sort = implode(',', $sorts);
 702          $orderby = "ORDER BY $sort";
 703      }
 704  
 705      $wheres = ['c.id <> ' . SITEID];
 706      $params = [];
 707  
 708      if (isset($USER->loginascontext) and $USER->loginascontext->contextlevel == CONTEXT_COURSE) {
 709          // list _only_ this course - anything else is asking for trouble...
 710          $wheres[] = "courseid = :loginas";
 711          $params['loginas'] = $USER->loginascontext->instanceid;
 712      }
 713  
 714      $coursefields = 'c.' .join(',c.', $fields);
 715      $ccselect = ', ' . context_helper::get_preload_record_columns_sql('ctx');
 716      $ccjoin = "LEFT JOIN {context} ctx ON (ctx.instanceid = c.id AND ctx.contextlevel = :contextlevel)";
 717      $params['contextlevel'] = CONTEXT_COURSE;
 718      $wheres = implode(" AND ", $wheres);
 719  
 720      $timeaccessselect = "";
 721      $timeaccessjoin = "";
 722  
 723      if (!empty($courseids)) {
 724          list($courseidssql, $courseidsparams) = $DB->get_in_or_equal($courseids, SQL_PARAMS_NAMED);
 725          $wheres = sprintf("%s AND c.id %s", $wheres, $courseidssql);
 726          $params = array_merge($params, $courseidsparams);
 727      }
 728  
 729      if (!empty($excludecourses)) {
 730          list($courseidssql, $courseidsparams) = $DB->get_in_or_equal($excludecourses, SQL_PARAMS_NAMED, 'param', false);
 731          $wheres = sprintf("%s AND c.id %s", $wheres, $courseidssql);
 732          $params = array_merge($params, $courseidsparams);
 733      }
 734  
 735      $courseidsql = "";
 736      // Logged-in, non-guest users get their enrolled courses.
 737      if (!isguestuser() && isloggedin()) {
 738          $courseidsql .= "
 739                  SELECT DISTINCT e.courseid
 740                    FROM {enrol} e
 741                    JOIN {user_enrolments} ue ON (ue.enrolid = e.id AND ue.userid = :userid1)
 742                   WHERE ue.status = :active AND e.status = :enabled AND ue.timestart <= :now1
 743                         AND (ue.timeend = 0 OR ue.timeend > :now2)";
 744          $params['userid1'] = $USER->id;
 745          $params['active'] = ENROL_USER_ACTIVE;
 746          $params['enabled'] = ENROL_INSTANCE_ENABLED;
 747          $params['now1'] = $params['now2'] = time();
 748  
 749          if ($sorttimeaccess) {
 750              $params['userid2'] = $USER->id;
 751              $timeaccessselect = ', ul.timeaccess as lastaccessed';
 752              $timeaccessjoin = "LEFT JOIN {user_lastaccess} ul ON (ul.courseid = c.id AND ul.userid = :userid2)";
 753          }
 754      }
 755  
 756      // When including non-enrolled but accessible courses...
 757      if ($allaccessible) {
 758          if (is_siteadmin()) {
 759              // Site admins can access all courses.
 760              $courseidsql = "SELECT DISTINCT c2.id AS courseid FROM {course} c2";
 761          } else {
 762              // If we used the enrolment as well, then this will be UNIONed.
 763              if ($courseidsql) {
 764                  $courseidsql .= " UNION ";
 765              }
 766  
 767              // Include courses with guest access and no password.
 768              $courseidsql .= "
 769                      SELECT DISTINCT e.courseid
 770                        FROM {enrol} e
 771                       WHERE e.enrol = 'guest' AND e.password = :emptypass AND e.status = :enabled2";
 772              $params['emptypass'] = '';
 773              $params['enabled2'] = ENROL_INSTANCE_ENABLED;
 774  
 775              // Include courses where the current user is currently using guest access (may include
 776              // those which require a password).
 777              $courseids = [];
 778              $accessdata = get_user_accessdata($USER->id);
 779              foreach ($accessdata['ra'] as $contextpath => $roles) {
 780                  if (array_key_exists($CFG->guestroleid, $roles)) {
 781                      // Work out the course id from context path.
 782                      $context = context::instance_by_id(preg_replace('~^.*/~', '', $contextpath));
 783                      if ($context instanceof context_course) {
 784                          $courseids[$context->instanceid] = true;
 785                      }
 786                  }
 787              }
 788  
 789              // Include courses where the current user has moodle/course:view capability.
 790              $courses = get_user_capability_course('moodle/course:view', null, false);
 791              if (!$courses) {
 792                  $courses = [];
 793              }
 794              foreach ($courses as $course) {
 795                  $courseids[$course->id] = true;
 796              }
 797  
 798              // If there are any in either category, list them individually.
 799              if ($courseids) {
 800                  list ($allowedsql, $allowedparams) = $DB->get_in_or_equal(
 801                          array_keys($courseids), SQL_PARAMS_NAMED);
 802                  $courseidsql .= "
 803                          UNION
 804                         SELECT DISTINCT c3.id AS courseid
 805                           FROM {course} c3
 806                          WHERE c3.id $allowedsql";
 807                  $params = array_merge($params, $allowedparams);
 808              }
 809          }
 810      }
 811  
 812      // Note: we can not use DISTINCT + text fields due to Oracle and MS limitations, that is why
 813      // we have the subselect there.
 814      $sql = "SELECT $coursefields $ccselect $timeaccessselect
 815                FROM {course} c
 816                JOIN ($courseidsql) en ON (en.courseid = c.id)
 817             $timeaccessjoin
 818             $ccjoin
 819               WHERE $wheres
 820            $orderby";
 821  
 822      $courses = $DB->get_records_sql($sql, $params, $offset, $limit);
 823  
 824      // preload contexts and check visibility
 825      foreach ($courses as $id=>$course) {
 826          context_helper::preload_from_record($course);
 827          if (!$course->visible) {
 828              if (!$context = context_course::instance($id, IGNORE_MISSING)) {
 829                  unset($courses[$id]);
 830                  continue;
 831              }
 832              if (!has_capability('moodle/course:viewhiddencourses', $context)) {
 833                  unset($courses[$id]);
 834                  continue;
 835              }
 836          }
 837          $courses[$id] = $course;
 838      }
 839  
 840      //wow! Is that really all? :-D
 841  
 842      return $courses;
 843  }
 844  
 845  /**
 846   * Returns course enrolment information icons.
 847   *
 848   * @param object $course
 849   * @param array $instances enrol instances of this course, improves performance
 850   * @return array of pix_icon
 851   */
 852  function enrol_get_course_info_icons($course, array $instances = NULL) {
 853      $icons = array();
 854      if (is_null($instances)) {
 855          $instances = enrol_get_instances($course->id, true);
 856      }
 857      $plugins = enrol_get_plugins(true);
 858      foreach ($plugins as $name => $plugin) {
 859          $pis = array();
 860          foreach ($instances as $instance) {
 861              if ($instance->status != ENROL_INSTANCE_ENABLED or $instance->courseid != $course->id) {
 862                  debugging('Invalid instances parameter submitted in enrol_get_info_icons()');
 863                  continue;
 864              }
 865              if ($instance->enrol == $name) {
 866                  $pis[$instance->id] = $instance;
 867              }
 868          }
 869          if ($pis) {
 870              $icons = array_merge($icons, $plugin->get_info_icons($pis));
 871          }
 872      }
 873      return $icons;
 874  }
 875  
 876  /**
 877   * Returns SQL ORDER arguments which reflect the admin settings to sort my courses.
 878   *
 879   * @param string|null $sort SQL ORDER arguments which were originally requested (optionally).
 880   * @return string SQL ORDER arguments.
 881   */
 882  function enrol_get_courses_sortingsql($sort = null) {
 883      global $CFG;
 884  
 885      // Prepare the visible SQL fragment as empty.
 886      $visible = '';
 887      // Only create a visible SQL fragment if the caller didn't already pass a sort order which contains the visible field.
 888      if ($sort === null || strpos($sort, 'visible') === false) {
 889          // If the admin did not explicitly want to have shown and hidden courses sorted as one list, we will sort hidden
 890          // courses to the end of the course list.
 891          if (!isset($CFG->navsortmycourseshiddenlast) || $CFG->navsortmycourseshiddenlast == true) {
 892              $visible = 'visible DESC, ';
 893          }
 894      }
 895  
 896      // Only create a sortorder SQL fragment if the caller didn't already pass one.
 897      if ($sort === null) {
 898          // If the admin has configured a course sort order, we will use this.
 899          if (!empty($CFG->navsortmycoursessort)) {
 900              $sort = $CFG->navsortmycoursessort . ' ASC';
 901  
 902              // Otherwise we will fall back to the sortorder sorting.
 903          } else {
 904              $sort = 'sortorder ASC';
 905          }
 906      }
 907  
 908      return $visible . $sort;
 909  }
 910  
 911  /**
 912   * Returns course enrolment detailed information.
 913   *
 914   * @param object $course
 915   * @return array of html fragments - can be used to construct lists
 916   */
 917  function enrol_get_course_description_texts($course) {
 918      $lines = array();
 919      $instances = enrol_get_instances($course->id, true);
 920      $plugins = enrol_get_plugins(true);
 921      foreach ($instances as $instance) {
 922          if (!isset($plugins[$instance->enrol])) {
 923              //weird
 924              continue;
 925          }
 926          $plugin = $plugins[$instance->enrol];
 927          $text = $plugin->get_description_text($instance);
 928          if ($text !== NULL) {
 929              $lines[] = $text;
 930          }
 931      }
 932      return $lines;
 933  }
 934  
 935  /**
 936   * Returns list of courses user is enrolled into.
 937   *
 938   * Note: Use {@link enrol_get_all_users_courses()} if you need the list without any capability checks.
 939   *
 940   * The $fields param is a list of field names to ADD so name just the fields you really need,
 941   * which will be added and uniq'd.
 942   *
 943   * @param int $userid User whose courses are returned, defaults to the current user.
 944   * @param bool $onlyactive Return only active enrolments in courses user may see.
 945   * @param string|array $fields Extra fields to be returned (array or comma-separated list).
 946   * @param string|null $sort Comma separated list of fields to sort by, defaults to respecting navsortmycoursessort.
 947   * @return array
 948   */
 949  function enrol_get_users_courses($userid, $onlyactive = false, $fields = null, $sort = null) {
 950      global $DB;
 951  
 952      $courses = enrol_get_all_users_courses($userid, $onlyactive, $fields, $sort);
 953  
 954      // preload contexts and check visibility
 955      if ($onlyactive) {
 956          foreach ($courses as $id=>$course) {
 957              context_helper::preload_from_record($course);
 958              if (!$course->visible) {
 959                  if (!$context = context_course::instance($id)) {
 960                      unset($courses[$id]);
 961                      continue;
 962                  }
 963                  if (!has_capability('moodle/course:viewhiddencourses', $context, $userid)) {
 964                      unset($courses[$id]);
 965                      continue;
 966                  }
 967              }
 968          }
 969      }
 970  
 971      return $courses;
 972  }
 973  
 974  /**
 975   * Returns list of roles per users into course.
 976   *
 977   * @param int $courseid Course id.
 978   * @return array Array[$userid][$roleid] = role_assignment.
 979   */
 980  function enrol_get_course_users_roles(int $courseid) : array {
 981      global $DB;
 982  
 983      $context = context_course::instance($courseid);
 984  
 985      $roles = array();
 986  
 987      $records = $DB->get_recordset('role_assignments', array('contextid' => $context->id));
 988      foreach ($records as $record) {
 989          if (isset($roles[$record->userid]) === false) {
 990              $roles[$record->userid] = array();
 991          }
 992          $roles[$record->userid][$record->roleid] = $record;
 993      }
 994      $records->close();
 995  
 996      return $roles;
 997  }
 998  
 999  /**
1000   * Can user access at least one enrolled course?
1001   *
1002   * Cheat if necessary, but find out as fast as possible!
1003   *
1004   * @param int|stdClass $user null means use current user
1005   * @return bool
1006   */
1007  function enrol_user_sees_own_courses($user = null) {
1008      global $USER;
1009  
1010      if ($user === null) {
1011          $user = $USER;
1012      }
1013      $userid = is_object($user) ? $user->id : $user;
1014  
1015      // Guest account does not have any courses
1016      if (isguestuser($userid) or empty($userid)) {
1017          return false;
1018      }
1019  
1020      // Let's cheat here if this is the current user,
1021      // if user accessed any course recently, then most probably
1022      // we do not need to query the database at all.
1023      if ($USER->id == $userid) {
1024          if (!empty($USER->enrol['enrolled'])) {
1025              foreach ($USER->enrol['enrolled'] as $until) {
1026                  if ($until > time()) {
1027                      return true;
1028                  }
1029              }
1030          }
1031      }
1032  
1033      // Now the slow way.
1034      $courses = enrol_get_all_users_courses($userid, true);
1035      foreach($courses as $course) {
1036          if ($course->visible) {
1037              return true;
1038          }
1039          context_helper::preload_from_record($course);
1040          $context = context_course::instance($course->id);
1041          if (has_capability('moodle/course:viewhiddencourses', $context, $user)) {
1042              return true;
1043          }
1044      }
1045  
1046      return false;
1047  }
1048  
1049  /**
1050   * Returns list of courses user is enrolled into without performing any capability checks.
1051   *
1052   * The $fields param is a list of field names to ADD so name just the fields you really need,
1053   * which will be added and uniq'd.
1054   *
1055   * @param int $userid User whose courses are returned, defaults to the current user.
1056   * @param bool $onlyactive Return only active enrolments in courses user may see.
1057   * @param string|array $fields Extra fields to be returned (array or comma-separated list).
1058   * @param string|null $sort Comma separated list of fields to sort by, defaults to respecting navsortmycoursessort.
1059   * @return array
1060   */
1061  function enrol_get_all_users_courses($userid, $onlyactive = false, $fields = null, $sort = null) {
1062      global $DB;
1063  
1064      // Re-Arrange the course sorting according to the admin settings.
1065      $sort = enrol_get_courses_sortingsql($sort);
1066  
1067      // Guest account does not have any courses
1068      if (isguestuser($userid) or empty($userid)) {
1069          return(array());
1070      }
1071  
1072      $basefields = array('id', 'category', 'sortorder',
1073              'shortname', 'fullname', 'idnumber',
1074              'startdate', 'visible',
1075              'defaultgroupingid',
1076              'groupmode', 'groupmodeforce');
1077  
1078      if (empty($fields)) {
1079          $fields = $basefields;
1080      } else if (is_string($fields)) {
1081          // turn the fields from a string to an array
1082          $fields = explode(',', $fields);
1083          $fields = array_map('trim', $fields);
1084          $fields = array_unique(array_merge($basefields, $fields));
1085      } else if (is_array($fields)) {
1086          $fields = array_unique(array_merge($basefields, $fields));
1087      } else {
1088          throw new coding_exception('Invalid $fields parameter in enrol_get_all_users_courses()');
1089      }
1090      if (in_array('*', $fields)) {
1091          $fields = array('*');
1092      }
1093  
1094      $orderby = "";
1095      $sort    = trim($sort);
1096      if (!empty($sort)) {
1097          $rawsorts = explode(',', $sort);
1098          $sorts = array();
1099          foreach ($rawsorts as $rawsort) {
1100              $rawsort = trim($rawsort);
1101              if (strpos($rawsort, 'c.') === 0) {
1102                  $rawsort = substr($rawsort, 2);
1103              }
1104              $sorts[] = trim($rawsort);
1105          }
1106          $sort = 'c.'.implode(',c.', $sorts);
1107          $orderby = "ORDER BY $sort";
1108      }
1109  
1110      $params = [];
1111  
1112      if ($onlyactive) {
1113          $subwhere = "WHERE ue.status = :active AND e.status = :enabled AND ue.timestart < :now1 AND (ue.timeend = 0 OR ue.timeend > :now2)";
1114          $params['now1']    = round(time(), -2); // improves db caching
1115          $params['now2']    = $params['now1'];
1116          $params['active']  = ENROL_USER_ACTIVE;
1117          $params['enabled'] = ENROL_INSTANCE_ENABLED;
1118      } else {
1119          $subwhere = "";
1120      }
1121  
1122      $coursefields = 'c.' .join(',c.', $fields);
1123      $ccselect = ', ' . context_helper::get_preload_record_columns_sql('ctx');
1124      $ccjoin = "LEFT JOIN {context} ctx ON (ctx.instanceid = c.id AND ctx.contextlevel = :contextlevel)";
1125      $params['contextlevel'] = CONTEXT_COURSE;
1126  
1127      //note: we can not use DISTINCT + text fields due to Oracle and MS limitations, that is why we have the subselect there
1128      $sql = "SELECT $coursefields $ccselect
1129                FROM {course} c
1130                JOIN (SELECT DISTINCT e.courseid
1131                        FROM {enrol} e
1132                        JOIN {user_enrolments} ue ON (ue.enrolid = e.id AND ue.userid = :userid)
1133                   $subwhere
1134                     ) en ON (en.courseid = c.id)
1135             $ccjoin
1136               WHERE c.id <> " . SITEID . "
1137            $orderby";
1138      $params['userid']  = $userid;
1139  
1140      $courses = $DB->get_records_sql($sql, $params);
1141  
1142      return $courses;
1143  }
1144  
1145  
1146  
1147  /**
1148   * Called when user is about to be deleted.
1149   * @param object $user
1150   * @return void
1151   */
1152  function enrol_user_delete($user) {
1153      global $DB;
1154  
1155      $plugins = enrol_get_plugins(true);
1156      foreach ($plugins as $plugin) {
1157          $plugin->user_delete($user);
1158      }
1159  
1160      // force cleanup of all broken enrolments
1161      $DB->delete_records('user_enrolments', array('userid'=>$user->id));
1162  }
1163  
1164  /**
1165   * Called when course is about to be deleted.
1166   * If a user id is passed, only enrolments that the user has permission to un-enrol will be removed,
1167   * otherwise all enrolments in the course will be removed.
1168   *
1169   * @param stdClass $course
1170   * @param int|null $userid
1171   * @return void
1172   */
1173  function enrol_course_delete($course, $userid = null) {
1174      global $DB;
1175  
1176      $context = context_course::instance($course->id);
1177      $instances = enrol_get_instances($course->id, false);
1178      $plugins = enrol_get_plugins(true);
1179  
1180      if ($userid) {
1181          // If the user id is present, include only course enrolment instances which allow manual unenrolment and
1182          // the given user have a capability to perform unenrolment.
1183          $instances = array_filter($instances, function($instance) use ($userid, $plugins, $context) {
1184              $unenrolcap = "enrol/{$instance->enrol}:unenrol";
1185              return $plugins[$instance->enrol]->allow_unenrol($instance) &&
1186                  has_capability($unenrolcap, $context, $userid);
1187          });
1188      }
1189  
1190      foreach ($instances as $instance) {
1191          if (isset($plugins[$instance->enrol])) {
1192              $plugins[$instance->enrol]->delete_instance($instance);
1193          }
1194          // low level delete in case plugin did not do it
1195          $DB->delete_records('role_assignments', array('itemid'=>$instance->id, 'component'=>'enrol_'.$instance->enrol));
1196          $DB->delete_records('user_enrolments', array('enrolid'=>$instance->id));
1197          $DB->delete_records('enrol', array('id'=>$instance->id));
1198      }
1199  }
1200  
1201  /**
1202   * Try to enrol user via default internal auth plugin.
1203   *
1204   * For now this is always using the manual enrol plugin...
1205   *
1206   * @param $courseid
1207   * @param $userid
1208   * @param $roleid
1209   * @param $timestart
1210   * @param $timeend
1211   * @return bool success
1212   */
1213  function enrol_try_internal_enrol($courseid, $userid, $roleid = null, $timestart = 0, $timeend = 0) {
1214      global $DB;
1215  
1216      //note: this is hardcoded to manual plugin for now
1217  
1218      if (!enrol_is_enabled('manual')) {
1219          return false;
1220      }
1221  
1222      if (!$enrol = enrol_get_plugin('manual')) {
1223          return false;
1224      }
1225      if (!$instances = $DB->get_records('enrol', array('enrol'=>'manual', 'courseid'=>$courseid, 'status'=>ENROL_INSTANCE_ENABLED), 'sortorder,id ASC')) {
1226          return false;
1227      }
1228  
1229      if ($roleid && !$DB->record_exists('role', ['id' => $roleid])) {
1230          return false;
1231      }
1232  
1233      $instance = reset($instances);
1234      $enrol->enrol_user($instance, $userid, $roleid, $timestart, $timeend);
1235  
1236      return true;
1237  }
1238  
1239  /**
1240   * Is there a chance users might self enrol
1241   * @param int $courseid
1242   * @return bool
1243   */
1244  function enrol_selfenrol_available($courseid) {
1245      $result = false;
1246  
1247      $plugins = enrol_get_plugins(true);
1248      $enrolinstances = enrol_get_instances($courseid, true);
1249      foreach($enrolinstances as $instance) {
1250          if (!isset($plugins[$instance->enrol])) {
1251              continue;
1252          }
1253          if ($instance->enrol === 'guest') {
1254              continue;
1255          }
1256          if ((isguestuser() || !isloggedin()) &&
1257              ($plugins[$instance->enrol]->is_self_enrol_available($instance) === true)) {
1258              $result = true;
1259              break;
1260          }
1261          if ($plugins[$instance->enrol]->show_enrolme_link($instance) === true) {
1262              $result = true;
1263              break;
1264          }
1265      }
1266  
1267      return $result;
1268  }
1269  
1270  /**
1271   * This function returns the end of current active user enrolment.
1272   *
1273   * It deals correctly with multiple overlapping user enrolments.
1274   *
1275   * @param int $courseid
1276   * @param int $userid
1277   * @return int|bool timestamp when active enrolment ends, false means no active enrolment now, 0 means never
1278   */
1279  function enrol_get_enrolment_end($courseid, $userid) {
1280      global $DB;
1281  
1282      $sql = "SELECT ue.*
1283                FROM {user_enrolments} ue
1284                JOIN {enrol} e ON (e.id = ue.enrolid AND e.courseid = :courseid)
1285                JOIN {user} u ON u.id = ue.userid
1286               WHERE ue.userid = :userid AND ue.status = :active AND e.status = :enabled AND u.deleted = 0";
1287      $params = array('enabled'=>ENROL_INSTANCE_ENABLED, 'active'=>ENROL_USER_ACTIVE, 'userid'=>$userid, 'courseid'=>$courseid);
1288  
1289      if (!$enrolments = $DB->get_records_sql($sql, $params)) {
1290          return false;
1291      }
1292  
1293      $changes = array();
1294  
1295      foreach ($enrolments as $ue) {
1296          $start = (int)$ue->timestart;
1297          $end = (int)$ue->timeend;
1298          if ($end != 0 and $end < $start) {
1299              debugging('Invalid enrolment start or end in user_enrolment id:'.$ue->id);
1300              continue;
1301          }
1302          if (isset($changes[$start])) {
1303              $changes[$start] = $changes[$start] + 1;
1304          } else {
1305              $changes[$start] = 1;
1306          }
1307          if ($end === 0) {
1308              // no end
1309          } else if (isset($changes[$end])) {
1310              $changes[$end] = $changes[$end] - 1;
1311          } else {
1312              $changes[$end] = -1;
1313          }
1314      }
1315  
1316      // let's sort then enrolment starts&ends and go through them chronologically,
1317      // looking for current status and the next future end of enrolment
1318      ksort($changes);
1319  
1320      $now = time();
1321      $current = 0;
1322      $present = null;
1323  
1324      foreach ($changes as $time => $change) {
1325          if ($time > $now) {
1326              if ($present === null) {
1327                  // we have just went past current time
1328                  $present = $current;
1329                  if ($present < 1) {
1330                      // no enrolment active
1331                      return false;
1332                  }
1333              }
1334              if ($present !== null) {
1335                  // we are already in the future - look for possible end
1336                  if ($current + $change < 1) {
1337                      return $time;
1338                  }
1339              }
1340          }
1341          $current += $change;
1342      }
1343  
1344      if ($current > 0) {
1345          return 0;
1346      } else {
1347          return false;
1348      }
1349  }
1350  
1351  /**
1352   * Is current user accessing course via this enrolment method?
1353   *
1354   * This is intended for operations that are going to affect enrol instances.
1355   *
1356   * @param stdClass $instance enrol instance
1357   * @return bool
1358   */
1359  function enrol_accessing_via_instance(stdClass $instance) {
1360      global $DB, $USER;
1361  
1362      if (empty($instance->id)) {
1363          return false;
1364      }
1365  
1366      if (is_siteadmin()) {
1367          // Admins may go anywhere.
1368          return false;
1369      }
1370  
1371      return $DB->record_exists('user_enrolments', array('userid'=>$USER->id, 'enrolid'=>$instance->id));
1372  }
1373  
1374  /**
1375   * Returns true if user is enrolled (is participating) in course
1376   * this is intended for students and teachers.
1377   *
1378   * Since 2.2 the result for active enrolments and current user are cached.
1379   *
1380   * @param context $context
1381   * @param int|stdClass $user if null $USER is used, otherwise user object or id expected
1382   * @param string $withcapability extra capability name
1383   * @param bool $onlyactive consider only active enrolments in enabled plugins and time restrictions
1384   * @return bool
1385   */
1386  function is_enrolled(context $context, $user = null, $withcapability = '', $onlyactive = false) {
1387      global $USER, $DB;
1388  
1389      // First find the course context.
1390      $coursecontext = $context->get_course_context();
1391  
1392      // Make sure there is a real user specified.
1393      if ($user === null) {
1394          $userid = isset($USER->id) ? $USER->id : 0;
1395      } else {
1396          $userid = is_object($user) ? $user->id : $user;
1397      }
1398  
1399      if (empty($userid)) {
1400          // Not-logged-in!
1401          return false;
1402      } else if (isguestuser($userid)) {
1403          // Guest account can not be enrolled anywhere.
1404          return false;
1405      }
1406  
1407      // Note everybody participates on frontpage, so for other contexts...
1408      if ($coursecontext->instanceid != SITEID) {
1409          // Try cached info first - the enrolled flag is set only when active enrolment present.
1410          if ($USER->id == $userid) {
1411              $coursecontext->reload_if_dirty();
1412              if (isset($USER->enrol['enrolled'][$coursecontext->instanceid])) {
1413                  if ($USER->enrol['enrolled'][$coursecontext->instanceid] > time()) {
1414                      if ($withcapability and !has_capability($withcapability, $context, $userid)) {
1415                          return false;
1416                      }
1417                      return true;
1418                  }
1419              }
1420          }
1421  
1422          if ($onlyactive) {
1423              // Look for active enrolments only.
1424              $until = enrol_get_enrolment_end($coursecontext->instanceid, $userid);
1425  
1426              if ($until === false) {
1427                  return false;
1428              }
1429  
1430              if ($USER->id == $userid) {
1431                  if ($until == 0) {
1432                      $until = ENROL_MAX_TIMESTAMP;
1433                  }
1434                  $USER->enrol['enrolled'][$coursecontext->instanceid] = $until;
1435                  if (isset($USER->enrol['tempguest'][$coursecontext->instanceid])) {
1436                      unset($USER->enrol['tempguest'][$coursecontext->instanceid]);
1437                      remove_temp_course_roles($coursecontext);
1438                  }
1439              }
1440  
1441          } else {
1442              // Any enrolment is good for us here, even outdated, disabled or inactive.
1443              $sql = "SELECT 'x'
1444                        FROM {user_enrolments} ue
1445                        JOIN {enrol} e ON (e.id = ue.enrolid AND e.courseid = :courseid)
1446                        JOIN {user} u ON u.id = ue.userid
1447                       WHERE ue.userid = :userid AND u.deleted = 0";
1448              $params = array('userid' => $userid, 'courseid' => $coursecontext->instanceid);
1449              if (!$DB->record_exists_sql($sql, $params)) {
1450                  return false;
1451              }
1452          }
1453      }
1454  
1455      if ($withcapability and !has_capability($withcapability, $context, $userid)) {
1456          return false;
1457      }
1458  
1459      return true;
1460  }
1461  
1462  /**
1463   * Returns an array of joins, wheres and params that will limit the group of
1464   * users to only those enrolled and with given capability (if specified).
1465   *
1466   * Note this join will return duplicate rows for users who have been enrolled
1467   * several times (e.g. as manual enrolment, and as self enrolment). You may
1468   * need to use a SELECT DISTINCT in your query (see get_enrolled_sql for example).
1469   *
1470   * In case is guaranteed some of the joins never match any rows, the resulting
1471   * join_sql->cannotmatchanyrows will be true. This happens when the capability
1472   * is prohibited.
1473   *
1474   * @param context $context
1475   * @param string $prefix optional, a prefix to the user id column
1476   * @param string|array $capability optional, may include a capability name, or array of names.
1477   *      If an array is provided then this is the equivalent of a logical 'OR',
1478   *      i.e. the user needs to have one of these capabilities.
1479   * @param int|array $groupids The groupids, 0 or [] means all groups and USERSWITHOUTGROUP no group
1480   * @param bool $onlyactive consider only active enrolments in enabled plugins and time restrictions
1481   * @param bool $onlysuspended inverse of onlyactive, consider only suspended enrolments
1482   * @param int $enrolid The enrolment ID. If not 0, only users enrolled using this enrolment method will be returned.
1483   * @return \core\dml\sql_join Contains joins, wheres, params and cannotmatchanyrows
1484   */
1485  function get_enrolled_with_capabilities_join(context $context, $prefix = '', $capability = '', $groupids = 0,
1486          $onlyactive = false, $onlysuspended = false, $enrolid = 0) {
1487      $uid = $prefix . 'u.id';
1488      $joins = array();
1489      $wheres = array();
1490      $cannotmatchanyrows = false;
1491  
1492      $enrolledjoin = get_enrolled_join($context, $uid, $onlyactive, $onlysuspended, $enrolid);
1493      $joins[] = $enrolledjoin->joins;
1494      $wheres[] = $enrolledjoin->wheres;
1495      $params = $enrolledjoin->params;
1496      $cannotmatchanyrows = $cannotmatchanyrows || $enrolledjoin->cannotmatchanyrows;
1497  
1498      if (!empty($capability)) {
1499          $capjoin = get_with_capability_join($context, $capability, $uid);
1500          $joins[] = $capjoin->joins;
1501          $wheres[] = $capjoin->wheres;
1502          $params = array_merge($params, $capjoin->params);
1503          $cannotmatchanyrows = $cannotmatchanyrows || $capjoin->cannotmatchanyrows;
1504      }
1505  
1506      if ($groupids) {
1507          $groupjoin = groups_get_members_join($groupids, $uid, $context);
1508          $joins[] = $groupjoin->joins;
1509          $params = array_merge($params, $groupjoin->params);
1510          if (!empty($groupjoin->wheres)) {
1511              $wheres[] = $groupjoin->wheres;
1512          }
1513          $cannotmatchanyrows = $cannotmatchanyrows || $groupjoin->cannotmatchanyrows;
1514      }
1515  
1516      $joins = implode("\n", $joins);
1517      $wheres[] = "{$prefix}u.deleted = 0";
1518      $wheres = implode(" AND ", $wheres);
1519  
1520      return new \core\dml\sql_join($joins, $wheres, $params, $cannotmatchanyrows);
1521  }
1522  
1523  /**
1524   * Returns array with sql code and parameters returning all ids
1525   * of users enrolled into course.
1526   *
1527   * This function is using 'eu[0-9]+_' prefix for table names and parameters.
1528   *
1529   * @param context $context
1530   * @param string $withcapability
1531   * @param int|array $groupids The groupids, 0 or [] means all groups and USERSWITHOUTGROUP no group
1532   * @param bool $onlyactive consider only active enrolments in enabled plugins and time restrictions
1533   * @param bool $onlysuspended inverse of onlyactive, consider only suspended enrolments
1534   * @param int $enrolid The enrolment ID. If not 0, only users enrolled using this enrolment method will be returned.
1535   * @return array list($sql, $params)
1536   */
1537  function get_enrolled_sql(context $context, $withcapability = '', $groupids = 0, $onlyactive = false, $onlysuspended = false,
1538                            $enrolid = 0) {
1539  
1540      // Use unique prefix just in case somebody makes some SQL magic with the result.
1541      static $i = 0;
1542      $i++;
1543      $prefix = 'eu' . $i . '_';
1544  
1545      $capjoin = get_enrolled_with_capabilities_join(
1546              $context, $prefix, $withcapability, $groupids, $onlyactive, $onlysuspended, $enrolid);
1547  
1548      $sql = "SELECT DISTINCT {$prefix}u.id
1549                FROM {user} {$prefix}u
1550              $capjoin->joins
1551               WHERE $capjoin->wheres";
1552  
1553      return array($sql, $capjoin->params);
1554  }
1555  
1556  /**
1557   * Returns array with sql joins and parameters returning all ids
1558   * of users enrolled into course.
1559   *
1560   * This function is using 'ej[0-9]+_' prefix for table names and parameters.
1561   *
1562   * @throws coding_exception
1563   *
1564   * @param context $context
1565   * @param string $useridcolumn User id column used the calling query, e.g. u.id
1566   * @param bool $onlyactive consider only active enrolments in enabled plugins and time restrictions
1567   * @param bool $onlysuspended inverse of onlyactive, consider only suspended enrolments
1568   * @param int $enrolid The enrolment ID. If not 0, only users enrolled using this enrolment method will be returned.
1569   * @return \core\dml\sql_join Contains joins, wheres, params
1570   */
1571  function get_enrolled_join(context $context, $useridcolumn, $onlyactive = false, $onlysuspended = false, $enrolid = 0) {
1572      // Use unique prefix just in case somebody makes some SQL magic with the result.
1573      static $i = 0;
1574      $i++;
1575      $prefix = 'ej' . $i . '_';
1576  
1577      // First find the course context.
1578      $coursecontext = $context->get_course_context();
1579  
1580      $isfrontpage = ($coursecontext->instanceid == SITEID);
1581  
1582      if ($onlyactive && $onlysuspended) {
1583          throw new coding_exception("Both onlyactive and onlysuspended are set, this is probably not what you want!");
1584      }
1585      if ($isfrontpage && $onlysuspended) {
1586          throw new coding_exception("onlysuspended is not supported on frontpage; please add your own early-exit!");
1587      }
1588  
1589      $joins  = array();
1590      $wheres = array();
1591      $params = array();
1592  
1593      $wheres[] = "1 = 1"; // Prevent broken where clauses later on.
1594  
1595      // Note all users are "enrolled" on the frontpage, but for others...
1596      if (!$isfrontpage) {
1597          $where1 = "{$prefix}ue.status = :{$prefix}active AND {$prefix}e.status = :{$prefix}enabled";
1598          $where2 = "{$prefix}ue.timestart < :{$prefix}now1 AND ({$prefix}ue.timeend = 0 OR {$prefix}ue.timeend > :{$prefix}now2)";
1599  
1600          $enrolconditions = array(
1601              "{$prefix}e.id = {$prefix}ue.enrolid",
1602              "{$prefix}e.courseid = :{$prefix}courseid",
1603          );
1604          if ($enrolid) {
1605              $enrolconditions[] = "{$prefix}e.id = :{$prefix}enrolid";
1606              $params[$prefix . 'enrolid'] = $enrolid;
1607          }
1608          $enrolconditionssql = implode(" AND ", $enrolconditions);
1609          $ejoin = "JOIN {enrol} {$prefix}e ON ($enrolconditionssql)";
1610  
1611          $params[$prefix.'courseid'] = $coursecontext->instanceid;
1612  
1613          if (!$onlysuspended) {
1614              $joins[] = "JOIN {user_enrolments} {$prefix}ue ON {$prefix}ue.userid = $useridcolumn";
1615              $joins[] = $ejoin;
1616              if ($onlyactive) {
1617                  $wheres[] = "$where1 AND $where2";
1618              }
1619          } else {
1620              // Suspended only where there is enrolment but ALL are suspended.
1621              // Consider multiple enrols where one is not suspended or plain role_assign.
1622              $enrolselect = "SELECT DISTINCT {$prefix}ue.userid FROM {user_enrolments} {$prefix}ue $ejoin WHERE $where1 AND $where2";
1623              $joins[] = "JOIN {user_enrolments} {$prefix}ue1 ON {$prefix}ue1.userid = $useridcolumn";
1624              $enrolconditions = array(
1625                  "{$prefix}e1.id = {$prefix}ue1.enrolid",
1626                  "{$prefix}e1.courseid = :{$prefix}_e1_courseid",
1627              );
1628              if ($enrolid) {
1629                  $enrolconditions[] = "{$prefix}e1.id = :{$prefix}e1_enrolid";
1630                  $params[$prefix . 'e1_enrolid'] = $enrolid;
1631              }
1632              $enrolconditionssql = implode(" AND ", $enrolconditions);
1633              $joins[] = "JOIN {enrol} {$prefix}e1 ON ($enrolconditionssql)";
1634              $params["{$prefix}_e1_courseid"] = $coursecontext->instanceid;
1635              $wheres[] = "$useridcolumn NOT IN ($enrolselect)";
1636          }
1637  
1638          if ($onlyactive || $onlysuspended) {
1639              $now = round(time(), -2); // Rounding helps caching in DB.
1640              $params = array_merge($params, array($prefix . 'enabled' => ENROL_INSTANCE_ENABLED,
1641                      $prefix . 'active' => ENROL_USER_ACTIVE,
1642                      $prefix . 'now1' => $now, $prefix . 'now2' => $now));
1643          }
1644      }
1645  
1646      $joins = implode("\n", $joins);
1647      $wheres = implode(" AND ", $wheres);
1648  
1649      return new \core\dml\sql_join($joins, $wheres, $params);
1650  }
1651  
1652  /**
1653   * Returns list of users enrolled into course.
1654   *
1655   * @param context $context
1656   * @param string $withcapability
1657   * @param int|array $groupids The groupids, 0 or [] means all groups and USERSWITHOUTGROUP no group
1658   * @param string $userfields requested user record fields
1659   * @param string $orderby
1660   * @param int $limitfrom return a subset of records, starting at this point (optional, required if $limitnum is set).
1661   * @param int $limitnum return a subset comprising this many records (optional, required if $limitfrom is set).
1662   * @param bool $onlyactive consider only active enrolments in enabled plugins and time restrictions
1663   * @return array of user records
1664   */
1665  function get_enrolled_users(context $context, $withcapability = '', $groupids = 0, $userfields = 'u.*', $orderby = null,
1666          $limitfrom = 0, $limitnum = 0, $onlyactive = false) {
1667      global $DB;
1668  
1669      list($esql, $params) = get_enrolled_sql($context, $withcapability, $groupids, $onlyactive);
1670      $sql = "SELECT $userfields
1671                FROM {user} u
1672                JOIN ($esql) je ON je.id = u.id
1673               WHERE u.deleted = 0";
1674  
1675      if ($orderby) {
1676          $sql = "$sql ORDER BY $orderby";
1677      } else {
1678          list($sort, $sortparams) = users_order_by_sql('u');
1679          $sql = "$sql ORDER BY $sort";
1680          $params = array_merge($params, $sortparams);
1681      }
1682  
1683      return $DB->get_records_sql($sql, $params, $limitfrom, $limitnum);
1684  }
1685  
1686  /**
1687   * Counts list of users enrolled into course (as per above function)
1688   *
1689   * @param context $context
1690   * @param string $withcapability
1691   * @param int|array $groupids The groupids, 0 or [] means all groups and USERSWITHOUTGROUP no group
1692   * @param bool $onlyactive consider only active enrolments in enabled plugins and time restrictions
1693   * @return int number of users enrolled into course
1694   */
1695  function count_enrolled_users(context $context, $withcapability = '', $groupids = 0, $onlyactive = false) {
1696      global $DB;
1697  
1698      $capjoin = get_enrolled_with_capabilities_join(
1699              $context, '', $withcapability, $groupids, $onlyactive);
1700  
1701      $sql = "SELECT COUNT(DISTINCT u.id)
1702                FROM {user} u
1703              $capjoin->joins
1704               WHERE $capjoin->wheres AND u.deleted = 0";
1705  
1706      return $DB->count_records_sql($sql, $capjoin->params);
1707  }
1708  
1709  /**
1710   * Send welcome email "from" options.
1711   *
1712   * @return array list of from options
1713   */
1714  function enrol_send_welcome_email_options() {
1715      return [
1716          ENROL_DO_NOT_SEND_EMAIL                 => get_string('no'),
1717          ENROL_SEND_EMAIL_FROM_COURSE_CONTACT    => get_string('sendfromcoursecontact', 'enrol'),
1718          ENROL_SEND_EMAIL_FROM_KEY_HOLDER        => get_string('sendfromkeyholder', 'enrol'),
1719          ENROL_SEND_EMAIL_FROM_NOREPLY           => get_string('sendfromnoreply', 'enrol')
1720      ];
1721  }
1722  
1723  /**
1724   * Serve the user enrolment form as a fragment.
1725   *
1726   * @param array $args List of named arguments for the fragment loader.
1727   * @return string
1728   */
1729  function enrol_output_fragment_user_enrolment_form($args) {
1730      global $CFG, $DB;
1731  
1732      $args = (object) $args;
1733      $context = $args->context;
1734      require_capability('moodle/course:enrolreview', $context);
1735  
1736      $ueid = $args->ueid;
1737      $userenrolment = $DB->get_record('user_enrolments', ['id' => $ueid], '*', MUST_EXIST);
1738      $instance = $DB->get_record('enrol', ['id' => $userenrolment->enrolid], '*', MUST_EXIST);
1739      $plugin = enrol_get_plugin($instance->enrol);
1740      $customdata = [
1741          'ue' => $userenrolment,
1742          'modal' => true,
1743          'enrolinstancename' => $plugin->get_instance_name($instance)
1744      ];
1745  
1746      // Set the data if applicable.
1747      $data = [];
1748      if (isset($args->formdata)) {
1749          $serialiseddata = json_decode($args->formdata);
1750          parse_str($serialiseddata, $data);
1751      }
1752  
1753      require_once("$CFG->dirroot/enrol/editenrolment_form.php");
1754      $mform = new \enrol_user_enrolment_form(null, $customdata, 'post', '', null, true, $data);
1755  
1756      if (!empty($data)) {
1757          $mform->set_data($data);
1758          $mform->is_validated();
1759      }
1760  
1761      return $mform->render();
1762  }
1763  
1764  /**
1765   * Returns the course where a user enrolment belong to.
1766   *
1767   * @param int $ueid user_enrolments id
1768   * @return stdClass
1769   */
1770  function enrol_get_course_by_user_enrolment_id($ueid) {
1771      global $DB;
1772      $sql = "SELECT c.* FROM {user_enrolments} ue
1773                JOIN {enrol} e ON e.id = ue.enrolid
1774                JOIN {course} c ON c.id = e.courseid
1775               WHERE ue.id = :ueid";
1776      return $DB->get_record_sql($sql, array('ueid' => $ueid));
1777  }
1778  
1779  /**
1780   * Return all users enrolled in a course.
1781   *
1782   * @param int $courseid Course id or false if using $uefilter (user enrolment ids may belong to different courses)
1783   * @param bool $onlyactive consider only active enrolments in enabled plugins and time restrictions
1784   * @param array $usersfilter Limit the results obtained to this list of user ids. $uefilter compatibility not guaranteed.
1785   * @param array $uefilter Limit the results obtained to this list of user enrolment ids. $usersfilter compatibility not guaranteed.
1786   * @param array $usergroups Limit the results of users to the ones that belong to one of the submitted group ids.
1787   * @return stdClass[]
1788   */
1789  function enrol_get_course_users($courseid = false, $onlyactive = false, $usersfilter = [], $uefilter = [],
1790                                  $usergroups = []) {
1791      global $DB;
1792  
1793      if (!$courseid && !$usersfilter && !$uefilter) {
1794          throw new \coding_exception('You should specify at least 1 filter: courseid, users or user enrolments');
1795      }
1796  
1797      $sql = "SELECT ue.id AS ueid, ue.status AS uestatus, ue.enrolid AS ueenrolid, ue.timestart AS uetimestart,
1798               ue.timeend AS uetimeend, ue.modifierid AS uemodifierid, ue.timecreated AS uetimecreated,
1799               ue.timemodified AS uetimemodified, e.status AS estatus,
1800               u.* FROM {user_enrolments} ue
1801                JOIN {enrol} e ON e.id = ue.enrolid
1802                JOIN {user} u ON ue.userid = u.id
1803               WHERE ";
1804      $params = array();
1805  
1806      if ($courseid) {
1807          $conditions[] = "e.courseid = :courseid";
1808          $params['courseid'] = $courseid;
1809      }
1810  
1811      if ($onlyactive) {
1812          $conditions[] = "ue.status = :active AND e.status = :enabled AND ue.timestart < :now1 AND " .
1813              "(ue.timeend = 0 OR ue.timeend > :now2)";
1814          // Improves db caching.
1815          $params['now1']    = round(time(), -2);
1816          $params['now2']    = $params['now1'];
1817          $params['active']  = ENROL_USER_ACTIVE;
1818          $params['enabled'] = ENROL_INSTANCE_ENABLED;
1819      }
1820  
1821      if ($usersfilter) {
1822          list($usersql, $userparams) = $DB->get_in_or_equal($usersfilter, SQL_PARAMS_NAMED);
1823          $conditions[] = "ue.userid $usersql";
1824          $params = $params + $userparams;
1825      }
1826  
1827      if ($uefilter) {
1828          list($uesql, $ueparams) = $DB->get_in_or_equal($uefilter, SQL_PARAMS_NAMED);
1829          $conditions[] = "ue.id $uesql";
1830          $params = $params + $ueparams;
1831      }
1832  
1833      // Only select enrolled users that belong to a specific group(s).
1834      if (!empty($usergroups)) {
1835          $usergroups = array_map(function ($item) { // Sanitize groupid to int to be save for sql.
1836              return (int)$item;
1837          }, $usergroups);
1838          list($ugsql, $ugparams) = $DB->get_in_or_equal($usergroups, SQL_PARAMS_NAMED);
1839          $conditions[] = 'ue.userid IN (SELECT userid FROM {groups_members} WHERE groupid ' . $ugsql . ')';
1840          $params = $params + $ugparams;
1841      }
1842  
1843      return $DB->get_records_sql($sql . ' ' . implode(' AND ', $conditions), $params);
1844  }
1845  
1846  /**
1847   * Get the list of options for the enrolment period dropdown
1848   *
1849   * @return array List of options for the enrolment period dropdown
1850   */
1851  function enrol_get_period_list() {
1852      $periodmenu = [];
1853      $periodmenu[''] = get_string('unlimited');
1854      for ($i = 1; $i <= 365; $i++) {
1855          $seconds = $i * DAYSECS;
1856          $periodmenu[$seconds] = get_string('numdays', '', $i);
1857      }
1858      return $periodmenu;
1859  }
1860  
1861  /**
1862   * Calculate duration base on start time and end time
1863   *
1864   * @param int $timestart Time start
1865   * @param int $timeend Time end
1866   * @return float|int Calculated duration
1867   */
1868  function enrol_calculate_duration($timestart, $timeend) {
1869      $duration = floor(($timeend - $timestart) / DAYSECS) * DAYSECS;
1870      return $duration;
1871  }
1872  
1873  /**
1874   * Enrolment plugins abstract class.
1875   *
1876   * All enrol plugins should be based on this class,
1877   * this is also the main source of documentation.
1878   *
1879   * @copyright  2010 Petr Skoda {@link http://skodak.org}
1880   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
1881   */
1882  abstract class enrol_plugin {
1883      protected $config = null;
1884  
1885      /**
1886       * Returns name of this enrol plugin
1887       * @return string
1888       */
1889      public function get_name() {
1890          // second word in class is always enrol name, sorry, no fancy plugin names with _
1891          $words = explode('_', get_class($this));
1892          return $words[1];
1893      }
1894  
1895      /**
1896       * Returns localised name of enrol instance
1897       *
1898       * @param object $instance (null is accepted too)
1899       * @return string
1900       */
1901      public function get_instance_name($instance) {
1902          if (empty($instance->name)) {
1903              $enrol = $this->get_name();
1904              return get_string('pluginname', 'enrol_'.$enrol);
1905          } else {
1906              $context = context_course::instance($instance->courseid);
1907              return format_string($instance->name, true, array('context'=>$context));
1908          }
1909      }
1910  
1911      /**
1912       * Returns optional enrolment information icons.
1913       *
1914       * This is used in course list for quick overview of enrolment options.
1915       *
1916       * We are not using single instance parameter because sometimes
1917       * we might want to prevent icon repetition when multiple instances
1918       * of one type exist. One instance may also produce several icons.
1919       *
1920       * @param array $instances all enrol instances of this type in one course
1921       * @return array of pix_icon
1922       */
1923      public function get_info_icons(array $instances) {
1924          return array();
1925      }
1926  
1927      /**
1928       * Returns optional enrolment instance description text.
1929       *
1930       * This is used in detailed course information.
1931       *
1932       *
1933       * @param object $instance
1934       * @return string short html text
1935       */
1936      public function get_description_text($instance) {
1937          return null;
1938      }
1939  
1940      /**
1941       * Makes sure config is loaded and cached.
1942       * @return void
1943       */
1944      protected function load_config() {
1945          if (!isset($this->config)) {
1946              $name = $this->get_name();
1947              $this->config = get_config("enrol_$name");
1948          }
1949      }
1950  
1951      /**
1952       * Returns plugin config value
1953       * @param  string $name
1954       * @param  string $default value if config does not exist yet
1955       * @return string value or default
1956       */
1957      public function get_config($name, $default = NULL) {
1958          $this->load_config();
1959          return isset($this->config->$name) ? $this->config->$name : $default;
1960      }
1961  
1962      /**
1963       * Sets plugin config value
1964       * @param  string $name name of config
1965       * @param  string $value string config value, null means delete
1966       * @return string value
1967       */
1968      public function set_config($name, $value) {
1969          $pluginname = $this->get_name();
1970          $this->load_config();
1971          if ($value === NULL) {
1972              unset($this->config->$name);
1973          } else {
1974              $this->config->$name = $value;
1975          }
1976          set_config($name, $value, "enrol_$pluginname");
1977      }
1978  
1979      /**
1980       * Does this plugin assign protected roles are can they be manually removed?
1981       * @return bool - false means anybody may tweak roles, it does not use itemid and component when assigning roles
1982       */
1983      public function roles_protected() {
1984          return true;
1985      }
1986  
1987      /**
1988       * Does this plugin allow manual enrolments?
1989       *
1990       * @param stdClass $instance course enrol instance
1991       * All plugins allowing this must implement 'enrol/xxx:enrol' capability
1992       *
1993       * @return bool - true means user with 'enrol/xxx:enrol' may enrol others freely, false means nobody may add more enrolments manually
1994       */
1995      public function allow_enrol(stdClass $instance) {
1996          return false;
1997      }
1998  
1999      /**
2000       * Does this plugin allow manual unenrolment of all users?
2001       * All plugins allowing this must implement 'enrol/xxx:unenrol' capability
2002       *
2003       * @param stdClass $instance course enrol instance
2004       * @return bool - true means user with 'enrol/xxx:unenrol' may unenrol others freely, false means nobody may touch user_enrolments
2005       */
2006      public function allow_unenrol(stdClass $instance) {
2007          return false;
2008      }
2009  
2010      /**
2011       * Does this plugin allow manual unenrolment of a specific user?
2012       * All plugins allowing this must implement 'enrol/xxx:unenrol' capability
2013       *
2014       * This is useful especially for synchronisation plugins that
2015       * do suspend instead of full unenrolment.
2016       *
2017       * @param stdClass $instance course enrol instance
2018       * @param stdClass $ue record from user_enrolments table, specifies user
2019       *
2020       * @return bool - true means user with 'enrol/xxx:unenrol' may unenrol this user, false means nobody may touch this user enrolment
2021       */
2022      public function allow_unenrol_user(stdClass $instance, stdClass $ue) {
2023          return $this->allow_unenrol($instance);
2024      }
2025  
2026      /**
2027       * Does this plugin allow manual changes in user_enrolments table?
2028       *
2029       * All plugins allowing this must implement 'enrol/xxx:manage' capability
2030       *
2031       * @param stdClass $instance course enrol instance
2032       * @return bool - true means it is possible to change enrol period and status in user_enrolments table
2033       */
2034      public function allow_manage(stdClass $instance) {
2035          return false;
2036      }
2037  
2038      /**
2039       * Does this plugin support some way to user to self enrol?
2040       *
2041       * @param stdClass $instance course enrol instance
2042       *
2043       * @return bool - true means show "Enrol me in this course" link in course UI
2044       */
2045      public function show_enrolme_link(stdClass $instance) {
2046          return false;
2047      }
2048  
2049      /**
2050       * Does this plugin support some way to self enrol?
2051       * This function doesn't check user capabilities. Use can_self_enrol to check capabilities.
2052       *
2053       * @param stdClass $instance enrolment instance
2054       * @return bool - true means "Enrol me in this course" link could be available.
2055       */
2056      public function is_self_enrol_available(stdClass $instance) {
2057          return false;
2058      }
2059  
2060      /**
2061       * Attempt to automatically enrol current user in course without any interaction,
2062       * calling code has to make sure the plugin and instance are active.
2063       *
2064       * This should return either a timestamp in the future or false.
2065       *
2066       * @param stdClass $instance course enrol instance
2067       * @return bool|int false means not enrolled, integer means timeend
2068       */
2069      public function try_autoenrol(stdClass $instance) {
2070          global $USER;
2071  
2072          return false;
2073      }
2074  
2075      /**
2076       * Attempt to automatically gain temporary guest access to course,
2077       * calling code has to make sure the plugin and instance are active.
2078       *
2079       * This should return either a timestamp in the future or false.
2080       *
2081       * @param stdClass $instance course enrol instance
2082       * @return bool|int false means no guest access, integer means timeend
2083       */
2084      public function try_guestaccess(stdClass $instance) {
2085          global $USER;
2086  
2087          return false;
2088      }
2089  
2090      /**
2091       * Enrol user into course via enrol instance.
2092       *
2093       * @param stdClass $instance
2094       * @param int $userid
2095       * @param int $roleid optional role id
2096       * @param int $timestart 0 means unknown
2097       * @param int $timeend 0 means forever
2098       * @param int $status default to ENROL_USER_ACTIVE for new enrolments, no change by default in updates
2099       * @param bool $recovergrades restore grade history
2100       * @return void
2101       */
2102      public function enrol_user(stdClass $instance, $userid, $roleid = null, $timestart = 0, $timeend = 0, $status = null, $recovergrades = null) {
2103          global $DB, $USER, $CFG; // CFG necessary!!!
2104  
2105          if ($instance->courseid == SITEID) {
2106              throw new coding_exception('invalid attempt to enrol into frontpage course!');
2107          }
2108  
2109          $name = $this->get_name();
2110          $courseid = $instance->courseid;
2111  
2112          if ($instance->enrol !== $name) {
2113              throw new coding_exception('invalid enrol instance!');
2114          }
2115          $context = context_course::instance($instance->courseid, MUST_EXIST);
2116          if (!isset($recovergrades)) {
2117              $recovergrades = $CFG->recovergradesdefault;
2118          }
2119  
2120          $inserted = false;
2121          $updated  = false;
2122          if ($ue = $DB->get_record('user_enrolments', array('enrolid'=>$instance->id, 'userid'=>$userid))) {
2123              //only update if timestart or timeend or status are different.
2124              if ($ue->timestart != $timestart or $ue->timeend != $timeend or (!is_null($status) and $ue->status != $status)) {
2125                  $this->update_user_enrol($instance, $userid, $status, $timestart, $timeend);
2126              }
2127          } else {
2128              $ue = new stdClass();
2129              $ue->enrolid      = $instance->id;
2130              $ue->status       = is_null($status) ? ENROL_USER_ACTIVE : $status;
2131              $ue->userid       = $userid;
2132              $ue->timestart    = $timestart;
2133              $ue->timeend      = $timeend;
2134              $ue->modifierid   = $USER->id;
2135              $ue->timecreated  = time();
2136              $ue->timemodified = $ue->timecreated;
2137              $ue->id = $DB->insert_record('user_enrolments', $ue);
2138  
2139              $inserted = true;
2140          }
2141  
2142          if ($inserted) {
2143              // Trigger event.
2144              $event = \core\event\user_enrolment_created::create(
2145                      array(
2146                          'objectid' => $ue->id,
2147                          'courseid' => $courseid,
2148                          'context' => $context,
2149                          'relateduserid' => $ue->userid,
2150                          'other' => array('enrol' => $name)
2151                          )
2152                      );
2153              $event->trigger();
2154              // Check if course contacts cache needs to be cleared.
2155              core_course_category::user_enrolment_changed($courseid, $ue->userid,
2156                      $ue->status, $ue->timestart, $ue->timeend);
2157          }
2158  
2159          if ($roleid) {
2160              // this must be done after the enrolment event so that the role_assigned event is triggered afterwards
2161              if ($this->roles_protected()) {
2162                  role_assign($roleid, $userid, $context->id, 'enrol_'.$name, $instance->id);
2163              } else {
2164                  role_assign($roleid, $userid, $context->id);
2165              }
2166          }
2167  
2168          // Recover old grades if present.
2169          if ($recovergrades) {
2170              require_once("$CFG->libdir/gradelib.php");
2171              grade_recover_history_grades($userid, $courseid);
2172          }
2173  
2174          // Add users to a communication room.
2175          if (core_communication\api::is_available()) {
2176              $communication = \core_communication\api::load_by_instance(
2177                  context: $context,
2178                  component: 'core_course',
2179                  instancetype: 'coursecommunication',
2180                  instanceid: $courseid,
2181              );
2182              $communication->add_members_to_room([$userid]);
2183          }
2184  
2185          // reset current user enrolment caching
2186          if ($userid == $USER->id) {
2187              if (isset($USER->enrol['enrolled'][$courseid])) {
2188                  unset($USER->enrol['enrolled'][$courseid]);
2189              }
2190              if (isset($USER->enrol['tempguest'][$courseid])) {
2191                  unset($USER->enrol['tempguest'][$courseid]);
2192                  remove_temp_course_roles($context);
2193              }
2194          }
2195      }
2196  
2197      /**
2198       * Store user_enrolments changes and trigger event.
2199       *
2200       * @param stdClass $instance
2201       * @param int $userid
2202       * @param int $status
2203       * @param int $timestart
2204       * @param int $timeend
2205       * @return void
2206       */
2207      public function update_user_enrol(stdClass $instance, $userid, $status = NULL, $timestart = NULL, $timeend = NULL) {
2208          global $DB, $USER, $CFG;
2209  
2210          $name = $this->get_name();
2211  
2212          if ($instance->enrol !== $name) {
2213              throw new coding_exception('invalid enrol instance!');
2214          }
2215  
2216          if (!$ue = $DB->get_record('user_enrolments', array('enrolid'=>$instance->id, 'userid'=>$userid))) {
2217              // weird, user not enrolled
2218              return;
2219          }
2220  
2221          $modified = false;
2222          $statusmodified = false;
2223          $timeendmodified = false;
2224          if (isset($status) and $ue->status != $status) {
2225              $ue->status = $status;
2226              $modified = true;
2227              $statusmodified = true;
2228          }
2229          if (isset($timestart) and $ue->timestart != $timestart) {
2230              $ue->timestart = $timestart;
2231              $modified = true;
2232          }
2233          if (isset($timeend) and $ue->timeend != $timeend) {
2234              $ue->timeend = $timeend;
2235              $modified = true;
2236              $timeendmodified = true;
2237          }
2238  
2239          if (!$modified) {
2240              // no change
2241              return;
2242          }
2243  
2244          // Add/remove users to/from communication room.
2245          if (core_communication\api::is_available()) {
2246              $course = enrol_get_course_by_user_enrolment_id($ue->id);
2247              $context = \core\context\course::instance($course->id);
2248              $communication = \core_communication\api::load_by_instance(
2249                  context: $context,
2250                  component: 'core_course',
2251                  instancetype: 'coursecommunication',
2252                  instanceid: $course->id,
2253              );
2254              if (($statusmodified && ((int) $ue->status === 1)) ||
2255                      ($timeendmodified && $ue->timeend !== 0 && (time() > $ue->timeend))) {
2256                  $communication->remove_members_from_room([$userid]);
2257              } else {
2258                  $communication->add_members_to_room([$userid]);
2259              }
2260          }
2261  
2262          $ue->modifierid = $USER->id;
2263          $ue->timemodified = time();
2264          $DB->update_record('user_enrolments', $ue);
2265  
2266          // User enrolments have changed, so mark user as dirty.
2267          mark_user_dirty($userid);
2268  
2269          // Invalidate core_access cache for get_suspended_userids.
2270          cache_helper::invalidate_by_definition('core', 'suspended_userids', array(), array($instance->courseid));
2271  
2272          // Trigger event.
2273          $event = \core\event\user_enrolment_updated::create(
2274                  array(
2275                      'objectid' => $ue->id,
2276                      'courseid' => $instance->courseid,
2277                      'context' => context_course::instance($instance->courseid),
2278                      'relateduserid' => $ue->userid,
2279                      'other' => array('enrol' => $name)
2280                      )
2281                  );
2282          $event->trigger();
2283  
2284          core_course_category::user_enrolment_changed($instance->courseid, $ue->userid,
2285                  $ue->status, $ue->timestart, $ue->timeend);
2286      }
2287  
2288      /**
2289       * Unenrol user from course,
2290       * the last unenrolment removes all remaining roles.
2291       *
2292       * @param stdClass $instance
2293       * @param int $userid
2294       * @return void
2295       */
2296      public function unenrol_user(stdClass $instance, $userid) {
2297          global $CFG, $USER, $DB;
2298          require_once("$CFG->dirroot/group/lib.php");
2299  
2300          $name = $this->get_name();
2301          $courseid = $instance->courseid;
2302  
2303          if ($instance->enrol !== $name) {
2304              throw new coding_exception('invalid enrol instance!');
2305          }
2306          $context = context_course::instance($instance->courseid, MUST_EXIST);
2307  
2308          if (!$ue = $DB->get_record('user_enrolments', array('enrolid'=>$instance->id, 'userid'=>$userid))) {
2309              // weird, user not enrolled
2310              return;
2311          }
2312  
2313          // Remove all users groups linked to this enrolment instance.
2314          if ($gms = $DB->get_records('groups_members', array('userid'=>$userid, 'component'=>'enrol_'.$name, 'itemid'=>$instance->id))) {
2315              foreach ($gms as $gm) {
2316                  groups_remove_member($gm->groupid, $gm->userid);
2317              }
2318          }
2319  
2320          role_unassign_all(array('userid'=>$userid, 'contextid'=>$context->id, 'component'=>'enrol_'.$name, 'itemid'=>$instance->id));
2321          $DB->delete_records('user_enrolments', array('id'=>$ue->id));
2322  
2323          // add extra info and trigger event
2324          $ue->courseid  = $courseid;
2325          $ue->enrol     = $name;
2326  
2327          $sql = "SELECT 'x'
2328                    FROM {user_enrolments} ue
2329                    JOIN {enrol} e ON (e.id = ue.enrolid)
2330                   WHERE ue.userid = :userid AND e.courseid = :courseid";
2331          if ($DB->record_exists_sql($sql, array('userid'=>$userid, 'courseid'=>$courseid))) {
2332              $ue->lastenrol = false;
2333  
2334          } else {
2335              // the big cleanup IS necessary!
2336              require_once("$CFG->libdir/gradelib.php");
2337  
2338              // remove all remaining roles
2339              role_unassign_all(array('userid'=>$userid, 'contextid'=>$context->id), true, false);
2340  
2341              //clean up ALL invisible user data from course if this is the last enrolment - groups, grades, etc.
2342              groups_delete_group_members($courseid, $userid);
2343  
2344              grade_user_unenrol($courseid, $userid);
2345  
2346              $DB->delete_records('user_lastaccess', array('userid'=>$userid, 'courseid'=>$courseid));
2347  
2348              $ue->lastenrol = true; // means user not enrolled any more
2349          }
2350          // Trigger event.
2351          $event = \core\event\user_enrolment_deleted::create(
2352                  array(
2353                      'courseid' => $courseid,
2354                      'context' => $context,
2355                      'relateduserid' => $ue->userid,
2356                      'objectid' => $ue->id,
2357                      'other' => array(
2358                          'userenrolment' => (array)$ue,
2359                          'enrol' => $name
2360                          )
2361                      )
2362                  );
2363          $event->trigger();
2364  
2365          // Remove users from a communication room.
2366          if (core_communication\api::is_available()) {
2367              $communication = \core_communication\api::load_by_instance(
2368                  context: $context,
2369                  component: 'core_course',
2370                  instancetype: 'coursecommunication',
2371                  instanceid: $courseid,
2372              );
2373              $communication->remove_members_from_room([$userid]);
2374          }
2375  
2376          // User enrolments have changed, so mark user as dirty.
2377          mark_user_dirty($userid);
2378  
2379          // Check if courrse contacts cache needs to be cleared.
2380          core_course_category::user_enrolment_changed($courseid, $ue->userid, ENROL_USER_SUSPENDED);
2381  
2382          // reset current user enrolment caching
2383          if ($userid == $USER->id) {
2384              if (isset($USER->enrol['enrolled'][$courseid])) {
2385                  unset($USER->enrol['enrolled'][$courseid]);
2386              }
2387              if (isset($USER->enrol['tempguest'][$courseid])) {
2388                  unset($USER->enrol['tempguest'][$courseid]);
2389                  remove_temp_course_roles($context);
2390              }
2391          }
2392      }
2393  
2394      /**
2395       * Forces synchronisation of user enrolments.
2396       *
2397       * This is important especially for external enrol plugins,
2398       * this function is called for all enabled enrol plugins
2399       * right after every user login.
2400       *
2401       * @param object $user user record
2402       * @return void
2403       */
2404      public function sync_user_enrolments($user) {
2405          // override if necessary
2406      }
2407  
2408      /**
2409       * This returns false for backwards compatibility, but it is really recommended.
2410       *
2411       * @since Moodle 3.1
2412       * @return boolean
2413       */
2414      public function use_standard_editing_ui() {
2415          return false;
2416      }
2417  
2418      /**
2419       * Return whether or not, given the current state, it is possible to add a new instance
2420       * of this enrolment plugin to the course.
2421       *
2422       * Default implementation is just for backwards compatibility.
2423       *
2424       * @param int $courseid
2425       * @return boolean
2426       */
2427      public function can_add_instance($courseid) {
2428          $link = $this->get_newinstance_link($courseid);
2429          return !empty($link);
2430      }
2431  
2432      /**
2433       * Return whether or not, given the current state, it is possible to edit an instance
2434       * of this enrolment plugin in the course. Used by the standard editing UI
2435       * to generate a link to the edit instance form if editing is allowed.
2436       *
2437       * @param stdClass $instance
2438       * @return boolean
2439       */
2440      public function can_edit_instance($instance) {
2441          $context = context_course::instance($instance->courseid);
2442  
2443          return has_capability('enrol/' . $instance->enrol . ':config', $context);
2444      }
2445  
2446      /**
2447       * Returns link to page which may be used to add new instance of enrolment plugin in course.
2448       * @param int $courseid
2449       * @return moodle_url page url
2450       */
2451      public function get_newinstance_link($courseid) {
2452          // override for most plugins, check if instance already exists in cases only one instance is supported
2453          return NULL;
2454      }
2455  
2456      /**
2457       * @deprecated since Moodle 2.8 MDL-35864 - please use can_delete_instance() instead.
2458       */
2459      public function instance_deleteable($instance) {
2460          throw new coding_exception('Function enrol_plugin::instance_deleteable() is deprecated, use
2461                  enrol_plugin::can_delete_instance() instead');
2462      }
2463  
2464      /**
2465       * Is it possible to delete enrol instance via standard UI?
2466       *
2467       * @param stdClass  $instance
2468       * @return bool
2469       */
2470      public function can_delete_instance($instance) {
2471          return false;
2472      }
2473  
2474      /**
2475       * Is it possible to hide/show enrol instance via standard UI?
2476       *
2477       * @param stdClass $instance
2478       * @return bool
2479       */
2480      public function can_hide_show_instance($instance) {
2481          debugging("The enrolment plugin '".$this->get_name()."' should override the function can_hide_show_instance().", DEBUG_DEVELOPER);
2482          return true;
2483      }
2484  
2485      /**
2486       * Returns link to manual enrol UI if exists.
2487       * Does the access control tests automatically.
2488       *
2489       * @param object $instance
2490       * @return moodle_url
2491       */
2492      public function get_manual_enrol_link($instance) {
2493          return NULL;
2494      }
2495  
2496      /**
2497       * Returns list of unenrol links for all enrol instances in course.
2498       *
2499       * @param stdClass $instance
2500       * @return moodle_url or NULL if self unenrolment not supported
2501       */
2502      public function get_unenrolself_link($instance) {
2503          global $USER, $CFG, $DB;
2504  
2505          $name = $this->get_name();
2506          if ($instance->enrol !== $name) {
2507              throw new coding_exception('invalid enrol instance!');
2508          }
2509  
2510          if ($instance->courseid == SITEID) {
2511              return NULL;
2512          }
2513  
2514          if (!enrol_is_enabled($name)) {
2515              return NULL;
2516          }
2517  
2518          if ($instance->status != ENROL_INSTANCE_ENABLED) {
2519              return NULL;
2520          }
2521  
2522          if (!file_exists("$CFG->dirroot/enrol/$name/unenrolself.php")) {
2523              return NULL;
2524          }
2525  
2526          $context = context_course::instance($instance->courseid, MUST_EXIST);
2527  
2528          if (!has_capability("enrol/$name:unenrolself", $context)) {
2529              return NULL;
2530          }
2531  
2532          if (!$DB->record_exists('user_enrolments', array('enrolid'=>$instance->id, 'userid'=>$USER->id, 'status'=>ENROL_USER_ACTIVE))) {
2533              return NULL;
2534          }
2535  
2536          return new moodle_url("/enrol/$name/unenrolself.php", array('enrolid'=>$instance->id));
2537      }
2538  
2539      /**
2540       * Adds enrol instance UI to course edit form
2541       *
2542       * @param object $instance enrol instance or null if does not exist yet
2543       * @param MoodleQuickForm $mform
2544       * @param object $data
2545       * @param object $context context of existing course or parent category if course does not exist
2546       * @return void
2547       */
2548      public function course_edit_form($instance, MoodleQuickForm $mform, $data, $context) {
2549          // override - usually at least enable/disable switch, has to add own form header
2550      }
2551  
2552      /**
2553       * Adds form elements to add/edit instance form.
2554       *
2555       * @since Moodle 3.1
2556       * @param object $instance enrol instance or null if does not exist yet
2557       * @param MoodleQuickForm $mform
2558       * @param context $context
2559       * @return void
2560       */
2561      public function edit_instance_form($instance, MoodleQuickForm $mform, $context) {
2562          // Do nothing by default.
2563      }
2564  
2565      /**
2566       * Perform custom validation of the data used to edit the instance.
2567       *
2568       * @since Moodle 3.1
2569       * @param array $data array of ("fieldname"=>value) of submitted data
2570       * @param array $files array of uploaded files "element_name"=>tmp_file_path
2571       * @param object $instance The instance data loaded from the DB.
2572       * @param context $context The context of the instance we are editing
2573       * @return array of "element_name"=>"error_description" if there are errors,
2574       *         or an empty array if everything is OK.
2575       */
2576      public function edit_instance_validation($data, $files, $instance, $context) {
2577          // No errors by default.
2578          debugging('enrol_plugin::edit_instance_validation() is missing. This plugin has no validation!', DEBUG_DEVELOPER);
2579          return array();
2580      }
2581  
2582      /**
2583       * Validates course edit form data
2584       *
2585       * @param object $instance enrol instance or null if does not exist yet
2586       * @param array $data
2587       * @param object $context context of existing course or parent category if course does not exist
2588       * @return array errors array
2589       */
2590      public function course_edit_validation($instance, array $data, $context) {
2591          return array();
2592      }
2593  
2594      /**
2595       * Called after updating/inserting course.
2596       *
2597       * @param bool $inserted true if course just inserted
2598       * @param object $course
2599       * @param object $data form data
2600       * @return void
2601       */
2602      public function course_updated($inserted, $course, $data) {
2603          if ($inserted) {
2604              if ($this->get_config('defaultenrol')) {
2605                  $this->add_default_instance($course);
2606              }
2607          }
2608      }
2609  
2610      /**
2611       * Add new instance of enrol plugin.
2612       * @param object $course
2613       * @param array instance fields
2614       * @return int id of new instance, null if can not be created
2615       */
2616      public function add_instance($course, array $fields = NULL) {
2617          global $DB;
2618  
2619          if ($course->id == SITEID) {
2620              throw new coding_exception('Invalid request to add enrol instance to frontpage.');
2621          }
2622  
2623          $instance = new stdClass();
2624          $instance->enrol          = $this->get_name();
2625          $instance->status         = ENROL_INSTANCE_ENABLED;
2626          $instance->courseid       = $course->id;
2627          $instance->enrolstartdate = 0;
2628          $instance->enrolenddate   = 0;
2629          $instance->timemodified   = time();
2630          $instance->timecreated    = $instance->timemodified;
2631          $instance->sortorder      = $DB->get_field('enrol', 'COALESCE(MAX(sortorder), -1) + 1', array('courseid'=>$course->id));
2632  
2633          $fields = (array)$fields;
2634          unset($fields['enrol']);
2635          unset($fields['courseid']);
2636          unset($fields['sortorder']);
2637          foreach($fields as $field=>$value) {
2638              $instance->$field = $value;
2639          }
2640  
2641          $instance->id = $DB->insert_record('enrol', $instance);
2642  
2643          \core\event\enrol_instance_created::create_from_record($instance)->trigger();
2644  
2645          return $instance->id;
2646      }
2647  
2648      /**
2649       * Update instance of enrol plugin.
2650       *
2651       * @since Moodle 3.1
2652       * @param stdClass $instance
2653       * @param stdClass $data modified instance fields
2654       * @return boolean
2655       */
2656      public function update_instance($instance, $data) {
2657          global $DB;
2658          $properties = array('status', 'name', 'password', 'customint1', 'customint2', 'customint3',
2659                              'customint4', 'customint5', 'customint6', 'customint7', 'customint8',
2660                              'customchar1', 'customchar2', 'customchar3', 'customdec1', 'customdec2',
2661                              'customtext1', 'customtext2', 'customtext3', 'customtext4', 'roleid',
2662                              'enrolperiod', 'expirynotify', 'notifyall', 'expirythreshold',
2663                              'enrolstartdate', 'enrolenddate', 'cost', 'currency');
2664  
2665          foreach ($properties as $key) {
2666              if (isset($data->$key)) {
2667                  $instance->$key = $data->$key;
2668              }
2669          }
2670          $instance->timemodified = time();
2671  
2672          $update = $DB->update_record('enrol', $instance);
2673          if ($update) {
2674              \core\event\enrol_instance_updated::create_from_record($instance)->trigger();
2675          }
2676          return $update;
2677      }
2678  
2679      /**
2680       * Add new instance of enrol plugin with default settings,
2681       * called when adding new instance manually or when adding new course.
2682       *
2683       * Not all plugins support this.
2684       *
2685       * @param object $course
2686       * @return int id of new instance or null if no default supported
2687       */
2688      public function add_default_instance($course) {
2689          return null;
2690      }
2691  
2692      /**
2693       * Add new instance of enrol plugin with custom settings,
2694       * called when adding new instance manually or when adding new course.
2695       * Used for example on course upload.
2696       *
2697       * Not all plugins support this.
2698       *
2699       * @param stdClass $course Course object
2700       * @param array|null $fields instance fields
2701       * @return int|null id of new instance or null if not supported
2702       */
2703      public function add_custom_instance(stdClass $course, ?array $fields = null): ?int {
2704          return null;
2705      }
2706  
2707      /**
2708       * Check if enrolment plugin is supported in csv course upload.
2709       *
2710       * @return bool
2711       */
2712      public function is_csv_upload_supported(): bool {
2713          return false;
2714      }
2715  
2716      /**
2717       * Update instance status
2718       *
2719       * Override when plugin needs to do some action when enabled or disabled.
2720       *
2721       * @param stdClass $instance
2722       * @param int $newstatus ENROL_INSTANCE_ENABLED, ENROL_INSTANCE_DISABLED
2723       * @return void
2724       */
2725      public function update_status($instance, $newstatus) {
2726          global $DB;
2727  
2728          $instance->status = $newstatus;
2729          $DB->update_record('enrol', $instance);
2730  
2731          $context = context_course::instance($instance->courseid);
2732          \core\event\enrol_instance_updated::create_from_record($instance)->trigger();
2733  
2734          // Invalidate all enrol caches.
2735          $context->mark_dirty();
2736      }
2737  
2738      /**
2739       * Update instance members.
2740       *
2741       * Update communication room membership for an instance action being performed.
2742       *
2743       * @param int $instanceid ID of the enrolment instance
2744       * @param string $action The update action being performed
2745       * @param int $courseid The id of the course
2746       * @return void
2747       */
2748      public function update_communication(int $instanceid, string $action, int $courseid): void {
2749          global $DB;
2750          // Get enrolled instance users.
2751          $instanceusers = $DB->get_records('user_enrolments', ['enrolid' => $instanceid, 'status' => 0]);
2752          $enrolledusers = [];
2753  
2754          foreach ($instanceusers as $user) {
2755              $enrolledusers[] = $user->userid;
2756          }
2757  
2758          $communication = \core_communication\api::load_by_instance(
2759              context: \core\context\course::instance($courseid),
2760              component: 'core_course',
2761              instancetype: 'coursecommunication',
2762              instanceid: $courseid,
2763          );
2764  
2765          switch ($action) {
2766              case 'add':
2767                  $communication->add_members_to_room($enrolledusers);
2768                  break;
2769  
2770              case 'remove':
2771                  $communication->remove_members_from_room($enrolledusers);
2772                  break;
2773              default:
2774                  throw new \coding_exception('Invalid action');
2775          }
2776      }
2777  
2778      /**
2779       * Delete course enrol plugin instance, unenrol all users.
2780       * @param object $instance
2781       * @return void
2782       */
2783      public function delete_instance($instance) {
2784          global $DB;
2785  
2786          $name = $this->get_name();
2787          if ($instance->enrol !== $name) {
2788              throw new coding_exception('invalid enrol instance!');
2789          }
2790  
2791          //first unenrol all users
2792          $participants = $DB->get_recordset('user_enrolments', array('enrolid'=>$instance->id));
2793          foreach ($participants as $participant) {
2794              $this->unenrol_user($instance, $participant->userid);
2795          }
2796          $participants->close();
2797  
2798          // now clean up all remainders that were not removed correctly
2799          if ($gms = $DB->get_records('groups_members', array('itemid' => $instance->id, 'component' => 'enrol_' . $name))) {
2800              foreach ($gms as $gm) {
2801                  groups_remove_member($gm->groupid, $gm->userid);
2802              }
2803          }
2804          $DB->delete_records('role_assignments', array('itemid'=>$instance->id, 'component'=>'enrol_'.$name));
2805          $DB->delete_records('user_enrolments', array('enrolid'=>$instance->id));
2806  
2807          // finally drop the enrol row
2808          $DB->delete_records('enrol', array('id'=>$instance->id));
2809  
2810          $context = context_course::instance($instance->courseid);
2811          \core\event\enrol_instance_deleted::create_from_record($instance)->trigger();
2812  
2813          // Invalidate all enrol caches.
2814          $context->mark_dirty();
2815      }
2816  
2817      /**
2818       * Creates course enrol form, checks if form submitted
2819       * and enrols user if necessary. It can also redirect.
2820       *
2821       * @param stdClass $instance
2822       * @return string html text, usually a form in a text box
2823       */
2824      public function enrol_page_hook(stdClass $instance) {
2825          return null;
2826      }
2827  
2828      /**
2829       * Checks if user can self enrol.
2830       *
2831       * @param stdClass $instance enrolment instance
2832       * @param bool $checkuserenrolment if true will check if user enrolment is inactive.
2833       *             used by navigation to improve performance.
2834       * @return bool|string true if successful, else error message or false
2835       */
2836      public function can_self_enrol(stdClass $instance, $checkuserenrolment = true) {
2837          return false;
2838      }
2839  
2840      /**
2841       * Return information for enrolment instance containing list of parameters required
2842       * for enrolment, name of enrolment plugin etc.
2843       *
2844       * @param stdClass $instance enrolment instance
2845       * @return stdClass|null instance info.
2846       */
2847      public function get_enrol_info(stdClass $instance) {
2848          return null;
2849      }
2850  
2851      /**
2852       * Adds navigation links into course admin block.
2853       *
2854       * By defaults looks for manage links only.
2855       *
2856       * @param navigation_node $instancesnode
2857       * @param stdClass $instance
2858       * @return void
2859       */
2860      public function add_course_navigation($instancesnode, stdClass $instance) {
2861          if ($this->use_standard_editing_ui()) {
2862              $context = context_course::instance($instance->courseid);
2863              $cap = 'enrol/' . $instance->enrol . ':config';
2864              if (has_capability($cap, $context)) {
2865                  $linkparams = array('courseid' => $instance->courseid, 'id' => $instance->id, 'type' => $instance->enrol);
2866                  $managelink = new moodle_url('/enrol/editinstance.php', $linkparams);
2867                  $instancesnode->add($this->get_instance_name($instance), $managelink, navigation_node::TYPE_SETTING);
2868              }
2869          }
2870      }
2871  
2872      /**
2873       * Returns edit icons for the page with list of instances
2874       * @param stdClass $instance
2875       * @return array
2876       */
2877      public function get_action_icons(stdClass $instance) {
2878          global $OUTPUT;
2879  
2880          $icons = array();
2881          if ($this->use_standard_editing_ui()) {
2882              $context = context_course::instance($instance->courseid);
2883              $cap = 'enrol/' . $instance->enrol . ':config';
2884              if (has_capability($cap, $context)) {
2885                  $linkparams = array('courseid' => $instance->courseid, 'id' => $instance->id, 'type' => $instance->enrol);
2886                  $editlink = new moodle_url("/enrol/editinstance.php", $linkparams);
2887                  $icons[] = $OUTPUT->action_icon($editlink, new pix_icon('t/edit', get_string('edit'), 'core',
2888                      array('class' => 'iconsmall')));
2889              }
2890          }
2891          return $icons;
2892      }
2893  
2894      /**
2895       * Reads version.php and determines if it is necessary
2896       * to execute the cron job now.
2897       * @return bool
2898       */
2899      public function is_cron_required() {
2900          global $CFG;
2901  
2902          $name = $this->get_name();
2903          $versionfile = "$CFG->dirroot/enrol/$name/version.php";
2904          $plugin = new stdClass();
2905          include($versionfile);
2906          if (empty($plugin->cron)) {
2907              return false;
2908          }
2909          $lastexecuted = $this->get_config('lastcron', 0);
2910          if ($lastexecuted + $plugin->cron < time()) {
2911              return true;
2912          } else {
2913              return false;
2914          }
2915      }
2916  
2917      /**
2918       * Called for all enabled enrol plugins that returned true from is_cron_required().
2919       * @return void
2920       */
2921      public function cron() {
2922      }
2923  
2924      /**
2925       * Called when user is about to be deleted
2926       * @param object $user
2927       * @return void
2928       */
2929      public function user_delete($user) {
2930          global $DB;
2931  
2932          $sql = "SELECT e.*
2933                    FROM {enrol} e
2934                    JOIN {user_enrolments} ue ON (ue.enrolid = e.id)
2935                   WHERE e.enrol = :name AND ue.userid = :userid";
2936          $params = array('name'=>$this->get_name(), 'userid'=>$user->id);
2937  
2938          $rs = $DB->get_recordset_sql($sql, $params);
2939          foreach($rs as $instance) {
2940              $this->unenrol_user($instance, $user->id);
2941          }
2942          $rs->close();
2943      }
2944  
2945      /**
2946       * Returns an enrol_user_button that takes the user to a page where they are able to
2947       * enrol users into the managers course through this plugin.
2948       *
2949       * Optional: If the plugin supports manual enrolments it can choose to override this
2950       * otherwise it shouldn't
2951       *
2952       * @param course_enrolment_manager $manager
2953       * @return enrol_user_button|false
2954       */
2955      public function get_manual_enrol_button(course_enrolment_manager $manager) {
2956          return false;
2957      }
2958  
2959      /**
2960       * Gets an array of the user enrolment actions
2961       *
2962       * @param course_enrolment_manager $manager
2963       * @param stdClass $ue
2964       * @return array An array of user_enrolment_actions
2965       */
2966      public function get_user_enrolment_actions(course_enrolment_manager $manager, $ue) {
2967          $actions = [];
2968          $context = $manager->get_context();
2969          $instance = $ue->enrolmentinstance;
2970          $params = $manager->get_moodlepage()->url->params();
2971          $params['ue'] = $ue->id;
2972  
2973          // Edit enrolment action.
2974          if ($this->allow_manage($instance) && has_capability("enrol/{$instance->enrol}:manage", $context)) {
2975              $title = get_string('editenrolment', 'enrol');
2976              $icon = new pix_icon('t/edit', $title);
2977              $url = new moodle_url('/enrol/editenrolment.php', $params);
2978              $actionparams = [
2979                  'class' => 'editenrollink',
2980                  'rel' => $ue->id,
2981                  'data-action' => ENROL_ACTION_EDIT
2982              ];
2983              $actions[] = new user_enrolment_action($icon, $title, $url, $actionparams);
2984          }
2985  
2986          // Unenrol action.
2987          if ($this->allow_unenrol_user($instance, $ue) && has_capability("enrol/{$instance->enrol}:unenrol", $context)) {
2988              $title = get_string('unenrol', 'enrol');
2989              $icon = new pix_icon('t/delete', $title);
2990              $url = new moodle_url('/enrol/unenroluser.php', $params);
2991              $actionparams = [
2992                  'class' => 'unenrollink',
2993                  'rel' => $ue->id,
2994                  'data-action' => ENROL_ACTION_UNENROL
2995              ];
2996              $actions[] = new user_enrolment_action($icon, $title, $url, $actionparams);
2997          }
2998          return $actions;
2999      }
3000  
3001      /**
3002       * Returns true if the plugin has one or more bulk operations that can be performed on
3003       * user enrolments.
3004       *
3005       * @param course_enrolment_manager $manager
3006       * @return bool
3007       */
3008      public function has_bulk_operations(course_enrolment_manager $manager) {
3009         return false;
3010      }
3011  
3012      /**
3013       * Return an array of enrol_bulk_enrolment_operation objects that define
3014       * the bulk actions that can be performed on user enrolments by the plugin.
3015       *
3016       * @param course_enrolment_manager $manager
3017       * @return array
3018       */
3019      public function get_bulk_operations(course_enrolment_manager $manager) {
3020          return array();
3021      }
3022  
3023      /**
3024       * Do any enrolments need expiration processing.
3025       *
3026       * Plugins that want to call this functionality must implement 'expiredaction' config setting.
3027       *
3028       * @param progress_trace $trace
3029       * @param int $courseid one course, empty mean all
3030       * @return bool true if any data processed, false if not
3031       */
3032      public function process_expirations(progress_trace $trace, $courseid = null) {
3033          global $DB;
3034  
3035          $name = $this->get_name();
3036          if (!enrol_is_enabled($name)) {
3037              $trace->finished();
3038              return false;
3039          }
3040  
3041          $processed = false;
3042          $params = array();
3043          $coursesql = "";
3044          if ($courseid) {
3045              $coursesql = "AND e.courseid = :courseid";
3046          }
3047  
3048          // Deal with expired accounts.
3049          $action = $this->get_config('expiredaction', ENROL_EXT_REMOVED_KEEP);
3050  
3051          if ($action == ENROL_EXT_REMOVED_UNENROL) {
3052              $instances = array();
3053              $sql = "SELECT ue.*, e.courseid, c.id AS contextid
3054                        FROM {user_enrolments} ue
3055                        JOIN {enrol} e ON (e.id = ue.enrolid AND e.enrol = :enrol)
3056                        JOIN {context} c ON (c.instanceid = e.courseid AND c.contextlevel = :courselevel)
3057                       WHERE ue.timeend > 0 AND ue.timeend < :now $coursesql";
3058              $params = array('now'=>time(), 'courselevel'=>CONTEXT_COURSE, 'enrol'=>$name, 'courseid'=>$courseid);
3059  
3060              $rs = $DB->get_recordset_sql($sql, $params);
3061              foreach ($rs as $ue) {
3062                  if (!$processed) {
3063                      $trace->output("Starting processing of enrol_$name expirations...");
3064                      $processed = true;
3065                  }
3066                  if (empty($instances[$ue->enrolid])) {
3067                      $instances[$ue->enrolid] = $DB->get_record('enrol', array('id'=>$ue->enrolid));
3068                  }
3069                  $instance = $instances[$ue->enrolid];
3070                  if (!$this->roles_protected()) {
3071                      // Let's just guess what extra roles are supposed to be removed.
3072                      if ($instance->roleid) {
3073                          role_unassign($instance->roleid, $ue->userid, $ue->contextid);
3074                      }
3075                  }
3076                  // The unenrol cleans up all subcontexts if this is the only course enrolment for this user.
3077                  $this->unenrol_user($instance, $ue->userid);
3078                  $trace->output("Unenrolling expired user $ue->userid from course $instance->courseid", 1);
3079              }
3080              $rs->close();
3081              unset($instances);
3082  
3083          } else if ($action == ENROL_EXT_REMOVED_SUSPENDNOROLES or $action == ENROL_EXT_REMOVED_SUSPEND) {
3084              $instances = array();
3085              $sql = "SELECT ue.*, e.courseid, c.id AS contextid
3086                        FROM {user_enrolments} ue
3087                        JOIN {enrol} e ON (e.id = ue.enrolid AND e.enrol = :enrol)
3088                        JOIN {context} c ON (c.instanceid = e.courseid AND c.contextlevel = :courselevel)
3089                       WHERE ue.timeend > 0 AND ue.timeend < :now
3090                             AND ue.status = :useractive $coursesql";
3091              $params = array('now'=>time(), 'courselevel'=>CONTEXT_COURSE, 'useractive'=>ENROL_USER_ACTIVE, 'enrol'=>$name, 'courseid'=>$courseid);
3092              $rs = $DB->get_recordset_sql($sql, $params);
3093              foreach ($rs as $ue) {
3094                  if (!$processed) {
3095                      $trace->output("Starting processing of enrol_$name expirations...");
3096                      $processed = true;
3097                  }
3098                  if (empty($instances[$ue->enrolid])) {
3099                      $instances[$ue->enrolid] = $DB->get_record('enrol', array('id'=>$ue->enrolid));
3100                  }
3101                  $instance = $instances[$ue->enrolid];
3102  
3103                  if ($action == ENROL_EXT_REMOVED_SUSPENDNOROLES) {
3104                      if (!$this->roles_protected()) {
3105                          // Let's just guess what roles should be removed.
3106                          $count = $DB->count_records('role_assignments', array('userid'=>$ue->userid, 'contextid'=>$ue->contextid));
3107                          if ($count == 1) {
3108                              role_unassign_all(array('userid'=>$ue->userid, 'contextid'=>$ue->contextid, 'component'=>'', 'itemid'=>0));
3109  
3110                          } else if ($count > 1 and $instance->roleid) {
3111                              role_unassign($instance->roleid, $ue->userid, $ue->contextid, '', 0);
3112                          }
3113                      }
3114                      // In any case remove all roles that belong to this instance and user.
3115                      role_unassign_all(array('userid'=>$ue->userid, 'contextid'=>$ue->contextid, 'component'=>'enrol_'.$name, 'itemid'=>$instance->id), true);
3116                      // Final cleanup of subcontexts if there are no more course roles.
3117                      if (0 == $DB->count_records('role_assignments', array('userid'=>$ue->userid, 'contextid'=>$ue->contextid))) {
3118                          role_unassign_all(array('userid'=>$ue->userid, 'contextid'=>$ue->contextid, 'component'=>'', 'itemid'=>0), true);
3119                      }
3120                  }
3121  
3122                  $this->update_user_enrol($instance, $ue->userid, ENROL_USER_SUSPENDED);
3123                  $trace->output("Suspending expired user $ue->userid in course $instance->courseid", 1);
3124              }
3125              $rs->close();
3126              unset($instances);
3127  
3128          } else {
3129              // ENROL_EXT_REMOVED_KEEP means no changes.
3130          }
3131  
3132          if ($processed) {
3133              $trace->output("...finished processing of enrol_$name expirations");
3134          } else {
3135              $trace->output("No expired enrol_$name enrolments detected");
3136          }
3137          $trace->finished();
3138  
3139          return $processed;
3140      }
3141  
3142      /**
3143       * Send expiry notifications.
3144       *
3145       * Plugin that wants to have expiry notification MUST implement following:
3146       * - expirynotifyhour plugin setting,
3147       * - configuration options in instance edit form (expirynotify, notifyall and expirythreshold),
3148       * - notification strings (expirymessageenrollersubject, expirymessageenrollerbody,
3149       *   expirymessageenrolledsubject and expirymessageenrolledbody),
3150       * - expiry_notification provider in db/messages.php,
3151       * - upgrade code that sets default thresholds for existing courses (should be 1 day),
3152       * - something that calls this method, such as cron.
3153       *
3154       * @param progress_trace $trace (accepts bool for backwards compatibility only)
3155       */
3156      public function send_expiry_notifications($trace) {
3157          global $DB, $CFG;
3158  
3159          $name = $this->get_name();
3160          if (!enrol_is_enabled($name)) {
3161              $trace->finished();
3162              return;
3163          }
3164  
3165          // Unfortunately this may take a long time, it should not be interrupted,
3166          // otherwise users get duplicate notification.
3167  
3168          core_php_time_limit::raise();
3169          raise_memory_limit(MEMORY_HUGE);
3170  
3171  
3172          $expirynotifylast = $this->get_config('expirynotifylast', 0);
3173          $expirynotifyhour = $this->get_config('expirynotifyhour');
3174          if (is_null($expirynotifyhour)) {
3175              debugging("send_expiry_notifications() in $name enrolment plugin needs expirynotifyhour setting");
3176              $trace->finished();
3177              return;
3178          }
3179  
3180          if (!($trace instanceof progress_trace)) {
3181              $trace = $trace ? new text_progress_trace() : new null_progress_trace();
3182              debugging('enrol_plugin::send_expiry_notifications() now expects progress_trace instance as parameter!', DEBUG_DEVELOPER);
3183          }
3184  
3185          $timenow = time();
3186          $notifytime = usergetmidnight($timenow, $CFG->timezone) + ($expirynotifyhour * 3600);
3187  
3188          if ($expirynotifylast > $notifytime) {
3189              $trace->output($name.' enrolment expiry notifications were already sent today at '.userdate($expirynotifylast, '', $CFG->timezone).'.');
3190              $trace->finished();
3191              return;
3192  
3193          } else if ($timenow < $notifytime) {
3194              $trace->output($name.' enrolment expiry notifications will be sent at '.userdate($notifytime, '', $CFG->timezone).'.');
3195              $trace->finished();
3196              return;
3197          }
3198  
3199          $trace->output('Processing '.$name.' enrolment expiration notifications...');
3200  
3201          // Notify users responsible for enrolment once every day.
3202          $sql = "SELECT ue.*, e.expirynotify, e.notifyall, e.expirythreshold, e.courseid, c.fullname
3203                    FROM {user_enrolments} ue
3204                    JOIN {enrol} e ON (e.id = ue.enrolid AND e.enrol = :name AND e.expirynotify > 0 AND e.status = :enabled)
3205                    JOIN {course} c ON (c.id = e.courseid)
3206                    JOIN {user} u ON (u.id = ue.userid AND u.deleted = 0 AND u.suspended = 0)
3207                   WHERE ue.status = :active AND ue.timeend > 0 AND ue.timeend > :now1 AND ue.timeend < (e.expirythreshold + :now2)
3208                ORDER BY ue.enrolid ASC, u.lastname ASC, u.firstname ASC, u.id ASC";
3209          $params = array('enabled'=>ENROL_INSTANCE_ENABLED, 'active'=>ENROL_USER_ACTIVE, 'now1'=>$timenow, 'now2'=>$timenow, 'name'=>$name);
3210  
3211          $rs = $DB->get_recordset_sql($sql, $params);
3212  
3213          $lastenrollid = 0;
3214          $users = array();
3215  
3216          foreach($rs as $ue) {
3217              if ($lastenrollid and $lastenrollid != $ue->enrolid) {
3218                  $this->notify_expiry_enroller($lastenrollid, $users, $trace);
3219                  $users = array();
3220              }
3221              $lastenrollid = $ue->enrolid;
3222  
3223              $enroller = $this->get_enroller($ue->enrolid);
3224              $context = context_course::instance($ue->courseid);
3225  
3226              $user = $DB->get_record('user', array('id'=>$ue->userid));
3227  
3228              $users[] = array('fullname'=>fullname($user, has_capability('moodle/site:viewfullnames', $context, $enroller)), 'timeend'=>$ue->timeend);
3229  
3230              if (!$ue->notifyall) {
3231                  continue;
3232              }
3233  
3234              if ($ue->timeend - $ue->expirythreshold + 86400 < $timenow) {
3235                  // Notify enrolled users only once at the start of the threshold.
3236                  $trace->output("user $ue->userid was already notified that enrolment in course $ue->courseid expires on ".userdate($ue->timeend, '', $CFG->timezone), 1);
3237                  continue;
3238              }
3239  
3240              $this->notify_expiry_enrolled($user, $ue, $trace);
3241          }
3242          $rs->close();
3243  
3244          if ($lastenrollid and $users) {
3245              $this->notify_expiry_enroller($lastenrollid, $users, $trace);
3246          }
3247  
3248          $trace->output('...notification processing finished.');
3249          $trace->finished();
3250  
3251          $this->set_config('expirynotifylast', $timenow);
3252      }
3253  
3254      /**
3255       * Returns the user who is responsible for enrolments for given instance.
3256       *
3257       * Override if plugin knows anybody better than admin.
3258       *
3259       * @param int $instanceid enrolment instance id
3260       * @return stdClass user record
3261       */
3262      protected function get_enroller($instanceid) {
3263          return get_admin();
3264      }
3265  
3266      /**
3267       * Notify user about incoming expiration of their enrolment,
3268       * it is called only if notification of enrolled users (aka students) is enabled in course.
3269       *
3270       * This is executed only once for each expiring enrolment right
3271       * at the start of the expiration threshold.
3272       *
3273       * @param stdClass $user
3274       * @param stdClass $ue
3275       * @param progress_trace $trace
3276       */
3277      protected function notify_expiry_enrolled($user, $ue, progress_trace $trace) {
3278          global $CFG;
3279  
3280          $name = $this->get_name();
3281  
3282          $oldforcelang = force_current_language($user->lang);
3283  
3284          $enroller = $this->get_enroller($ue->enrolid);
3285          $context = context_course::instance($ue->courseid);
3286  
3287          $a = new stdClass();
3288          $a->course   = format_string($ue->fullname, true, array('context'=>$context));
3289          $a->user     = fullname($user, true);
3290          $a->timeend  = userdate($ue->timeend, '', $user->timezone);
3291          $a->enroller = fullname($enroller, has_capability('moodle/site:viewfullnames', $context, $user));
3292  
3293          $subject = get_string('expirymessageenrolledsubject', 'enrol_'.$name, $a);
3294          $body = get_string('expirymessageenrolledbody', 'enrol_'.$name, $a);
3295  
3296          $message = new \core\message\message();
3297          $message->courseid          = $ue->courseid;
3298          $message->notification      = 1;
3299          $message->component         = 'enrol_'.$name;
3300          $message->name              = 'expiry_notification';
3301          $message->userfrom          = $enroller;
3302          $message->userto            = $user;
3303          $message->subject           = $subject;
3304          $message->fullmessage       = $body;
3305          $message->fullmessageformat = FORMAT_MARKDOWN;
3306          $message->fullmessagehtml   = markdown_to_html($body);
3307          $message->smallmessage      = $subject;
3308          $message->contexturlname    = $a->course;
3309          $message->contexturl        = (string)new moodle_url('/course/view.php', array('id'=>$ue->courseid));
3310  
3311          if (message_send($message)) {
3312              $trace->output("notifying user $ue->userid that enrolment in course $ue->courseid expires on ".userdate($ue->timeend, '', $CFG->timezone), 1);
3313          } else {
3314              $trace->output("error notifying user $ue->userid that enrolment in course $ue->courseid expires on ".userdate($ue->timeend, '', $CFG->timezone), 1);
3315          }
3316  
3317          force_current_language($oldforcelang);
3318      }
3319  
3320      /**
3321       * Notify person responsible for enrolments that some user enrolments will be expired soon,
3322       * it is called only if notification of enrollers (aka teachers) is enabled in course.
3323       *
3324       * This is called repeatedly every day for each course if there are any pending expiration
3325       * in the expiration threshold.
3326       *
3327       * @param int $eid
3328       * @param array $users
3329       * @param progress_trace $trace
3330       */
3331      protected function notify_expiry_enroller($eid, $users, progress_trace $trace) {
3332          global $DB;
3333  
3334          $name = $this->get_name();
3335  
3336          $instance = $DB->get_record('enrol', array('id'=>$eid, 'enrol'=>$name));
3337          $context = context_course::instance($instance->courseid);
3338          $course = $DB->get_record('course', array('id'=>$instance->courseid));
3339  
3340          $enroller = $this->get_enroller($instance->id);
3341          $admin = get_admin();
3342  
3343          $oldforcelang = force_current_language($enroller->lang);
3344  
3345          foreach($users as $key=>$info) {
3346              $users[$key] = '* '.$info['fullname'].' - '.userdate($info['timeend'], '', $enroller->timezone);
3347          }
3348  
3349          $a = new stdClass();
3350          $a->course    = format_string($course->fullname, true, array('context'=>$context));
3351          $a->threshold = get_string('numdays', '', $instance->expirythreshold / (60*60*24));
3352          $a->users     = implode("\n", $users);
3353          $a->extendurl = (string)new moodle_url('/user/index.php', array('id'=>$instance->courseid));
3354  
3355          $subject = get_string('expirymessageenrollersubject', 'enrol_'.$name, $a);
3356          $body = get_string('expirymessageenrollerbody', 'enrol_'.$name, $a);
3357  
3358          $message = new \core\message\message();
3359          $message->courseid          = $course->id;
3360          $message->notification      = 1;
3361          $message->component         = 'enrol_'.$name;
3362          $message->name              = 'expiry_notification';
3363          $message->userfrom          = $admin;
3364          $message->userto            = $enroller;
3365          $message->subject           = $subject;
3366          $message->fullmessage       = $body;
3367          $message->fullmessageformat = FORMAT_MARKDOWN;
3368          $message->fullmessagehtml   = markdown_to_html($body);
3369          $message->smallmessage      = $subject;
3370          $message->contexturlname    = $a->course;
3371          $message->contexturl        = $a->extendurl;
3372  
3373          if (message_send($message)) {
3374              $trace->output("notifying user $enroller->id about all expiring $name enrolments in course $instance->courseid", 1);
3375          } else {
3376              $trace->output("error notifying user $enroller->id about all expiring $name enrolments in course $instance->courseid", 1);
3377          }
3378  
3379          force_current_language($oldforcelang);
3380      }
3381  
3382      /**
3383       * Backup execution step hook to annotate custom fields.
3384       *
3385       * @param backup_enrolments_execution_step $step
3386       * @param stdClass $enrol
3387       */
3388      public function backup_annotate_custom_fields(backup_enrolments_execution_step $step, stdClass $enrol) {
3389          // Override as necessary to annotate custom fields in the enrol table.
3390      }
3391  
3392      /**
3393       * Automatic enrol sync executed during restore.
3394       * Useful for automatic sync by course->idnumber or course category.
3395       * @param stdClass $course course record
3396       */
3397      public function restore_sync_course($course) {
3398          // Override if necessary.
3399      }
3400  
3401      /**
3402       * Restore instance and map settings.
3403       *
3404       * @param restore_enrolments_structure_step $step
3405       * @param stdClass $data
3406       * @param stdClass $course
3407       * @param int $oldid
3408       */
3409      public function restore_instance(restore_enrolments_structure_step $step, stdClass $data, $course, $oldid) {
3410          // Do not call this from overridden methods, restore and set new id there.
3411          $step->set_mapping('enrol', $oldid, 0);
3412      }
3413  
3414      /**
3415       * Restore user enrolment.
3416       *
3417       * @param restore_enrolments_structure_step $step
3418       * @param stdClass $data
3419       * @param stdClass $instance
3420       * @param int $oldinstancestatus
3421       * @param int $userid
3422       */
3423      public function restore_user_enrolment(restore_enrolments_structure_step $step, $data, $instance, $userid, $oldinstancestatus) {
3424          // Override as necessary if plugin supports restore of enrolments.
3425      }
3426  
3427      /**
3428       * Restore role assignment.
3429       *
3430       * @param stdClass $instance
3431       * @param int $roleid
3432       * @param int $userid
3433       * @param int $contextid
3434       */
3435      public function restore_role_assignment($instance, $roleid, $userid, $contextid) {
3436          // No role assignment by default, override if necessary.
3437      }
3438  
3439      /**
3440       * Restore user group membership.
3441       * @param stdClass $instance
3442       * @param int $groupid
3443       * @param int $userid
3444       */
3445      public function restore_group_member($instance, $groupid, $userid) {
3446          // Implement if you want to restore protected group memberships,
3447          // usually this is not necessary because plugins should be able to recreate the memberships automatically.
3448      }
3449  
3450      /**
3451       * Returns defaults for new instances.
3452       * @since Moodle 3.1
3453       * @return array
3454       */
3455      public function get_instance_defaults() {
3456          return array();
3457      }
3458  
3459      /**
3460       * Validate a list of parameter names and types.
3461       * @since Moodle 3.1
3462       *
3463       * @param array $data array of ("fieldname"=>value) of submitted data
3464       * @param array $rules array of ("fieldname"=>PARAM_X types - or "fieldname"=>array( list of valid options )
3465       * @return array of "element_name"=>"error_description" if there are errors,
3466       *         or an empty array if everything is OK.
3467       */
3468      public function validate_param_types($data, $rules) {
3469          $errors = array();
3470          $invalidstr = get_string('invaliddata', 'error');
3471          foreach ($rules as $fieldname => $rule) {
3472              if (is_array($rule)) {
3473                  if (!in_array($data[$fieldname], $rule)) {
3474                      $errors[$fieldname] = $invalidstr;
3475                  }
3476              } else {
3477                  if ($data[$fieldname] != clean_param($data[$fieldname], $rule)) {
3478                      $errors[$fieldname] = $invalidstr;
3479                  }
3480              }
3481          }
3482          return $errors;
3483      }
3484  
3485      /**
3486       * Fill custom fields data for a given enrolment plugin.
3487       *
3488       * @param array $enrolmentdata enrolment data.
3489       * @param int $courseid Course ID.
3490       * @return array Updated enrolment data with custom fields info.
3491       */
3492      public function fill_enrol_custom_fields(array $enrolmentdata, int $courseid) : array {
3493          return $enrolmentdata;
3494      }
3495  
3496      /**
3497       * Check if data is valid for a given enrolment plugin
3498       *
3499       * @param array $enrolmentdata enrolment data to validate.
3500       * @param int|null $courseid Course ID.
3501       * @return array Errors
3502       */
3503      public function validate_enrol_plugin_data(array $enrolmentdata, ?int $courseid = null) : array {
3504          $errors = [];
3505          if (!$this->is_csv_upload_supported()) {
3506              $errors['errorunsupportedmethod'] =
3507                  new lang_string('errorunsupportedmethod', 'tool_uploadcourse',
3508                      get_class($this));
3509  
3510          }
3511          return $errors;
3512      }
3513  
3514      /**
3515       * Check if plugin custom data is allowed in relevant context.
3516       *
3517       * @param array $enrolmentdata enrolment data to validate.
3518       * @param int|null $courseid Course ID.
3519       * @return lang_string|null Error
3520       */
3521      public function validate_plugin_data_context(array $enrolmentdata, ?int $courseid = null) : ?lang_string {
3522          return null;
3523      }
3524  
3525  }