Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 3.10.x will end 8 November 2021 (12 months).
  • Bug fixes for security issues in 3.10.x will end 9 May 2022 (18 months).
  • PHP version: minimum PHP 7.2.0 Note: minimum PHP version has increased since Moodle 3.8. PHP 7.3.x and 7.4.x are supported too.
/lib/ -> enrollib.php (source)

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

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