Search moodle.org's
Developer Documentation

See Release Notes

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

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

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