Search moodle.org's
Developer Documentation

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

    Differences Between: [Versions 310 and 311] [Versions 311 and 400] [Versions 37 and 311] [Versions 38 and 311] [Versions 39 and 311]

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