Search moodle.org's
Developer Documentation

See Release Notes
Long Term Support Release

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

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

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