Search moodle.org's
Developer Documentation

See Release Notes

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

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

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