Search moodle.org's
Developer Documentation

See Release Notes
Long Term Support Release

  • Bug fixes for general core bugs in 4.1.x will end 13 November 2023 (12 months).
  • Bug fixes for security issues in 4.1.x will end 10 November 2025 (36 months).
  • PHP version: minimum PHP 7.4.0 Note: minimum PHP version has increased since Moodle 4.0. PHP 8.0.x is supported too.
/lib/ -> enrollib.php (source)

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

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