Search moodle.org's
Developer Documentation

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