/lib/enrollib.php

https://bitbucket.org/synergylearning/campusconnect · PHP · 2327 lines · 1292 code · 302 blank · 733 comment · 236 complexity · a1b7184a9727ce5936ae2896ad219b47 MD5 · raw file

  1. <?php
  2. // This file is part of Moodle - http://moodle.org/
  3. //
  4. // Moodle is free software: you can redistribute it and/or modify
  5. // it under the terms of the GNU General Public License as published by
  6. // the Free Software Foundation, either version 3 of the License, or
  7. // (at your option) any later version.
  8. //
  9. // Moodle is distributed in the hope that it will be useful,
  10. // but WITHOUT ANY WARRANTY; without even the implied warranty of
  11. // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  12. // GNU General Public License for more details.
  13. //
  14. // You should have received a copy of the GNU General Public License
  15. // along with Moodle. If not, see <http://www.gnu.org/licenses/>.
  16. /**
  17. * This library includes the basic parts of enrol api.
  18. * It is available on each page.
  19. *
  20. * @package core
  21. * @subpackage enrol
  22. * @copyright 2010 Petr Skoda {@link http://skodak.org}
  23. * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  24. */
  25. defined('MOODLE_INTERNAL') || die();
  26. /** Course enrol instance enabled. (used in enrol->status) */
  27. define('ENROL_INSTANCE_ENABLED', 0);
  28. /** Course enrol instance disabled, user may enter course if other enrol instance enabled. (used in enrol->status)*/
  29. define('ENROL_INSTANCE_DISABLED', 1);
  30. /** User is active participant (used in user_enrolments->status)*/
  31. define('ENROL_USER_ACTIVE', 0);
  32. /** User participation in course is suspended (used in user_enrolments->status) */
  33. define('ENROL_USER_SUSPENDED', 1);
  34. /** @deprecated - enrol caching was reworked, use ENROL_MAX_TIMESTAMP instead */
  35. define('ENROL_REQUIRE_LOGIN_CACHE_PERIOD', 1800);
  36. /** The timestamp indicating forever */
  37. define('ENROL_MAX_TIMESTAMP', 2147483647);
  38. /** When user disappears from external source, the enrolment is completely removed */
  39. define('ENROL_EXT_REMOVED_UNENROL', 0);
  40. /** When user disappears from external source, the enrolment is kept as is - one way sync */
  41. define('ENROL_EXT_REMOVED_KEEP', 1);
  42. /** @deprecated since 2.4 not used any more, migrate plugin to new restore methods */
  43. define('ENROL_RESTORE_TYPE', 'enrolrestore');
  44. /**
  45. * When user disappears from external source, user enrolment is suspended, roles are kept as is.
  46. * In some cases user needs a role with some capability to be visible in UI - suc has in gradebook,
  47. * assignments, etc.
  48. */
  49. define('ENROL_EXT_REMOVED_SUSPEND', 2);
  50. /**
  51. * When user disappears from external source, the enrolment is suspended and roles assigned
  52. * by enrol instance are removed. Please note that user may "disappear" from gradebook and other areas.
  53. * */
  54. define('ENROL_EXT_REMOVED_SUSPENDNOROLES', 3);
  55. /**
  56. * Returns instances of enrol plugins
  57. * @param bool $enabled return enabled only
  58. * @return array of enrol plugins name=>instance
  59. */
  60. function enrol_get_plugins($enabled) {
  61. global $CFG;
  62. $result = array();
  63. if ($enabled) {
  64. // sorted by enabled plugin order
  65. $enabled = explode(',', $CFG->enrol_plugins_enabled);
  66. $plugins = array();
  67. foreach ($enabled as $plugin) {
  68. $plugins[$plugin] = "$CFG->dirroot/enrol/$plugin";
  69. }
  70. } else {
  71. // sorted alphabetically
  72. $plugins = core_component::get_plugin_list('enrol');
  73. ksort($plugins);
  74. }
  75. foreach ($plugins as $plugin=>$location) {
  76. if (!file_exists("$location/lib.php")) {
  77. continue;
  78. }
  79. include_once("$location/lib.php");
  80. $class = "enrol_{$plugin}_plugin";
  81. if (!class_exists($class)) {
  82. continue;
  83. }
  84. $result[$plugin] = new $class();
  85. }
  86. return $result;
  87. }
  88. /**
  89. * Returns instance of enrol plugin
  90. * @param string $name name of enrol plugin ('manual', 'guest', ...)
  91. * @return enrol_plugin
  92. */
  93. function enrol_get_plugin($name) {
  94. global $CFG;
  95. $name = clean_param($name, PARAM_PLUGIN);
  96. if (empty($name)) {
  97. // ignore malformed or missing plugin names completely
  98. return null;
  99. }
  100. $location = "$CFG->dirroot/enrol/$name";
  101. if (!file_exists("$location/lib.php")) {
  102. return null;
  103. }
  104. include_once("$location/lib.php");
  105. $class = "enrol_{$name}_plugin";
  106. if (!class_exists($class)) {
  107. return null;
  108. }
  109. return new $class();
  110. }
  111. /**
  112. * Returns enrolment instances in given course.
  113. * @param int $courseid
  114. * @param bool $enabled
  115. * @return array of enrol instances
  116. */
  117. function enrol_get_instances($courseid, $enabled) {
  118. global $DB, $CFG;
  119. if (!$enabled) {
  120. return $DB->get_records('enrol', array('courseid'=>$courseid), 'sortorder,id');
  121. }
  122. $result = $DB->get_records('enrol', array('courseid'=>$courseid, 'status'=>ENROL_INSTANCE_ENABLED), 'sortorder,id');
  123. $enabled = explode(',', $CFG->enrol_plugins_enabled);
  124. foreach ($result as $key=>$instance) {
  125. if (!in_array($instance->enrol, $enabled)) {
  126. unset($result[$key]);
  127. continue;
  128. }
  129. if (!file_exists("$CFG->dirroot/enrol/$instance->enrol/lib.php")) {
  130. // broken plugin
  131. unset($result[$key]);
  132. continue;
  133. }
  134. }
  135. return $result;
  136. }
  137. /**
  138. * Checks if a given plugin is in the list of enabled enrolment plugins.
  139. *
  140. * @param string $enrol Enrolment plugin name
  141. * @return boolean Whether the plugin is enabled
  142. */
  143. function enrol_is_enabled($enrol) {
  144. global $CFG;
  145. if (empty($CFG->enrol_plugins_enabled)) {
  146. return false;
  147. }
  148. return in_array($enrol, explode(',', $CFG->enrol_plugins_enabled));
  149. }
  150. /**
  151. * Check all the login enrolment information for the given user object
  152. * by querying the enrolment plugins
  153. *
  154. * This function may be very slow, use only once after log-in or login-as.
  155. *
  156. * @param stdClass $user
  157. * @return void
  158. */
  159. function enrol_check_plugins($user) {
  160. global $CFG;
  161. if (empty($user->id) or isguestuser($user)) {
  162. // shortcut - there is no enrolment work for guests and not-logged-in users
  163. return;
  164. }
  165. // originally there was a broken admin test, but accidentally it was non-functional in 2.2,
  166. // which proved it was actually not necessary.
  167. static $inprogress = array(); // To prevent this function being called more than once in an invocation
  168. if (!empty($inprogress[$user->id])) {
  169. return;
  170. }
  171. $inprogress[$user->id] = true; // Set the flag
  172. $enabled = enrol_get_plugins(true);
  173. foreach($enabled as $enrol) {
  174. $enrol->sync_user_enrolments($user);
  175. }
  176. unset($inprogress[$user->id]); // Unset the flag
  177. }
  178. /**
  179. * Do these two students share any course?
  180. *
  181. * The courses has to be visible and enrolments has to be active,
  182. * timestart and timeend restrictions are ignored.
  183. *
  184. * This function calls {@see enrol_get_shared_courses()} setting checkexistsonly
  185. * to true.
  186. *
  187. * @param stdClass|int $user1
  188. * @param stdClass|int $user2
  189. * @return bool
  190. */
  191. function enrol_sharing_course($user1, $user2) {
  192. return enrol_get_shared_courses($user1, $user2, false, true);
  193. }
  194. /**
  195. * Returns any courses shared by the two users
  196. *
  197. * The courses has to be visible and enrolments has to be active,
  198. * timestart and timeend restrictions are ignored.
  199. *
  200. * @global moodle_database $DB
  201. * @param stdClass|int $user1
  202. * @param stdClass|int $user2
  203. * @param bool $preloadcontexts If set to true contexts for the returned courses
  204. * will be preloaded.
  205. * @param bool $checkexistsonly If set to true then this function will return true
  206. * if the users share any courses and false if not.
  207. * @return array|bool An array of courses that both users are enrolled in OR if
  208. * $checkexistsonly set returns true if the users share any courses
  209. * and false if not.
  210. */
  211. function enrol_get_shared_courses($user1, $user2, $preloadcontexts = false, $checkexistsonly = false) {
  212. global $DB, $CFG;
  213. $user1 = isset($user1->id) ? $user1->id : $user1;
  214. $user2 = isset($user2->id) ? $user2->id : $user2;
  215. if (empty($user1) or empty($user2)) {
  216. return false;
  217. }
  218. if (!$plugins = explode(',', $CFG->enrol_plugins_enabled)) {
  219. return false;
  220. }
  221. list($plugins, $params) = $DB->get_in_or_equal($plugins, SQL_PARAMS_NAMED, 'ee');
  222. $params['enabled'] = ENROL_INSTANCE_ENABLED;
  223. $params['active1'] = ENROL_USER_ACTIVE;
  224. $params['active2'] = ENROL_USER_ACTIVE;
  225. $params['user1'] = $user1;
  226. $params['user2'] = $user2;
  227. $ctxselect = '';
  228. $ctxjoin = '';
  229. if ($preloadcontexts) {
  230. $ctxselect = ', ' . context_helper::get_preload_record_columns_sql('ctx');
  231. $ctxjoin = "LEFT JOIN {context} ctx ON (ctx.instanceid = c.id AND ctx.contextlevel = :contextlevel)";
  232. $params['contextlevel'] = CONTEXT_COURSE;
  233. }
  234. $sql = "SELECT c.* $ctxselect
  235. FROM {course} c
  236. JOIN (
  237. SELECT DISTINCT c.id
  238. FROM {enrol} e
  239. JOIN {user_enrolments} ue1 ON (ue1.enrolid = e.id AND ue1.status = :active1 AND ue1.userid = :user1)
  240. JOIN {user_enrolments} ue2 ON (ue2.enrolid = e.id AND ue2.status = :active2 AND ue2.userid = :user2)
  241. JOIN {course} c ON (c.id = e.courseid AND c.visible = 1)
  242. WHERE e.status = :enabled AND e.enrol $plugins
  243. ) ec ON ec.id = c.id
  244. $ctxjoin";
  245. if ($checkexistsonly) {
  246. return $DB->record_exists_sql($sql, $params);
  247. } else {
  248. $courses = $DB->get_records_sql($sql, $params);
  249. if ($preloadcontexts) {
  250. array_map('context_helper::preload_from_record', $courses);
  251. }
  252. return $courses;
  253. }
  254. }
  255. /**
  256. * This function adds necessary enrol plugins UI into the course edit form.
  257. *
  258. * @param MoodleQuickForm $mform
  259. * @param object $data course edit form data
  260. * @param object $context context of existing course or parent category if course does not exist
  261. * @return void
  262. */
  263. function enrol_course_edit_form(MoodleQuickForm $mform, $data, $context) {
  264. $plugins = enrol_get_plugins(true);
  265. if (!empty($data->id)) {
  266. $instances = enrol_get_instances($data->id, false);
  267. foreach ($instances as $instance) {
  268. if (!isset($plugins[$instance->enrol])) {
  269. continue;
  270. }
  271. $plugin = $plugins[$instance->enrol];
  272. $plugin->course_edit_form($instance, $mform, $data, $context);
  273. }
  274. } else {
  275. foreach ($plugins as $plugin) {
  276. $plugin->course_edit_form(NULL, $mform, $data, $context);
  277. }
  278. }
  279. }
  280. /**
  281. * Validate course edit form data
  282. *
  283. * @param array $data raw form data
  284. * @param object $context context of existing course or parent category if course does not exist
  285. * @return array errors array
  286. */
  287. function enrol_course_edit_validation(array $data, $context) {
  288. $errors = array();
  289. $plugins = enrol_get_plugins(true);
  290. if (!empty($data['id'])) {
  291. $instances = enrol_get_instances($data['id'], false);
  292. foreach ($instances as $instance) {
  293. if (!isset($plugins[$instance->enrol])) {
  294. continue;
  295. }
  296. $plugin = $plugins[$instance->enrol];
  297. $errors = array_merge($errors, $plugin->course_edit_validation($instance, $data, $context));
  298. }
  299. } else {
  300. foreach ($plugins as $plugin) {
  301. $errors = array_merge($errors, $plugin->course_edit_validation(NULL, $data, $context));
  302. }
  303. }
  304. return $errors;
  305. }
  306. /**
  307. * Update enrol instances after course edit form submission
  308. * @param bool $inserted true means new course added, false course already existed
  309. * @param object $course
  310. * @param object $data form data
  311. * @return void
  312. */
  313. function enrol_course_updated($inserted, $course, $data) {
  314. global $DB, $CFG;
  315. $plugins = enrol_get_plugins(true);
  316. foreach ($plugins as $plugin) {
  317. $plugin->course_updated($inserted, $course, $data);
  318. }
  319. }
  320. /**
  321. * Add navigation nodes
  322. * @param navigation_node $coursenode
  323. * @param object $course
  324. * @return void
  325. */
  326. function enrol_add_course_navigation(navigation_node $coursenode, $course) {
  327. global $CFG;
  328. $coursecontext = context_course::instance($course->id);
  329. $instances = enrol_get_instances($course->id, true);
  330. $plugins = enrol_get_plugins(true);
  331. // we do not want to break all course pages if there is some borked enrol plugin, right?
  332. foreach ($instances as $k=>$instance) {
  333. if (!isset($plugins[$instance->enrol])) {
  334. unset($instances[$k]);
  335. }
  336. }
  337. $usersnode = $coursenode->add(get_string('users'), null, navigation_node::TYPE_CONTAINER, null, 'users');
  338. if ($course->id != SITEID) {
  339. // list all participants - allows assigning roles, groups, etc.
  340. if (has_capability('moodle/course:enrolreview', $coursecontext)) {
  341. $url = new moodle_url('/enrol/users.php', array('id'=>$course->id));
  342. $usersnode->add(get_string('enrolledusers', 'enrol'), $url, navigation_node::TYPE_SETTING, null, 'review', new pix_icon('i/enrolusers', ''));
  343. }
  344. // manage enrol plugin instances
  345. if (has_capability('moodle/course:enrolconfig', $coursecontext) or has_capability('moodle/course:enrolreview', $coursecontext)) {
  346. $url = new moodle_url('/enrol/instances.php', array('id'=>$course->id));
  347. } else {
  348. $url = NULL;
  349. }
  350. $instancesnode = $usersnode->add(get_string('enrolmentinstances', 'enrol'), $url, navigation_node::TYPE_SETTING, null, 'manageinstances');
  351. // each instance decides how to configure itself or how many other nav items are exposed
  352. foreach ($instances as $instance) {
  353. if (!isset($plugins[$instance->enrol])) {
  354. continue;
  355. }
  356. $plugins[$instance->enrol]->add_course_navigation($instancesnode, $instance);
  357. }
  358. if (!$url) {
  359. $instancesnode->trim_if_empty();
  360. }
  361. }
  362. // Manage groups in this course or even frontpage
  363. if (($course->groupmode || !$course->groupmodeforce) && has_capability('moodle/course:managegroups', $coursecontext)) {
  364. $url = new moodle_url('/group/index.php', array('id'=>$course->id));
  365. $usersnode->add(get_string('groups'), $url, navigation_node::TYPE_SETTING, null, 'groups', new pix_icon('i/group', ''));
  366. }
  367. if (has_any_capability(array( 'moodle/role:assign', 'moodle/role:safeoverride','moodle/role:override', 'moodle/role:review'), $coursecontext)) {
  368. // Override roles
  369. if (has_capability('moodle/role:review', $coursecontext)) {
  370. $url = new moodle_url('/admin/roles/permissions.php', array('contextid'=>$coursecontext->id));
  371. } else {
  372. $url = NULL;
  373. }
  374. $permissionsnode = $usersnode->add(get_string('permissions', 'role'), $url, navigation_node::TYPE_SETTING, null, 'override');
  375. // Add assign or override roles if allowed
  376. if ($course->id == SITEID or (!empty($CFG->adminsassignrolesincourse) and is_siteadmin())) {
  377. if (has_capability('moodle/role:assign', $coursecontext)) {
  378. $url = new moodle_url('/admin/roles/assign.php', array('contextid'=>$coursecontext->id));
  379. $permissionsnode->add(get_string('assignedroles', 'role'), $url, navigation_node::TYPE_SETTING, null, 'roles', new pix_icon('i/assignroles', ''));
  380. }
  381. }
  382. // Check role permissions
  383. if (has_any_capability(array('moodle/role:assign', 'moodle/role:safeoverride','moodle/role:override', 'moodle/role:assign'), $coursecontext)) {
  384. $url = new moodle_url('/admin/roles/check.php', array('contextid'=>$coursecontext->id));
  385. $permissionsnode->add(get_string('checkpermissions', 'role'), $url, navigation_node::TYPE_SETTING, null, 'permissions', new pix_icon('i/checkpermissions', ''));
  386. }
  387. }
  388. // Deal somehow with users that are not enrolled but still got a role somehow
  389. if ($course->id != SITEID) {
  390. //TODO, create some new UI for role assignments at course level
  391. if (has_capability('moodle/role:assign', $coursecontext)) {
  392. $url = new moodle_url('/enrol/otherusers.php', array('id'=>$course->id));
  393. $usersnode->add(get_string('notenrolledusers', 'enrol'), $url, navigation_node::TYPE_SETTING, null, 'otherusers', new pix_icon('i/assignroles', ''));
  394. }
  395. }
  396. // just in case nothing was actually added
  397. $usersnode->trim_if_empty();
  398. if ($course->id != SITEID) {
  399. if (isguestuser() or !isloggedin()) {
  400. // guest account can not be enrolled - no links for them
  401. } else if (is_enrolled($coursecontext)) {
  402. // unenrol link if possible
  403. foreach ($instances as $instance) {
  404. if (!isset($plugins[$instance->enrol])) {
  405. continue;
  406. }
  407. $plugin = $plugins[$instance->enrol];
  408. if ($unenrollink = $plugin->get_unenrolself_link($instance)) {
  409. $shortname = format_string($course->shortname, true, array('context' => $coursecontext));
  410. $coursenode->add(get_string('unenrolme', 'core_enrol', $shortname), $unenrollink, navigation_node::TYPE_SETTING, null, 'unenrolself', new pix_icon('i/user', ''));
  411. break;
  412. //TODO. deal with multiple unenrol links - not likely case, but still...
  413. }
  414. }
  415. } else {
  416. // enrol link if possible
  417. if (is_viewing($coursecontext)) {
  418. // better not show any enrol link, this is intended for managers and inspectors
  419. } else {
  420. foreach ($instances as $instance) {
  421. if (!isset($plugins[$instance->enrol])) {
  422. continue;
  423. }
  424. $plugin = $plugins[$instance->enrol];
  425. if ($plugin->show_enrolme_link($instance)) {
  426. $url = new moodle_url('/enrol/index.php', array('id'=>$course->id));
  427. $shortname = format_string($course->shortname, true, array('context' => $coursecontext));
  428. $coursenode->add(get_string('enrolme', 'core_enrol', $shortname), $url, navigation_node::TYPE_SETTING, null, 'enrolself', new pix_icon('i/user', ''));
  429. break;
  430. }
  431. }
  432. }
  433. }
  434. }
  435. }
  436. /**
  437. * Returns list of courses current $USER is enrolled in and can access
  438. *
  439. * - $fields is an array of field names to ADD
  440. * so name the fields you really need, which will
  441. * be added and uniq'd
  442. *
  443. * @param string|array $fields
  444. * @param string $sort
  445. * @param int $limit max number of courses
  446. * @return array
  447. */
  448. function enrol_get_my_courses($fields = NULL, $sort = 'visible DESC,sortorder ASC', $limit = 0) {
  449. global $DB, $USER;
  450. // Guest account does not have any courses
  451. if (isguestuser() or !isloggedin()) {
  452. return(array());
  453. }
  454. $basefields = array('id', 'category', 'sortorder',
  455. 'shortname', 'fullname', 'idnumber',
  456. 'startdate', 'visible',
  457. 'groupmode', 'groupmodeforce', 'cacherev');
  458. if (empty($fields)) {
  459. $fields = $basefields;
  460. } else if (is_string($fields)) {
  461. // turn the fields from a string to an array
  462. $fields = explode(',', $fields);
  463. $fields = array_map('trim', $fields);
  464. $fields = array_unique(array_merge($basefields, $fields));
  465. } else if (is_array($fields)) {
  466. $fields = array_unique(array_merge($basefields, $fields));
  467. } else {
  468. throw new coding_exception('Invalid $fileds parameter in enrol_get_my_courses()');
  469. }
  470. if (in_array('*', $fields)) {
  471. $fields = array('*');
  472. }
  473. $orderby = "";
  474. $sort = trim($sort);
  475. if (!empty($sort)) {
  476. $rawsorts = explode(',', $sort);
  477. $sorts = array();
  478. foreach ($rawsorts as $rawsort) {
  479. $rawsort = trim($rawsort);
  480. if (strpos($rawsort, 'c.') === 0) {
  481. $rawsort = substr($rawsort, 2);
  482. }
  483. $sorts[] = trim($rawsort);
  484. }
  485. $sort = 'c.'.implode(',c.', $sorts);
  486. $orderby = "ORDER BY $sort";
  487. }
  488. $wheres = array("c.id <> :siteid");
  489. $params = array('siteid'=>SITEID);
  490. if (isset($USER->loginascontext) and $USER->loginascontext->contextlevel == CONTEXT_COURSE) {
  491. // list _only_ this course - anything else is asking for trouble...
  492. $wheres[] = "courseid = :loginas";
  493. $params['loginas'] = $USER->loginascontext->instanceid;
  494. }
  495. $coursefields = 'c.' .join(',c.', $fields);
  496. $ccselect = ', ' . context_helper::get_preload_record_columns_sql('ctx');
  497. $ccjoin = "LEFT JOIN {context} ctx ON (ctx.instanceid = c.id AND ctx.contextlevel = :contextlevel)";
  498. $params['contextlevel'] = CONTEXT_COURSE;
  499. $wheres = implode(" AND ", $wheres);
  500. //note: we can not use DISTINCT + text fields due to Oracle and MS limitations, that is why we have the subselect there
  501. $sql = "SELECT $coursefields $ccselect
  502. FROM {course} c
  503. JOIN (SELECT DISTINCT e.courseid
  504. FROM {enrol} e
  505. JOIN {user_enrolments} ue ON (ue.enrolid = e.id AND ue.userid = :userid)
  506. WHERE ue.status = :active AND e.status = :enabled AND ue.timestart < :now1 AND (ue.timeend = 0 OR ue.timeend > :now2)
  507. ) en ON (en.courseid = c.id)
  508. $ccjoin
  509. WHERE $wheres
  510. $orderby";
  511. $params['userid'] = $USER->id;
  512. $params['active'] = ENROL_USER_ACTIVE;
  513. $params['enabled'] = ENROL_INSTANCE_ENABLED;
  514. $params['now1'] = round(time(), -2); // improves db caching
  515. $params['now2'] = $params['now1'];
  516. $courses = $DB->get_records_sql($sql, $params, 0, $limit);
  517. // preload contexts and check visibility
  518. foreach ($courses as $id=>$course) {
  519. context_helper::preload_from_record($course);
  520. if (!$course->visible) {
  521. if (!$context = context_course::instance($id, IGNORE_MISSING)) {
  522. unset($courses[$id]);
  523. continue;
  524. }
  525. if (!has_capability('moodle/course:viewhiddencourses', $context)) {
  526. unset($courses[$id]);
  527. continue;
  528. }
  529. }
  530. $courses[$id] = $course;
  531. }
  532. //wow! Is that really all? :-D
  533. return $courses;
  534. }
  535. /**
  536. * Returns course enrolment information icons.
  537. *
  538. * @param object $course
  539. * @param array $instances enrol instances of this course, improves performance
  540. * @return array of pix_icon
  541. */
  542. function enrol_get_course_info_icons($course, array $instances = NULL) {
  543. $icons = array();
  544. if (is_null($instances)) {
  545. $instances = enrol_get_instances($course->id, true);
  546. }
  547. $plugins = enrol_get_plugins(true);
  548. foreach ($plugins as $name => $plugin) {
  549. $pis = array();
  550. foreach ($instances as $instance) {
  551. if ($instance->status != ENROL_INSTANCE_ENABLED or $instance->courseid != $course->id) {
  552. debugging('Invalid instances parameter submitted in enrol_get_info_icons()');
  553. continue;
  554. }
  555. if ($instance->enrol == $name) {
  556. $pis[$instance->id] = $instance;
  557. }
  558. }
  559. if ($pis) {
  560. $icons = array_merge($icons, $plugin->get_info_icons($pis));
  561. }
  562. }
  563. return $icons;
  564. }
  565. /**
  566. * Returns course enrolment detailed information.
  567. *
  568. * @param object $course
  569. * @return array of html fragments - can be used to construct lists
  570. */
  571. function enrol_get_course_description_texts($course) {
  572. $lines = array();
  573. $instances = enrol_get_instances($course->id, true);
  574. $plugins = enrol_get_plugins(true);
  575. foreach ($instances as $instance) {
  576. if (!isset($plugins[$instance->enrol])) {
  577. //weird
  578. continue;
  579. }
  580. $plugin = $plugins[$instance->enrol];
  581. $text = $plugin->get_description_text($instance);
  582. if ($text !== NULL) {
  583. $lines[] = $text;
  584. }
  585. }
  586. return $lines;
  587. }
  588. /**
  589. * Returns list of courses user is enrolled into.
  590. * (Note: use enrol_get_all_users_courses if you want to use the list wihtout any cap checks )
  591. *
  592. * - $fields is an array of fieldnames to ADD
  593. * so name the fields you really need, which will
  594. * be added and uniq'd
  595. *
  596. * @param int $userid
  597. * @param bool $onlyactive return only active enrolments in courses user may see
  598. * @param string|array $fields
  599. * @param string $sort
  600. * @return array
  601. */
  602. function enrol_get_users_courses($userid, $onlyactive = false, $fields = NULL, $sort = 'visible DESC,sortorder ASC') {
  603. global $DB;
  604. $courses = enrol_get_all_users_courses($userid, $onlyactive, $fields, $sort);
  605. // preload contexts and check visibility
  606. if ($onlyactive) {
  607. foreach ($courses as $id=>$course) {
  608. context_helper::preload_from_record($course);
  609. if (!$course->visible) {
  610. if (!$context = context_course::instance($id)) {
  611. unset($courses[$id]);
  612. continue;
  613. }
  614. if (!has_capability('moodle/course:viewhiddencourses', $context, $userid)) {
  615. unset($courses[$id]);
  616. continue;
  617. }
  618. }
  619. }
  620. }
  621. return $courses;
  622. }
  623. /**
  624. * Can user access at least one enrolled course?
  625. *
  626. * Cheat if necessary, but find out as fast as possible!
  627. *
  628. * @param int|stdClass $user null means use current user
  629. * @return bool
  630. */
  631. function enrol_user_sees_own_courses($user = null) {
  632. global $USER;
  633. if ($user === null) {
  634. $user = $USER;
  635. }
  636. $userid = is_object($user) ? $user->id : $user;
  637. // Guest account does not have any courses
  638. if (isguestuser($userid) or empty($userid)) {
  639. return false;
  640. }
  641. // Let's cheat here if this is the current user,
  642. // if user accessed any course recently, then most probably
  643. // we do not need to query the database at all.
  644. if ($USER->id == $userid) {
  645. if (!empty($USER->enrol['enrolled'])) {
  646. foreach ($USER->enrol['enrolled'] as $until) {
  647. if ($until > time()) {
  648. return true;
  649. }
  650. }
  651. }
  652. }
  653. // Now the slow way.
  654. $courses = enrol_get_all_users_courses($userid, true);
  655. foreach($courses as $course) {
  656. if ($course->visible) {
  657. return true;
  658. }
  659. context_helper::preload_from_record($course);
  660. $context = context_course::instance($course->id);
  661. if (has_capability('moodle/course:viewhiddencourses', $context, $user)) {
  662. return true;
  663. }
  664. }
  665. return false;
  666. }
  667. /**
  668. * Returns list of courses user is enrolled into without any capability checks
  669. * - $fields is an array of fieldnames to ADD
  670. * so name the fields you really need, which will
  671. * be added and uniq'd
  672. *
  673. * @param int $userid
  674. * @param bool $onlyactive return only active enrolments in courses user may see
  675. * @param string|array $fields
  676. * @param string $sort
  677. * @return array
  678. */
  679. function enrol_get_all_users_courses($userid, $onlyactive = false, $fields = NULL, $sort = 'visible DESC,sortorder ASC') {
  680. global $DB;
  681. // Guest account does not have any courses
  682. if (isguestuser($userid) or empty($userid)) {
  683. return(array());
  684. }
  685. $basefields = array('id', 'category', 'sortorder',
  686. 'shortname', 'fullname', 'idnumber',
  687. 'startdate', 'visible',
  688. 'groupmode', 'groupmodeforce');
  689. if (empty($fields)) {
  690. $fields = $basefields;
  691. } else if (is_string($fields)) {
  692. // turn the fields from a string to an array
  693. $fields = explode(',', $fields);
  694. $fields = array_map('trim', $fields);
  695. $fields = array_unique(array_merge($basefields, $fields));
  696. } else if (is_array($fields)) {
  697. $fields = array_unique(array_merge($basefields, $fields));
  698. } else {
  699. throw new coding_exception('Invalid $fileds parameter in enrol_get_my_courses()');
  700. }
  701. if (in_array('*', $fields)) {
  702. $fields = array('*');
  703. }
  704. $orderby = "";
  705. $sort = trim($sort);
  706. if (!empty($sort)) {
  707. $rawsorts = explode(',', $sort);
  708. $sorts = array();
  709. foreach ($rawsorts as $rawsort) {
  710. $rawsort = trim($rawsort);
  711. if (strpos($rawsort, 'c.') === 0) {
  712. $rawsort = substr($rawsort, 2);
  713. }
  714. $sorts[] = trim($rawsort);
  715. }
  716. $sort = 'c.'.implode(',c.', $sorts);
  717. $orderby = "ORDER BY $sort";
  718. }
  719. $params = array('siteid'=>SITEID);
  720. if ($onlyactive) {
  721. $subwhere = "WHERE ue.status = :active AND e.status = :enabled AND ue.timestart < :now1 AND (ue.timeend = 0 OR ue.timeend > :now2)";
  722. $params['now1'] = round(time(), -2); // improves db caching
  723. $params['now2'] = $params['now1'];
  724. $params['active'] = ENROL_USER_ACTIVE;
  725. $params['enabled'] = ENROL_INSTANCE_ENABLED;
  726. } else {
  727. $subwhere = "";
  728. }
  729. $coursefields = 'c.' .join(',c.', $fields);
  730. $ccselect = ', ' . context_helper::get_preload_record_columns_sql('ctx');
  731. $ccjoin = "LEFT JOIN {context} ctx ON (ctx.instanceid = c.id AND ctx.contextlevel = :contextlevel)";
  732. $params['contextlevel'] = CONTEXT_COURSE;
  733. //note: we can not use DISTINCT + text fields due to Oracle and MS limitations, that is why we have the subselect there
  734. $sql = "SELECT $coursefields $ccselect
  735. FROM {course} c
  736. JOIN (SELECT DISTINCT e.courseid
  737. FROM {enrol} e
  738. JOIN {user_enrolments} ue ON (ue.enrolid = e.id AND ue.userid = :userid)
  739. $subwhere
  740. ) en ON (en.courseid = c.id)
  741. $ccjoin
  742. WHERE c.id <> :siteid
  743. $orderby";
  744. $params['userid'] = $userid;
  745. $courses = $DB->get_records_sql($sql, $params);
  746. return $courses;
  747. }
  748. /**
  749. * Called when user is about to be deleted.
  750. * @param object $user
  751. * @return void
  752. */
  753. function enrol_user_delete($user) {
  754. global $DB;
  755. $plugins = enrol_get_plugins(true);
  756. foreach ($plugins as $plugin) {
  757. $plugin->user_delete($user);
  758. }
  759. // force cleanup of all broken enrolments
  760. $DB->delete_records('user_enrolments', array('userid'=>$user->id));
  761. }
  762. /**
  763. * Called when course is about to be deleted.
  764. * @param stdClass $course
  765. * @return void
  766. */
  767. function enrol_course_delete($course) {
  768. global $DB;
  769. $instances = enrol_get_instances($course->id, false);
  770. $plugins = enrol_get_plugins(true);
  771. foreach ($instances as $instance) {
  772. if (isset($plugins[$instance->enrol])) {
  773. $plugins[$instance->enrol]->delete_instance($instance);
  774. }
  775. // low level delete in case plugin did not do it
  776. $DB->delete_records('user_enrolments', array('enrolid'=>$instance->id));
  777. $DB->delete_records('role_assignments', array('itemid'=>$instance->id, 'component'=>'enrol_'.$instance->enrol));
  778. $DB->delete_records('user_enrolments', array('enrolid'=>$instance->id));
  779. $DB->delete_records('enrol', array('id'=>$instance->id));
  780. }
  781. }
  782. /**
  783. * Try to enrol user via default internal auth plugin.
  784. *
  785. * For now this is always using the manual enrol plugin...
  786. *
  787. * @param $courseid
  788. * @param $userid
  789. * @param $roleid
  790. * @param $timestart
  791. * @param $timeend
  792. * @return bool success
  793. */
  794. function enrol_try_internal_enrol($courseid, $userid, $roleid = null, $timestart = 0, $timeend = 0) {
  795. global $DB;
  796. //note: this is hardcoded to manual plugin for now
  797. if (!enrol_is_enabled('manual')) {
  798. return false;
  799. }
  800. if (!$enrol = enrol_get_plugin('manual')) {
  801. return false;
  802. }
  803. if (!$instances = $DB->get_records('enrol', array('enrol'=>'manual', 'courseid'=>$courseid, 'status'=>ENROL_INSTANCE_ENABLED), 'sortorder,id ASC')) {
  804. return false;
  805. }
  806. $instance = reset($instances);
  807. $enrol->enrol_user($instance, $userid, $roleid, $timestart, $timeend);
  808. return true;
  809. }
  810. /**
  811. * Is there a chance users might self enrol
  812. * @param int $courseid
  813. * @return bool
  814. */
  815. function enrol_selfenrol_available($courseid) {
  816. $result = false;
  817. $plugins = enrol_get_plugins(true);
  818. $enrolinstances = enrol_get_instances($courseid, true);
  819. foreach($enrolinstances as $instance) {
  820. if (!isset($plugins[$instance->enrol])) {
  821. continue;
  822. }
  823. if ($instance->enrol === 'guest') {
  824. // blacklist known temporary guest plugins
  825. continue;
  826. }
  827. if ($plugins[$instance->enrol]->show_enrolme_link($instance)) {
  828. $result = true;
  829. break;
  830. }
  831. }
  832. return $result;
  833. }
  834. /**
  835. * This function returns the end of current active user enrolment.
  836. *
  837. * It deals correctly with multiple overlapping user enrolments.
  838. *
  839. * @param int $courseid
  840. * @param int $userid
  841. * @return int|bool timestamp when active enrolment ends, false means no active enrolment now, 0 means never
  842. */
  843. function enrol_get_enrolment_end($courseid, $userid) {
  844. global $DB;
  845. $sql = "SELECT ue.*
  846. FROM {user_enrolments} ue
  847. JOIN {enrol} e ON (e.id = ue.enrolid AND e.courseid = :courseid)
  848. JOIN {user} u ON u.id = ue.userid
  849. WHERE ue.userid = :userid AND ue.status = :active AND e.status = :enabled AND u.deleted = 0";
  850. $params = array('enabled'=>ENROL_INSTANCE_ENABLED, 'active'=>ENROL_USER_ACTIVE, 'userid'=>$userid, 'courseid'=>$courseid);
  851. if (!$enrolments = $DB->get_records_sql($sql, $params)) {
  852. return false;
  853. }
  854. $changes = array();
  855. foreach ($enrolments as $ue) {
  856. $start = (int)$ue->timestart;
  857. $end = (int)$ue->timeend;
  858. if ($end != 0 and $end < $start) {
  859. debugging('Invalid enrolment start or end in user_enrolment id:'.$ue->id);
  860. continue;
  861. }
  862. if (isset($changes[$start])) {
  863. $changes[$start] = $changes[$start] + 1;
  864. } else {
  865. $changes[$start] = 1;
  866. }
  867. if ($end === 0) {
  868. // no end
  869. } else if (isset($changes[$end])) {
  870. $changes[$end] = $changes[$end] - 1;
  871. } else {
  872. $changes[$end] = -1;
  873. }
  874. }
  875. // let's sort then enrolment starts&ends and go through them chronologically,
  876. // looking for current status and the next future end of enrolment
  877. ksort($changes);
  878. $now = time();
  879. $current = 0;
  880. $present = null;
  881. foreach ($changes as $time => $change) {
  882. if ($time > $now) {
  883. if ($present === null) {
  884. // we have just went past current time
  885. $present = $current;
  886. if ($present < 1) {
  887. // no enrolment active
  888. return false;
  889. }
  890. }
  891. if ($present !== null) {
  892. // we are already in the future - look for possible end
  893. if ($current + $change < 1) {
  894. return $time;
  895. }
  896. }
  897. }
  898. $current += $change;
  899. }
  900. if ($current > 0) {
  901. return 0;
  902. } else {
  903. return false;
  904. }
  905. }
  906. /**
  907. * Is current user accessing course via this enrolment method?
  908. *
  909. * This is intended for operations that are going to affect enrol instances.
  910. *
  911. * @param stdClass $instance enrol instance
  912. * @return bool
  913. */
  914. function enrol_accessing_via_instance(stdClass $instance) {
  915. global $DB, $USER;
  916. if (empty($instance->id)) {
  917. return false;
  918. }
  919. if (is_siteadmin()) {
  920. // Admins may go anywhere.
  921. return false;
  922. }
  923. return $DB->record_exists('user_enrolments', array('userid'=>$USER->id, 'enrolid'=>$instance->id));
  924. }
  925. /**
  926. * All enrol plugins should be based on this class,
  927. * this is also the main source of documentation.
  928. */
  929. abstract class enrol_plugin {
  930. protected $config = null;
  931. /**
  932. * Returns name of this enrol plugin
  933. * @return string
  934. */
  935. public function get_name() {
  936. // second word in class is always enrol name, sorry, no fancy plugin names with _
  937. $words = explode('_', get_class($this));
  938. return $words[1];
  939. }
  940. /**
  941. * Returns localised name of enrol instance
  942. *
  943. * @param object $instance (null is accepted too)
  944. * @return string
  945. */
  946. public function get_instance_name($instance) {
  947. if (empty($instance->name)) {
  948. $enrol = $this->get_name();
  949. return get_string('pluginname', 'enrol_'.$enrol);
  950. } else {
  951. $context = context_course::instance($instance->courseid);
  952. return format_string($instance->name, true, array('context'=>$context));
  953. }
  954. }
  955. /**
  956. * Returns optional enrolment information icons.
  957. *
  958. * This is used in course list for quick overview of enrolment options.
  959. *
  960. * We are not using single instance parameter because sometimes
  961. * we might want to prevent icon repetition when multiple instances
  962. * of one type exist. One instance may also produce several icons.
  963. *
  964. * @param array $instances all enrol instances of this type in one course
  965. * @return array of pix_icon
  966. */
  967. public function get_info_icons(array $instances) {
  968. return array();
  969. }
  970. /**
  971. * Returns optional enrolment instance description text.
  972. *
  973. * This is used in detailed course information.
  974. *
  975. *
  976. * @param object $instance
  977. * @return string short html text
  978. */
  979. public function get_description_text($instance) {
  980. return null;
  981. }
  982. /**
  983. * Makes sure config is loaded and cached.
  984. * @return void
  985. */
  986. protected function load_config() {
  987. if (!isset($this->config)) {
  988. $name = $this->get_name();
  989. $this->config = get_config("enrol_$name");
  990. }
  991. }
  992. /**
  993. * Returns plugin config value
  994. * @param string $name
  995. * @param string $default value if config does not exist yet
  996. * @return string value or default
  997. */
  998. public function get_config($name, $default = NULL) {
  999. $this->load_config();
  1000. return isset($this->config->$name) ? $this->config->$name : $default;
  1001. }
  1002. /**
  1003. * Sets plugin config value
  1004. * @param string $name name of config
  1005. * @param string $value string config value, null means delete
  1006. * @return string value
  1007. */
  1008. public function set_config($name, $value) {
  1009. $pluginname = $this->get_name();
  1010. $this->load_config();
  1011. if ($value === NULL) {
  1012. unset($this->config->$name);
  1013. } else {
  1014. $this->config->$name = $value;
  1015. }
  1016. set_config($name, $value, "enrol_$pluginname");
  1017. }
  1018. /**
  1019. * Does this plugin assign protected roles are can they be manually removed?
  1020. * @return bool - false means anybody may tweak roles, it does not use itemid and component when assigning roles
  1021. */
  1022. public function roles_protected() {
  1023. return true;
  1024. }
  1025. /**
  1026. * Does this plugin allow manual enrolments?
  1027. *
  1028. * @param stdClass $instance course enrol instance
  1029. * All plugins allowing this must implement 'enrol/xxx:enrol' capability
  1030. *
  1031. * @return bool - true means user with 'enrol/xxx:enrol' may enrol others freely, false means nobody may add more enrolments manually
  1032. */
  1033. public function allow_enrol(stdClass $instance) {
  1034. return false;
  1035. }
  1036. /**
  1037. * Does this plugin allow manual unenrolment of all users?
  1038. * All plugins allowing this must implement 'enrol/xxx:unenrol' capability
  1039. *
  1040. * @param stdClass $instance course enrol instance
  1041. * @return bool - true means user with 'enrol/xxx:unenrol' may unenrol others freely, false means nobody may touch user_enrolments
  1042. */
  1043. public function allow_unenrol(stdClass $instance) {
  1044. return false;
  1045. }
  1046. /**
  1047. * Does this plugin allow manual unenrolment of a specific user?
  1048. * All plugins allowing this must implement 'enrol/xxx:unenrol' capability
  1049. *
  1050. * This is useful especially for synchronisation plugins that
  1051. * do suspend instead of full unenrolment.
  1052. *
  1053. * @param stdClass $instance course enrol instance
  1054. * @param stdClass $ue record from user_enrolments table, specifies user
  1055. *
  1056. * @return bool - true means user with 'enrol/xxx:unenrol' may unenrol this user, false means nobody may touch this user enrolment
  1057. */
  1058. public function allow_unenrol_user(stdClass $instance, stdClass $ue) {
  1059. return $this->allow_unenrol($instance);
  1060. }
  1061. /**
  1062. * Does this plugin allow manual changes in user_enrolments table?
  1063. *
  1064. * All plugins allowing this must implement 'enrol/xxx:manage' capability
  1065. *
  1066. * @param stdClass $instance course enrol instance
  1067. * @return bool - true means it is possible to change enrol period and status in user_enrolments table
  1068. */
  1069. public function allow_manage(stdClass $instance) {
  1070. return false;
  1071. }
  1072. /**
  1073. * Does this plugin support some way to user to self enrol?
  1074. *
  1075. * @param stdClass $instance course enrol instance
  1076. *
  1077. * @return bool - true means show "Enrol me in this course" link in course UI
  1078. */
  1079. public function show_enrolme_link(stdClass $instance) {
  1080. return false;
  1081. }
  1082. /**
  1083. * Attempt to automatically enrol current user in course without any interaction,
  1084. * calling code has to make sure the plugin and instance are active.
  1085. *
  1086. * This should return either a timestamp in the future or false.
  1087. *
  1088. * @param stdClass $instance course enrol instance
  1089. * @return bool|int false means not enrolled, integer means timeend
  1090. */
  1091. public function try_autoenrol(stdClass $instance) {
  1092. global $USER;
  1093. return false;
  1094. }
  1095. /**
  1096. * Attempt to automatically gain temporary guest access to course,
  1097. * calling code has to make sure the plugin and instance are active.
  1098. *
  1099. * This should return either a timestamp in the future or false.
  1100. *
  1101. * @param stdClass $instance course enrol instance
  1102. * @return bool|int false means no guest access, integer means timeend
  1103. */
  1104. public function try_guestaccess(stdClass $instance) {
  1105. global $USER;
  1106. return false;
  1107. }
  1108. /**
  1109. * Enrol user into course via enrol instance.
  1110. *
  1111. * @param stdClass $instance
  1112. * @param int $userid
  1113. * @param int $roleid optional role id
  1114. * @param int $timestart 0 means unknown
  1115. * @param int $timeend 0 means forever
  1116. * @param int $status default to ENROL_USER_ACTIVE for new enrolments, no change by default in updates
  1117. * @param bool $recovergrades restore grade history
  1118. * @return void
  1119. */
  1120. public function enrol_user(stdClass $instance, $userid, $roleid = null, $timestart = 0, $timeend = 0, $status = null, $recovergrades = null) {
  1121. global $DB, $USER, $CFG; // CFG necessary!!!
  1122. if ($instance->courseid == SITEID) {
  1123. throw new coding_exception('invalid attempt to enrol into frontpage course!');
  1124. }
  1125. $name = $this->get_name();
  1126. $courseid = $instance->courseid;
  1127. if ($instance->enrol !== $name) {
  1128. throw new coding_exception('invalid enrol instance!');
  1129. }
  1130. $context = context_course::instance($instance->courseid, MUST_EXIST);
  1131. if (!isset($recovergrades)) {
  1132. $recovergrades = $CFG->recovergradesdefault;
  1133. }
  1134. $inserted = false;
  1135. $updated = false;
  1136. if ($ue = $DB->get_record('user_enrolments', array('enrolid'=>$instance->id, 'userid'=>$userid))) {
  1137. //only update if timestart or timeend or status are different.
  1138. if ($ue->timestart != $timestart or $ue->timeend != $timeend or (!is_null($status) and $ue->status != $status)) {
  1139. $this->update_user_enrol($instance, $userid, $status, $timestart, $timeend);
  1140. }
  1141. } else {
  1142. $ue = new stdClass();
  1143. $ue->enrolid = $instance->id;
  1144. $ue->status = is_null($status) ? ENROL_USER_ACTIVE : $status;
  1145. $ue->userid = $userid;
  1146. $ue->timestart = $timestart;
  1147. $ue->timeend = $timeend;
  1148. $ue->modifierid = $USER->id;
  1149. $ue->timecreated = time();
  1150. $ue->timemodified = $ue->timecreated;
  1151. $ue->id = $DB->insert_record('user_enrolments', $ue);
  1152. $inserted = true;
  1153. }
  1154. if ($inserted) {
  1155. // Trigger event.
  1156. $event = \core\event\user_enrolment_created::create(
  1157. array(
  1158. 'objectid' => $ue->id,
  1159. 'courseid' => $courseid,
  1160. 'context' => $context,
  1161. 'relateduserid' => $ue->userid,
  1162. 'other' => array('enrol' => $name)
  1163. )
  1164. );
  1165. $event->trigger();
  1166. }
  1167. if ($roleid) {
  1168. // this must be done after the enrolment event so that the role_assigned event is triggered afterwards
  1169. if ($this->roles_protected()) {
  1170. role_assign($roleid, $userid, $context->id, 'enrol_'.$name, $instance->id);
  1171. } else {
  1172. role_assign($roleid, $userid, $context->id);
  1173. }
  1174. }
  1175. // Recover old grades if present.
  1176. if ($recovergrades) {
  1177. require_once("$CFG->libdir/gradelib.php");
  1178. grade_recover_history_grades($userid, $courseid);
  1179. }
  1180. // reset current user enrolment caching
  1181. if ($userid == $USER->id) {
  1182. if (isset($USER->enrol['enrolled'][$courseid])) {
  1183. unset($USER->enrol['enrolled'][$courseid]);
  1184. }
  1185. if (isset($USER->enrol['tempguest'][$courseid])) {
  1186. unset($USER->enrol['tempguest'][$courseid]);
  1187. remove_temp_course_roles($context);
  1188. }
  1189. }
  1190. }
  1191. /**
  1192. * Store user_enrolments changes and trigger event.
  1193. *
  1194. * @param stdClass $instance
  1195. * @param int $userid
  1196. * @param int $status
  1197. * @param int $timestart
  1198. * @param int $timeend
  1199. * @return void
  1200. */
  1201. public function update_user_enrol(stdClass $instance, $userid, $status = NULL, $timestart = NULL, $timeend = NULL) {
  1202. global $DB, $USER;
  1203. $name = $this->get_name();
  1204. if ($instance->enrol !== $name) {
  1205. throw new coding_exception('invalid enrol instance!');
  1206. }
  1207. if (!$ue = $DB->get_record('user_enrolments', array('enrolid'=>$instance->id, 'userid'=>$userid))) {
  1208. // weird, user not enrolled
  1209. return;
  1210. }
  1211. $modified = false;
  1212. if (isset($status) and $ue->status != $status) {
  1213. $ue->status = $status;
  1214. $modified = true;
  1215. }
  1216. if (isset($timestart) and $ue->timestart != $timestart) {
  1217. $ue->timestart = $timestart;
  1218. $modified = true;
  1219. }
  1220. if (isset($timeend) and $ue->timeend != $timeend) {
  1221. $ue->timeend = $timeend;
  1222. $modified = true;
  1223. }
  1224. if (!$modified) {
  1225. // no change
  1226. return;
  1227. }
  1228. $ue->modifierid = $USER->id;
  1229. $DB->update_record('user_enrolments', $ue);
  1230. context_course::instance($instance->courseid)->mark_dirty(); // reset enrol caches
  1231. // Invalidate core_access cache for get_suspended_userids.
  1232. cache_helper::invalidate_by_definition('core', 'suspended_userids', array(), array($instance->courseid));
  1233. // Trigger event.
  1234. $event = \core\event\user_enrolment_updated::create(
  1235. array(
  1236. 'objectid' => $ue->id,
  1237. 'courseid' => $instance->courseid,
  1238. 'context' => context_course::instance($instance->courseid),
  1239. 'relateduserid' => $ue->userid,
  1240. 'other' => array('enrol' => $name)
  1241. )
  1242. );
  1243. $event->trigger();
  1244. }
  1245. /**
  1246. * Unenrol user from course,
  1247. * the last unenrolment removes all remaining roles.
  1248. *
  1249. * @param stdClass $instance
  1250. * @param int $userid
  1251. * @return void
  1252. */
  1253. public function unenrol_user(stdClass $instance, $userid) {
  1254. global $CFG, $USER, $DB;
  1255. require_once("$CFG->dirroot/group/lib.php");
  1256. $name = $this->get_name();
  1257. $courseid = $instance->courseid;
  1258. if ($instance->enrol !== $name) {
  1259. throw new coding_exception('invalid enrol instance!');
  1260. }
  1261. $context = context_course::instance($instance->courseid, MUST_EXIST);
  1262. if (!$ue = $DB->get_record('user_enrolments', array('enrolid'=>$instance->id, 'userid'=>$userid))) {
  1263. // weird, user not enrolled
  1264. return;
  1265. }
  1266. // Remove all users groups linked to this enrolment instance.
  1267. if ($gms = $DB->get_records('groups_members', array('userid'=>$userid, 'component'=>'enrol_'.$name, 'itemid'=>$instance->id))) {
  1268. foreach ($gms as $gm) {
  1269. groups_remove_member($gm->groupid, $gm->userid);
  1270. }
  1271. }
  1272. role_unassign_all(array('userid'=>$userid, 'contextid'=>$context->id, 'component'=>'enrol_'.$name, 'itemid'=>$instance->id));
  1273. $DB->delete_records('user_enrolments', array('id'=>$ue->id));
  1274. // add extra info and trigger event
  1275. $ue->courseid = $courseid;
  1276. $ue->enrol = $name;
  1277. $sql = "SELECT 'x'
  1278. FROM {user_enrolments} ue
  1279. JOIN {enrol} e ON (e.id = ue.enrolid)
  1280. WHERE ue.userid = :userid AND e.courseid = :courseid";
  1281. if ($DB->record_exists_sql($sql, array('userid'=>$userid, 'courseid'=>$courseid))) {
  1282. $ue->lastenrol = false;
  1283. } else {
  1284. // the big cleanup IS necessary!
  1285. require_once("$CFG->libdir/gradelib.php");
  1286. // remove all remaining roles
  1287. role_unassign_all(array('userid'=>$userid, 'contextid'=>$context->id), true, false);
  1288. //clean up ALL invisible user data from course if this is the last enrolment - groups, grades, etc.
  1289. groups_delete_group_members($courseid, $userid);
  1290. grade_user_unenrol($courseid, $userid);
  1291. $DB->delete_records('user_lastaccess', array('userid'=>$userid, 'courseid'=>$courseid));
  1292. $ue->lastenrol = true; // means user not enrolled any more
  1293. }
  1294. // Trigger event.
  1295. $event = \core\event\user_enrolment_deleted::create(
  1296. array(
  1297. 'courseid' => $courseid,
  1298. 'context' => $context,
  1299. 'relateduserid' => $ue->userid,
  1300. 'objectid' => $ue->id,
  1301. 'other' => array(
  1302. 'userenrolment' => (array)$ue,
  1303. 'enrol' => $name
  1304. )
  1305. )
  1306. );
  1307. $event->trigger();
  1308. // reset all enrol caches
  1309. $context->mark_dirty();
  1310. // reset current user enrolment caching
  1311. if ($userid == $USER->id) {
  1312. if (isset($USER->enrol['enrolled'][$courseid])) {
  1313. unset($USER->enrol['enrolled'][$courseid]);
  1314. }
  1315. if (isset($USER->enrol['tempguest'][$courseid])) {
  1316. unset($USER->enrol['tempguest'][$courseid]);
  1317. remove_temp_course_roles($context);
  1318. }
  1319. }
  1320. }
  1321. /**
  1322. * Forces synchronisation of user enrolments.
  1323. *
  1324. * This is important especially for external enrol plugins,
  1325. * this function is called for all enabled enrol plugins
  1326. * right after every user login.
  1327. *
  1328. * @param object $user user record
  1329. * @return void
  1330. */
  1331. public function sync_user_enrolments($user) {
  1332. // override if necessary
  1333. }
  1334. /**
  1335. * Returns link to page which may be used to add new instance of enrolment plugin in course.
  1336. * @param int $courseid
  1337. * @return moodle_url page url
  1338. */
  1339. public function get_newinstance_link($courseid) {
  1340. // override for most plugins, check if instance already exists in cases only one instance is supported
  1341. return NULL;
  1342. }
  1343. /**
  1344. * Is it possible to delete enrol instance via standard UI?
  1345. *
  1346. * @param object $instance
  1347. * @return bool
  1348. */
  1349. public function instance_deleteable($instance) {
  1350. return true;
  1351. }
  1352. /**
  1353. * Returns link to manual enrol UI if exists.
  1354. * Does the access control tests automatically.
  1355. *
  1356. * @param object $instance
  1357. * @return moodle_url
  1358. */
  1359. public function get_manual_enrol_link($instance) {
  1360. return NULL;
  1361. }
  1362. /**
  1363. * Returns list of unenrol links for all enrol instances in course.
  1364. *
  1365. * @param int $instance
  1366. * @return moodle_url or NULL if self unenrolment not supported
  1367. */
  1368. public function get_unenrolself_link($instance) {
  1369. global $USER, $CFG, $DB;
  1370. $name = $this->get_name();
  1371. if ($instance->enrol !== $name) {
  1372. throw new coding_exception('invalid enrol instance!');
  1373. }
  1374. if ($instance->courseid == SITEID) {
  1375. return NULL;
  1376. }
  1377. if (!enrol_is_enabled($name)) {
  1378. return NULL;
  1379. }
  1380. if ($instance->status != ENROL_INSTANCE_ENABLED) {
  1381. return NULL;
  1382. }
  1383. if (!file_exists("$CFG->dirroot/enrol/$name/unenrolself.php")) {
  1384. return NULL;
  1385. }
  1386. $context = context_course::instance($instance->courseid, MUST_EXIST);
  1387. if (!has_capability("enrol/$name:unenrolself", $context)) {
  1388. return NULL;
  1389. }
  1390. if (!$DB->record_exists('user_enrolments', array('enrolid'=>$instance->id, 'userid'=>$USER->id, 'status'=>ENROL_USER_ACTIVE))) {
  1391. return NULL;
  1392. }
  1393. return new moodle_url("/enrol/$name/unenrolself.php", array('enrolid'=>$instance->id));
  1394. }
  1395. /**
  1396. * Adds enrol instance UI to course edit form
  1397. *
  1398. * @param object $instance enrol instance or null if does not exist yet
  1399. * @param MoodleQuickForm $mform
  1400. * @param object $data
  1401. * @param object $context context of existing course or parent category if course does not exist
  1402. * @return void
  1403. */
  1404. public function course_edit_form($instance, MoodleQuickForm $mform, $data, $context) {
  1405. // override - usually at least enable/disable switch, has to add own form header
  1406. }
  1407. /**
  1408. * Validates course edit form data
  1409. *
  1410. * @param object $instance enrol instance or null if does not exist yet
  1411. * @param array $data
  1412. * @param object $context context of existing course or parent category if course does not exist
  1413. * @return array errors array
  1414. */
  1415. public function course_edit_validation($instance, array $data, $context) {
  1416. return array();
  1417. }
  1418. /**
  1419. * Called after updating/inserting course.
  1420. *
  1421. * @param bool $inserted true if course just inserted
  1422. * @param object $course
  1423. * @param object $data form data
  1424. * @return void
  1425. */
  1426. public function course_updated($inserted, $course, $data) {
  1427. if ($inserted) {
  1428. if ($this->get_config('defaultenrol')) {
  1429. $this->add_default_instance($course);
  1430. }
  1431. }
  1432. }
  1433. /**
  1434. * Add new instance of enrol plugin.
  1435. * @param object $course
  1436. * @param array instance fields
  1437. * @return int id of new instance, null if can not be created
  1438. */
  1439. public function add_instance($course, array $fields = NULL) {
  1440. global $DB;
  1441. if ($course->id == SITEID) {
  1442. throw new coding_exception('Invalid request to add enrol instance to frontpage.');
  1443. }
  1444. $instance = new stdClass();
  1445. $instance->enrol = $this->get_name();
  1446. $instance->status = ENROL_INSTANCE_ENABLED;
  1447. $instance->courseid = $course->id;
  1448. $instance->enrolstartdate = 0;
  1449. $instance->enrolenddate = 0;
  1450. $instance->timemodified = time();
  1451. $instance->timecreated = $instance->timemodified;
  1452. $instance->sortorder = $DB->get_field('enrol', 'COALESCE(MAX(sortorder), -1) + 1', array('courseid'=>$course->id));
  1453. $fields = (array)$fields;
  1454. unset($fields['enrol']);
  1455. unset($fields['courseid']);
  1456. unset($fields['sortorder']);
  1457. foreach($fields as $field=>$value) {
  1458. $instance->$field = $value;
  1459. }
  1460. return $DB->insert_record('enrol', $instance);
  1461. }
  1462. /**
  1463. * Add new instance of enrol plugin with default settings,
  1464. * called when adding new instance manually or when adding new course.
  1465. *
  1466. * Not all plugins support this.
  1467. *
  1468. * @param object $course
  1469. * @return int id of new instance or null if no default supported
  1470. */
  1471. public function add_default_instance($course) {
  1472. return null;
  1473. }
  1474. /**
  1475. * Update instance status
  1476. *
  1477. * Override when plugin needs to do some action when enabled or disabled.
  1478. *
  1479. * @param stdClass $instance
  1480. * @param int $newstatus ENROL_INSTANCE_ENABLED, ENROL_INSTANCE_DISABLED
  1481. * @return void
  1482. */
  1483. public function update_status($instance, $newstatus) {
  1484. global $DB;
  1485. $instance->status = $newstatus;
  1486. $DB->update_record('enrol', $instance);
  1487. // invalidate all enrol caches
  1488. $context = context_course::instance($instance->courseid);
  1489. $context->mark_dirty();
  1490. }
  1491. /**
  1492. * Delete course enrol plugin instance, unenrol all users.
  1493. * @param object $instance
  1494. * @return void
  1495. */
  1496. public function delete_instance($instance) {
  1497. global $DB;
  1498. $name = $this->get_name();
  1499. if ($instance->enrol !== $name) {
  1500. throw new coding_exception('invalid enrol instance!');
  1501. }
  1502. //first unenrol all users
  1503. $participants = $DB->get_recordset('user_enrolments', array('enrolid'=>$instance->id));
  1504. foreach ($participants as $participant) {
  1505. $this->unenrol_user($instance, $participant->userid);
  1506. }
  1507. $participants->close();
  1508. // now clean up all remainders that were not removed correctly
  1509. $DB->delete_records('groups_members', array('itemid'=>$instance->id, 'component'=>'enrol_'.$name));
  1510. $DB->delete_records('role_assignments', array('itemid'=>$instance->id, 'component'=>'enrol_'.$name));
  1511. $DB->delete_records('user_enrolments', array('enrolid'=>$instance->id));
  1512. // finally drop the enrol row
  1513. $DB->delete_records('enrol', array('id'=>$instance->id));
  1514. // invalidate all enrol caches
  1515. $context = context_course::instance($instance->courseid);
  1516. $context->mark_dirty();
  1517. }
  1518. /**
  1519. * Creates course enrol form, checks if form submitted
  1520. * and enrols user if necessary. It can also redirect.
  1521. *
  1522. * @param stdClass $instance
  1523. * @return string html text, usually a form in a text box
  1524. */
  1525. public function enrol_page_hook(stdClass $instance) {
  1526. return null;
  1527. }
  1528. /**
  1529. * Checks if user can self enrol.
  1530. *
  1531. * @param stdClass $instance enrolment instance
  1532. * @param bool $checkuserenrolment if true will check if user enrolment is inactive.
  1533. * used by navigation to improve performance.
  1534. * @return bool|string true if successful, else error message or false
  1535. */
  1536. public function can_self_enrol(stdClass $instance, $checkuserenrolment = true) {
  1537. return false;
  1538. }
  1539. /**
  1540. * Return information for enrolment instance containing list of parameters required
  1541. * for enrolment, name of enrolment plugin etc.
  1542. *
  1543. * @param stdClass $instance enrolment instance
  1544. * @return array instance info.
  1545. */
  1546. public function get_enrol_info(stdClass $instance) {
  1547. return null;
  1548. }
  1549. /**
  1550. * Adds navigation links into course admin block.
  1551. *
  1552. * By defaults looks for manage links only.
  1553. *
  1554. * @param navigation_node $instancesnode
  1555. * @param stdClass $instance
  1556. * @return void
  1557. */
  1558. public function add_course_navigation($instancesnode, stdClass $instance) {
  1559. // usually adds manage users
  1560. }
  1561. /**
  1562. * Returns edit icons for the page with list of instances
  1563. * @param stdClass $instance
  1564. * @return array
  1565. */
  1566. public function get_action_icons(stdClass $instance) {
  1567. return array();
  1568. }
  1569. /**
  1570. * Reads version.php and determines if it is necessary
  1571. * to execute the cron job now.
  1572. * @return bool
  1573. */
  1574. public function is_cron_required() {
  1575. global $CFG;
  1576. $name = $this->get_name();
  1577. $versionfile = "$CFG->dirroot/enrol/$name/version.php";
  1578. $plugin = new stdClass();
  1579. include($versionfile);
  1580. if (empty($plugin->cron)) {
  1581. return false;
  1582. }
  1583. $lastexecuted = $this->get_config('lastcron', 0);
  1584. if ($lastexecuted + $plugin->cron < time()) {
  1585. return true;
  1586. } else {
  1587. return false;
  1588. }
  1589. }
  1590. /**
  1591. * Called for all enabled enrol plugins that returned true from is_cron_required().
  1592. * @return void
  1593. */
  1594. public function cron() {
  1595. }
  1596. /**
  1597. * Called when user is about to be deleted
  1598. * @param object $user
  1599. * @return void
  1600. */
  1601. public function user_delete($user) {
  1602. global $DB;
  1603. $sql = "SELECT e.*
  1604. FROM {enrol} e
  1605. JOIN {user_enrolments} ue ON (ue.enrolid = e.id)
  1606. WHERE e.enrol = :name AND ue.userid = :userid";
  1607. $params = array('name'=>$this->get_name(), 'userid'=>$user->id);
  1608. $rs = $DB->get_recordset_sql($sql, $params);
  1609. foreach($rs as $instance) {
  1610. $this->unenrol_user($instance, $user->id);
  1611. }
  1612. $rs->close();
  1613. }
  1614. /**
  1615. * Returns an enrol_user_button that takes the user to a page where they are able to
  1616. * enrol users into the managers course through this plugin.
  1617. *
  1618. * Optional: If the plugin supports manual enrolments it can choose to override this
  1619. * otherwise it shouldn't
  1620. *
  1621. * @param course_enrolment_manager $manager
  1622. * @return enrol_user_button|false
  1623. */
  1624. public function get_manual_enrol_button(course_enrolment_manager $manager) {
  1625. return false;
  1626. }
  1627. /**
  1628. * Gets an array of the user enrolment actions
  1629. *
  1630. * @param course_enrolment_manager $manager
  1631. * @param stdClass $ue
  1632. * @return array An array of user_enrolment_actions
  1633. */
  1634. public function get_user_enrolment_actions(course_enrolment_manager $manager, $ue) {
  1635. return array();
  1636. }
  1637. /**
  1638. * Returns true if the plugin has one or more bulk operations that can be performed on
  1639. * user enrolments.
  1640. *
  1641. * @param course_enrolment_manager $manager
  1642. * @return bool
  1643. */
  1644. public function has_bulk_operations(course_enrolment_manager $manager) {
  1645. return false;
  1646. }
  1647. /**
  1648. * Return an array of enrol_bulk_enrolment_operation objects that define
  1649. * the bulk actions that can be performed on user enrolments by the plugin.
  1650. *
  1651. * @param course_enrolment_manager $manager
  1652. * @return array
  1653. */
  1654. public function get_bulk_operations(course_enrolment_manager $manager) {
  1655. return array();
  1656. }
  1657. /**
  1658. * Do any enrolments need expiration processing.
  1659. *
  1660. * Plugins that want to call this functionality must implement 'expiredaction' config setting.
  1661. *
  1662. * @param progress_trace $trace
  1663. * @param int $courseid one course, empty mean all
  1664. * @return bool true if any data processed, false if not
  1665. */
  1666. public function process_expirations(progress_trace $trace, $courseid = null) {
  1667. global $DB;
  1668. $name = $this->get_name();
  1669. if (!enrol_is_enabled($name)) {
  1670. $trace->finished();
  1671. return false;
  1672. }
  1673. $processed = false;
  1674. $params = array();
  1675. $coursesql = "";
  1676. if ($courseid) {
  1677. $coursesql = "AND e.courseid = :courseid";
  1678. }
  1679. // Deal with expired accounts.
  1680. $action = $this->get_config('expiredaction', ENROL_EXT_REMOVED_KEEP);
  1681. if ($action == ENROL_EXT_REMOVED_UNENROL) {
  1682. $instances = array();
  1683. $sql = "SELECT ue.*, e.courseid, c.id AS contextid
  1684. FROM {user_enrolments} ue
  1685. JOIN {enrol} e ON (e.id = ue.enrolid AND e.enrol = :enrol)
  1686. JOIN {context} c ON (c.instanceid = e.courseid AND c.contextlevel = :courselevel)
  1687. WHERE ue.timeend > 0 AND ue.timeend < :now $coursesql";
  1688. $params = array('now'=>time(), 'courselevel'=>CONTEXT_COURSE, 'enrol'=>$name, 'courseid'=>$courseid);
  1689. $rs = $DB->get_recordset_sql($sql, $params);
  1690. foreach ($rs as $ue) {
  1691. if (!$processed) {
  1692. $trace->output("Starting processing of enrol_$name expirations...");
  1693. $processed = true;
  1694. }
  1695. if (empty($instances[$ue->enrolid])) {
  1696. $instances[$ue->enrolid] = $DB->get_record('enrol', array('id'=>$ue->enrolid));
  1697. }
  1698. $instance = $instances[$ue->enrolid];
  1699. if (!$this->roles_protected()) {
  1700. // Let's just guess what extra roles are supposed to be removed.
  1701. if ($instance->roleid) {
  1702. role_unassign($instance->roleid, $ue->userid, $ue->contextid);
  1703. }
  1704. }
  1705. // The unenrol cleans up all subcontexts if this is the only course enrolment for this user.
  1706. $this->unenrol_user($instance, $ue->userid);
  1707. $trace->output("Unenrolling expired user $ue->userid from course $instance->courseid", 1);
  1708. }
  1709. $rs->close();
  1710. unset($instances);
  1711. } else if ($action == ENROL_EXT_REMOVED_SUSPENDNOROLES or $action == ENROL_EXT_REMOVED_SUSPEND) {
  1712. $instances = array();
  1713. $sql = "SELECT ue.*, e.courseid, c.id AS contextid
  1714. FROM {user_enrolments} ue
  1715. JOIN {enrol} e ON (e.id = ue.enrolid AND e.enrol = :enrol)
  1716. JOIN {context} c ON (c.instanceid = e.courseid AND c.contextlevel = :courselevel)
  1717. WHERE ue.timeend > 0 AND ue.timeend < :now
  1718. AND ue.status = :useractive $coursesql";
  1719. $params = array('now'=>time(), 'courselevel'=>CONTEXT_COURSE, 'useractive'=>ENROL_USER_ACTIVE, 'enrol'=>$name, 'courseid'=>$courseid);
  1720. $rs = $DB->get_recordset_sql($sql, $params);
  1721. foreach ($rs as $ue) {
  1722. if (!$processed) {
  1723. $trace->output("Starting processing of enrol_$name expirations...");
  1724. $processed = true;
  1725. }
  1726. if (empty($instances[$ue->enrolid])) {
  1727. $instances[$ue->enrolid] = $DB->get_record('enrol', array('id'=>$ue->enrolid));
  1728. }
  1729. $instance = $instances[$ue->enrolid];
  1730. if ($action == ENROL_EXT_REMOVED_SUSPENDNOROLES) {
  1731. if (!$this->roles_protected()) {
  1732. // Let's just guess what roles should be removed.
  1733. $count = $DB->count_records('role_assignments', array('userid'=>$ue->userid, 'contextid'=>$ue->contextid));
  1734. if ($count == 1) {
  1735. role_unassign_all(array('userid'=>$ue->userid, 'contextid'=>$ue->contextid, 'component'=>'', 'itemid'=>0));
  1736. } else if ($count > 1 and $instance->roleid) {
  1737. role_unassign($instance->roleid, $ue->userid, $ue->contextid, '', 0);
  1738. }
  1739. }
  1740. // In any case remove all roles that belong to this instance and user.
  1741. role_unassign_all(array('userid'=>$ue->userid, 'contextid'=>$ue->contextid, 'component'=>'enrol_'.$name, 'itemid'=>$instance->id), true);
  1742. // Final cleanup of subcontexts if there are no more course roles.
  1743. if (0 == $DB->count_records('role_assignments', array('userid'=>$ue->userid, 'contextid'=>$ue->contextid))) {
  1744. role_unassign_all(array('userid'=>$ue->userid, 'contextid'=>$ue->contextid, 'component'=>'', 'itemid'=>0), true);
  1745. }
  1746. }
  1747. $this->update_user_enrol($instance, $ue->userid, ENROL_USER_SUSPENDED);
  1748. $trace->output("Suspending expired user $ue->userid in course $instance->courseid", 1);
  1749. }
  1750. $rs->close();
  1751. unset($instances);
  1752. } else {
  1753. // ENROL_EXT_REMOVED_KEEP means no changes.
  1754. }
  1755. if ($processed) {
  1756. $trace->output("...finished processing of enrol_$name expirations");
  1757. } else {
  1758. $trace->output("No expired enrol_$name enrolments detected");
  1759. }
  1760. $trace->finished();
  1761. return $processed;
  1762. }
  1763. /**
  1764. * Send expiry notifications.
  1765. *
  1766. * Plugin that wants to have expiry notification MUST implement following:
  1767. * - expirynotifyhour plugin setting,
  1768. * - configuration options in instance edit form (expirynotify, notifyall and expirythreshold),
  1769. * - notification strings (expirymessageenrollersubject, expirymessageenrollerbody,
  1770. * expirymessageenrolledsubject and expirymessageenrolledbody),
  1771. * - expiry_notification provider in db/messages.php,
  1772. * - upgrade code that sets default thresholds for existing courses (should be 1 day),
  1773. * - something that calls this method, such as cron.
  1774. *
  1775. * @param progress_trace $trace (accepts bool for backwards compatibility only)
  1776. */
  1777. public function send_expiry_notifications($trace) {
  1778. global $DB, $CFG;
  1779. $name = $this->get_name();
  1780. if (!enrol_is_enabled($name)) {
  1781. $trace->finished();
  1782. return;
  1783. }
  1784. // Unfortunately this may take a long time, it should not be interrupted,
  1785. // otherwise users get duplicate notification.
  1786. @set_time_limit(0);
  1787. raise_memory_limit(MEMORY_HUGE);
  1788. $expirynotifylast = $this->get_config('expirynotifylast', 0);
  1789. $expirynotifyhour = $this->get_config('expirynotifyhour');
  1790. if (is_null($expirynotifyhour)) {
  1791. debugging("send_expiry_notifications() in $name enrolment plugin needs expirynotifyhour setting");
  1792. $trace->finished();
  1793. return;
  1794. }
  1795. if (!($trace instanceof progress_trace)) {
  1796. $trace = $trace ? new text_progress_trace() : new null_progress_trace();
  1797. debugging('enrol_plugin::send_expiry_notifications() now expects progress_trace instance as parameter!', DEBUG_DEVELOPER);
  1798. }
  1799. $timenow = time();
  1800. $notifytime = usergetmidnight($timenow, $CFG->timezone) + ($expirynotifyhour * 3600);
  1801. if ($expirynotifylast > $notifytime) {
  1802. $trace->output($name.' enrolment expiry notifications were already sent today at '.userdate($expirynotifylast, '', $CFG->timezone).'.');
  1803. $trace->finished();
  1804. return;
  1805. } else if ($timenow < $notifytime) {
  1806. $trace->output($name.' enrolment expiry notifications will be sent at '.userdate($notifytime, '', $CFG->timezone).'.');
  1807. $trace->finished();
  1808. return;
  1809. }
  1810. $trace->output('Processing '.$name.' enrolment expiration notifications...');
  1811. // Notify users responsible for enrolment once every day.
  1812. $sql = "SELECT ue.*, e.expirynotify, e.notifyall, e.expirythreshold, e.courseid, c.fullname
  1813. FROM {user_enrolments} ue
  1814. JOIN {enrol} e ON (e.id = ue.enrolid AND e.enrol = :name AND e.expirynotify > 0 AND e.status = :enabled)
  1815. JOIN {course} c ON (c.id = e.courseid)
  1816. JOIN {user} u ON (u.id = ue.userid AND u.deleted = 0 AND u.suspended = 0)
  1817. WHERE ue.status = :active AND ue.timeend > 0 AND ue.timeend > :now1 AND ue.timeend < (e.expirythreshold + :now2)
  1818. ORDER BY ue.enrolid ASC, u.lastname ASC, u.firstname ASC, u.id ASC";
  1819. $params = array('enabled'=>ENROL_INSTANCE_ENABLED, 'active'=>ENROL_USER_ACTIVE, 'now1'=>$timenow, 'now2'=>$timenow, 'name'=>$name);
  1820. $rs = $DB->get_recordset_sql($sql, $params);
  1821. $lastenrollid = 0;
  1822. $users = array();
  1823. foreach($rs as $ue) {
  1824. if ($lastenrollid and $lastenrollid != $ue->enrolid) {
  1825. $this->notify_expiry_enroller($lastenrollid, $users, $trace);
  1826. $users = array();
  1827. }
  1828. $lastenrollid = $ue->enrolid;
  1829. $enroller = $this->get_enroller($ue->enrolid);
  1830. $context = context_course::instance($ue->courseid);
  1831. $user = $DB->get_record('user', array('id'=>$ue->userid));
  1832. $users[] = array('fullname'=>fullname($user, has_capability('moodle/site:viewfullnames', $context, $enroller)), 'timeend'=>$ue->timeend);
  1833. if (!$ue->notifyall) {
  1834. continue;
  1835. }
  1836. if ($ue->timeend - $ue->expirythreshold + 86400 < $timenow) {
  1837. // Notify enrolled users only once at the start of the threshold.
  1838. $trace->output("user $ue->userid was already notified that enrolment in course $ue->courseid expires on ".userdate($ue->timeend, '', $CFG->timezone), 1);
  1839. continue;
  1840. }
  1841. $this->notify_expiry_enrolled($user, $ue, $trace);
  1842. }
  1843. $rs->close();
  1844. if ($lastenrollid and $users) {
  1845. $this->notify_expiry_enroller($lastenrollid, $users, $trace);
  1846. }
  1847. $trace->output('...notification processing finished.');
  1848. $trace->finished();
  1849. $this->set_config('expirynotifylast', $timenow);
  1850. }
  1851. /**
  1852. * Returns the user who is responsible for enrolments for given instance.
  1853. *
  1854. * Override if plugin knows anybody better than admin.
  1855. *
  1856. * @param int $instanceid enrolment instance id
  1857. * @return stdClass user record
  1858. */
  1859. protected function get_enroller($instanceid) {
  1860. return get_admin();
  1861. }
  1862. /**
  1863. * Notify user about incoming expiration of their enrolment,
  1864. * it is called only if notification of enrolled users (aka students) is enabled in course.
  1865. *
  1866. * This is executed only once for each expiring enrolment right
  1867. * at the start of the expiration threshold.
  1868. *
  1869. * @param stdClass $user
  1870. * @param stdClass $ue
  1871. * @param progress_trace $trace
  1872. */
  1873. protected function notify_expiry_enrolled($user, $ue, progress_trace $trace) {
  1874. global $CFG, $SESSION;
  1875. $name = $this->get_name();
  1876. // Some nasty hackery to get strings and dates localised for target user.
  1877. $sessionlang = isset($SESSION->lang) ? $SESSION->lang : null;
  1878. if (get_string_manager()->translation_exists($user->lang, false)) {
  1879. $SESSION->lang = $user->lang;
  1880. moodle_setlocale();
  1881. }
  1882. $enroller = $this->get_enroller($ue->enrolid);
  1883. $context = context_course::instance($ue->courseid);
  1884. $a = new stdClass();
  1885. $a->course = format_string($ue->fullname, true, array('context'=>$context));
  1886. $a->user = fullname($user, true);
  1887. $a->timeend = userdate($ue->timeend, '', $user->timezone);
  1888. $a->enroller = fullname($enroller, has_capability('moodle/site:viewfullnames', $context, $user));
  1889. $subject = get_string('expirymessageenrolledsubject', 'enrol_'.$name, $a);
  1890. $body = get_string('expirymessageenrolledbody', 'enrol_'.$name, $a);
  1891. $message = new stdClass();
  1892. $message->notification = 1;
  1893. $message->component = 'enrol_'.$name;
  1894. $message->name = 'expiry_notification';
  1895. $message->userfrom = $enroller;
  1896. $message->userto = $user;
  1897. $message->subject = $subject;
  1898. $message->fullmessage = $body;
  1899. $message->fullmessageformat = FORMAT_MARKDOWN;
  1900. $message->fullmessagehtml = markdown_to_html($body);
  1901. $message->smallmessage = $subject;
  1902. $message->contexturlname = $a->course;
  1903. $message->contexturl = (string)new moodle_url('/course/view.php', array('id'=>$ue->courseid));
  1904. if (message_send($message)) {
  1905. $trace->output("notifying user $ue->userid that enrolment in course $ue->courseid expires on ".userdate($ue->timeend, '', $CFG->timezone), 1);
  1906. } else {
  1907. $trace->output("error notifying user $ue->userid that enrolment in course $ue->courseid expires on ".userdate($ue->timeend, '', $CFG->timezone), 1);
  1908. }
  1909. if ($SESSION->lang !== $sessionlang) {
  1910. $SESSION->lang = $sessionlang;
  1911. moodle_setlocale();
  1912. }
  1913. }
  1914. /**
  1915. * Notify person responsible for enrolments that some user enrolments will be expired soon,
  1916. * it is called only if notification of enrollers (aka teachers) is enabled in course.
  1917. *
  1918. * This is called repeatedly every day for each course if there are any pending expiration
  1919. * in the expiration threshold.
  1920. *
  1921. * @param int $eid
  1922. * @param array $users
  1923. * @param progress_trace $trace
  1924. */
  1925. protected function notify_expiry_enroller($eid, $users, progress_trace $trace) {
  1926. global $DB, $SESSION;
  1927. $name = $this->get_name();
  1928. $instance = $DB->get_record('enrol', array('id'=>$eid, 'enrol'=>$name));
  1929. $context = context_course::instance($instance->courseid);
  1930. $course = $DB->get_record('course', array('id'=>$instance->courseid));
  1931. $enroller = $this->get_enroller($instance->id);
  1932. $admin = get_admin();
  1933. // Some nasty hackery to get strings and dates localised for target user.
  1934. $sessionlang = isset($SESSION->lang) ? $SESSION->lang : null;
  1935. if (get_string_manager()->translation_exists($enroller->lang, false)) {
  1936. $SESSION->lang = $enroller->lang;
  1937. moodle_setlocale();
  1938. }
  1939. foreach($users as $key=>$info) {
  1940. $users[$key] = '* '.$info['fullname'].' - '.userdate($info['timeend'], '', $enroller->timezone);
  1941. }
  1942. $a = new stdClass();
  1943. $a->course = format_string($course->fullname, true, array('context'=>$context));
  1944. $a->threshold = get_string('numdays', '', $instance->expirythreshold / (60*60*24));
  1945. $a->users = implode("\n", $users);
  1946. $a->extendurl = (string)new moodle_url('/enrol/users.php', array('id'=>$instance->courseid));
  1947. $subject = get_string('expirymessageenrollersubject', 'enrol_'.$name, $a);
  1948. $body = get_string('expirymessageenrollerbody', 'enrol_'.$name, $a);
  1949. $message = new stdClass();
  1950. $message->notification = 1;
  1951. $message->component = 'enrol_'.$name;
  1952. $message->name = 'expiry_notification';
  1953. $message->userfrom = $admin;
  1954. $message->userto = $enroller;
  1955. $message->subject = $subject;
  1956. $message->fullmessage = $body;
  1957. $message->fullmessageformat = FORMAT_MARKDOWN;
  1958. $message->fullmessagehtml = markdown_to_html($body);
  1959. $message->smallmessage = $subject;
  1960. $message->contexturlname = $a->course;
  1961. $message->contexturl = $a->extendurl;
  1962. if (message_send($message)) {
  1963. $trace->output("notifying user $enroller->id about all expiring $name enrolments in course $instance->courseid", 1);
  1964. } else {
  1965. $trace->output("error notifying user $enroller->id about all expiring $name enrolments in course $instance->courseid", 1);
  1966. }
  1967. if ($SESSION->lang !== $sessionlang) {
  1968. $SESSION->lang = $sessionlang;
  1969. moodle_setlocale();
  1970. }
  1971. }
  1972. /**
  1973. * Automatic enrol sync executed during restore.
  1974. * Useful for automatic sync by course->idnumber or course category.
  1975. * @param stdClass $course course record
  1976. */
  1977. public function restore_sync_course($course) {
  1978. // Override if necessary.
  1979. }
  1980. /**
  1981. * Restore instance and map settings.
  1982. *
  1983. * @param restore_enrolments_structure_step $step
  1984. * @param stdClass $data
  1985. * @param stdClass $course
  1986. * @param int $oldid
  1987. */
  1988. public function restore_instance(restore_enrolments_structure_step $step, stdClass $data, $course, $oldid) {
  1989. // Do not call this from overridden methods, restore and set new id there.
  1990. $step->set_mapping('enrol', $oldid, 0);
  1991. }
  1992. /**
  1993. * Restore user enrolment.
  1994. *
  1995. * @param restore_enrolments_structure_step $step
  1996. * @param stdClass $data
  1997. * @param stdClass $instance
  1998. * @param int $oldinstancestatus
  1999. * @param int $userid
  2000. */
  2001. public function restore_user_enrolment(restore_enrolments_structure_step $step, $data, $instance, $userid, $oldinstancestatus) {
  2002. // Override as necessary if plugin supports restore of enrolments.
  2003. }
  2004. /**
  2005. * Restore role assignment.
  2006. *
  2007. * @param stdClass $instance
  2008. * @param int $roleid
  2009. * @param int $userid
  2010. * @param int $contextid
  2011. */
  2012. public function restore_role_assignment($instance, $roleid, $userid, $contextid) {
  2013. // No role assignment by default, override if necessary.
  2014. }
  2015. /**
  2016. * Restore user group membership.
  2017. * @param stdClass $instance
  2018. * @param int $groupid
  2019. * @param int $userid
  2020. */
  2021. public function restore_group_member($instance, $groupid, $userid) {
  2022. // Implement if you want to restore protected group memberships,
  2023. // usually this is not necessary because plugins should be able to recreate the memberships automatically.
  2024. }
  2025. }