2 // This file is part of Moodle - http://moodle.org/
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.
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.
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/>.
18 * Local stuff for category enrolment plugin.
20 * @package enrol_category
21 * @copyright 2010 Petr Skoda {@link http://skodak.org}
22 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
25 defined('MOODLE_INTERNAL') || die();
29 * Event handler for category enrolment plugin.
31 * We try to keep everything in sync via listening to events,
32 * it may fail sometimes, so we always do a full sync in cron too.
34 class enrol_category_handler {
36 * Triggered when user is assigned a new role.
41 public static function role_assigned($ra) {
44 if (!enrol_is_enabled('category')) {
48 // Only category level roles are interesting.
49 $parentcontext = get_context_instance_by_id($ra->contextid);
50 if ($parentcontext->contextlevel != CONTEXT_COURSECAT) {
54 // Make sure the role is to be actually synchronised,
55 // please note we are ignoring overrides of the synchronised capability (for performance reasons in full sync).
56 $syscontext = context_system::instance();
57 if (!$DB->record_exists('role_capabilities', array('contextid'=>$syscontext->id, 'roleid'=>$ra->roleid, 'capability'=>'enrol/category:synchronised', 'permission'=>CAP_ALLOW))) {
61 // Add necessary enrol instances.
62 $plugin = enrol_get_plugin('category');
65 JOIN {context} ctx ON (ctx.instanceid = c.id AND ctx.contextlevel = :courselevel AND ctx.path LIKE :match)
66 LEFT JOIN {enrol} e ON (e.courseid = c.id AND e.enrol = 'category')
68 $params = array('courselevel'=>CONTEXT_COURSE, 'match'=>$parentcontext->path.'/%');
69 $rs = $DB->get_recordset_sql($sql, $params);
70 foreach ($rs as $course) {
71 $plugin->add_instance($course);
75 // Now look for missing enrolments.
78 JOIN {context} ctx ON (ctx.instanceid = c.id AND ctx.contextlevel = :courselevel AND ctx.path LIKE :match)
79 JOIN {enrol} e ON (e.courseid = c.id AND e.enrol = 'category')
80 LEFT JOIN {user_enrolments} ue ON (ue.enrolid = e.id AND ue.userid = :userid)
82 $params = array('courselevel'=>CONTEXT_COURSE, 'match'=>$parentcontext->path.'/%', 'userid'=>$ra->userid);
83 $rs = $DB->get_recordset_sql($sql, $params);
84 foreach ($rs as $instance) {
85 $plugin->enrol_user($instance, $ra->userid, null, $ra->timemodified);
93 * Triggered when user role is unassigned.
98 public static function role_unassigned($ra) {
101 if (!enrol_is_enabled('category')) {
105 // Only category level roles are interesting.
106 $parentcontext = get_context_instance_by_id($ra->contextid);
107 if ($parentcontext->contextlevel != CONTEXT_COURSECAT) {
111 // Now this is going to be a bit slow, take all enrolments in child courses and verify each separately.
112 $syscontext = context_system::instance();
113 if (!$roles = get_roles_with_capability('enrol/category:synchronised', CAP_ALLOW, $syscontext)) {
117 $plugin = enrol_get_plugin('category');
121 JOIN {context} ctx ON (ctx.instanceid = c.id AND ctx.contextlevel = :courselevel AND ctx.path LIKE :match)
122 JOIN {enrol} e ON (e.courseid = c.id AND e.enrol = 'category')
123 JOIN {user_enrolments} ue ON (ue.enrolid = e.id AND ue.userid = :userid)";
124 $params = array('courselevel'=>CONTEXT_COURSE, 'match'=>$parentcontext->path.'/%', 'userid'=>$ra->userid);
125 $rs = $DB->get_recordset_sql($sql, $params);
127 list($roleids, $params) = $DB->get_in_or_equal(array_keys($roles), SQL_PARAMS_NAMED, 'r');
128 $params['userid'] = $ra->userid;
130 foreach ($rs as $instance) {
131 $coursecontext = context_course::instance($instance->courseid);
132 $contextids = get_parent_contexts($coursecontext);
133 array_pop($contextids); // Remove system context, we are interested in categories only.
135 list($contextids, $contextparams) = $DB->get_in_or_equal($contextids, SQL_PARAMS_NAMED, 'c');
136 $params = array_merge($params, $contextparams);
139 FROM {role_assignments} ra
140 WHERE ra.userid = :userid AND ra.contextid $contextids AND ra.roleid $roleids";
141 if (!$DB->record_exists_sql($sql, $params)) {
142 // User does not have any interesting role in any parent context, let's unenrol.
143 $plugin->unenrol_user($instance, $ra->userid);
153 * Sync all category enrolments in one course
154 * @param stdClass $course
157 function enrol_category_sync_course($course) {
160 if (!enrol_is_enabled('category')) {
164 $plugin = enrol_get_plugin('category');
166 $syscontext = context_system::instance();
167 $roles = get_roles_with_capability('enrol/category:synchronised', CAP_ALLOW, $syscontext);
170 // Nothing to sync, so remove the instance completely if exists.
171 if ($instances = $DB->get_records('enrol', array('courseid'=>$course->id, 'enrol'=>'category'))) {
172 foreach ($instances as $instance) {
173 $plugin->delete_instance($instance);
179 // First find out if any parent category context contains interesting role assignments.
180 $coursecontext = context_course::instance($course->id);
181 $contextids = get_parent_contexts($coursecontext);
182 array_pop($contextids); // Remove system context, we are interested in categories only.
184 list($roleids, $params) = $DB->get_in_or_equal(array_keys($roles), SQL_PARAMS_NAMED, 'r');
185 list($contextids, $contextparams) = $DB->get_in_or_equal($contextids, SQL_PARAMS_NAMED, 'c');
186 $params = array_merge($params, $contextparams);
187 $params['courseid'] = $course->id;
190 FROM {role_assignments}
191 WHERE roleid $roleids AND contextid $contextids";
192 if (!$DB->record_exists_sql($sql, $params)) {
193 if ($instances = $DB->get_records('enrol', array('courseid'=>$course->id, 'enrol'=>'category'))) {
194 // Should be max one instance, but anyway.
195 foreach ($instances as $instance) {
196 $plugin->delete_instance($instance);
202 // Make sure the enrol instance exists - there should be always only one instance.
203 $delinstances = array();
204 if ($instances = $DB->get_records('enrol', array('courseid'=>$course->id, 'enrol'=>'category'))) {
205 $instance = array_shift($instances);
206 $delinstances = $instances;
208 $i = $plugin->add_instance($course);
209 $instance = $DB->get_record('enrol', array('id'=>$i));
212 // Add new enrolments.
213 $sql = "SELECT ra.userid, ra.estart
214 FROM (SELECT xra.userid, MIN(xra.timemodified) AS estart
215 FROM {role_assignments} xra
216 WHERE xra.roleid $roleids AND xra.contextid $contextids
219 LEFT JOIN {user_enrolments} ue ON (ue.enrolid = :instanceid AND ue.userid = ra.userid)
220 WHERE ue.id IS NULL";
221 $params['instanceid'] = $instance->id;
222 $rs = $DB->get_recordset_sql($sql, $params);
223 foreach ($rs as $ra) {
224 $plugin->enrol_user($instance, $ra->userid, null, $ra->estart);
228 // Remove unwanted enrolments.
229 $sql = "SELECT DISTINCT ue.userid
230 FROM {user_enrolments} ue
231 LEFT JOIN {role_assignments} ra ON (ra.roleid $roleids AND ra.contextid $contextids AND ra.userid = ue.userid)
232 WHERE ue.enrolid = :instanceid AND ra.id IS NULL";
233 $rs = $DB->get_recordset_sql($sql, $params);
234 foreach ($rs as $ra) {
235 $plugin->unenrol_user($instance, $ra->userid);
240 // We have to do this as the last step in order to prevent temporary unenrolment.
241 foreach ($delinstances as $delinstance) {
242 $plugin->delete_instance($delinstance);
248 * Synchronise courses in all categories.
250 * It gets out-of-sync if:
251 * - you move course to different category
252 * - reorder categories
253 * - disable enrol_category and enable it again
255 * @param bool $verbose
256 * @return int exit code - 0 is ok, 1 means error, 2 if plugin disabled
258 function enrol_category_sync_full($verbose = false) {
262 if (!enrol_is_enabled('category')) {
266 // We may need a lot of time here.
269 $plugin = enrol_get_plugin('category');
271 $syscontext = context_system::instance();
273 // Any interesting roles worth synchronising?
274 if (!$roles = get_roles_with_capability('enrol/category:synchronised', CAP_ALLOW, $syscontext)) {
275 // yay, nothing to do, so let's remove all leftovers
277 mtrace("No roles with 'enrol/category:synchronised' capability found.");
279 if ($instances = $DB->get_records('enrol', array('enrol'=>'category'))) {
280 foreach ($instances as $instance) {
282 mtrace(" deleting category enrol instance from course {$instance->courseid}");
284 $plugin->delete_instance($instance);
289 $rolenames = role_fix_names($roles, null, ROLENAME_SHORT, true);
291 mtrace('Synchronising category enrolments for roles: '.implode(', ', $rolenames).'...');
294 list($roleids, $params) = $DB->get_in_or_equal(array_keys($roles), SQL_PARAMS_NAMED, 'r');
295 $params['courselevel'] = CONTEXT_COURSE;
296 $params['catlevel'] = CONTEXT_COURSECAT;
298 // First of all add necessary enrol instances to all courses.
299 $parentcat = $DB->sql_concat("cat.path", "'/%'");
300 $parentcctx = $DB->sql_concat("cctx.path", "'/%'");
301 // Need whole course records to be used by add_instance(), use inner view (ci) to
302 // get distinct records only.
303 // TODO: Moodle 2.1. Improve enrol API to accept courseid / courserec
309 JOIN {context} ctx ON (ctx.instanceid = c.id AND ctx.contextlevel = :courselevel)
310 JOIN (SELECT DISTINCT cctx.path
311 FROM {course_categories} cc
312 JOIN {context} cctx ON (cctx.instanceid = cc.id AND cctx.contextlevel = :catlevel)
313 JOIN {role_assignments} ra ON (ra.contextid = cctx.id AND ra.roleid $roleids)
314 ) cat ON (ctx.path LIKE $parentcat)
315 LEFT JOIN {enrol} e ON (e.courseid = c.id AND e.enrol = 'category')
316 WHERE e.id IS NULL) ci ON (c.id = ci.id)";
318 $rs = $DB->get_recordset_sql($sql, $params);
319 foreach($rs as $course) {
320 $plugin->add_instance($course);
324 // Now look for courses that do not have any interesting roles in parent contexts,
325 // but still have the instance and delete them.
328 JOIN {context} ctx ON (ctx.instanceid = e.courseid AND ctx.contextlevel = :courselevel)
329 LEFT JOIN ({course_categories} cc
330 JOIN {context} cctx ON (cctx.instanceid = cc.id AND cctx.contextlevel = :catlevel)
331 JOIN {role_assignments} ra ON (ra.contextid = cctx.id AND ra.roleid $roleids)
332 ) ON (ctx.path LIKE $parentcctx)
333 WHERE e.enrol = 'category' AND cc.id IS NULL";
335 $rs = $DB->get_recordset_sql($sql, $params);
336 foreach($rs as $instance) {
337 $plugin->delete_instance($instance);
341 // Add missing enrolments.
342 $sql = "SELECT e.*, cat.userid, cat.estart
344 JOIN {context} ctx ON (ctx.instanceid = e.courseid AND ctx.contextlevel = :courselevel)
345 JOIN (SELECT cctx.path, ra.userid, MIN(ra.timemodified) AS estart
346 FROM {course_categories} cc
347 JOIN {context} cctx ON (cctx.instanceid = cc.id AND cctx.contextlevel = :catlevel)
348 JOIN {role_assignments} ra ON (ra.contextid = cctx.id AND ra.roleid $roleids)
349 GROUP BY cctx.path, ra.userid
350 ) cat ON (ctx.path LIKE $parentcat)
351 LEFT JOIN {user_enrolments} ue ON (ue.enrolid = e.id AND ue.userid = cat.userid)
352 WHERE e.enrol = 'category' AND ue.id IS NULL";
353 $rs = $DB->get_recordset_sql($sql, $params);
354 foreach($rs as $instance) {
355 $userid = $instance->userid;
356 $estart = $instance->estart;
357 unset($instance->userid);
358 unset($instance->estart);
359 $plugin->enrol_user($instance, $userid, null, $estart);
361 mtrace(" enrolling: user $userid ==> course $instance->courseid");
366 // Remove stale enrolments.
367 $sql = "SELECT e.*, ue.userid
369 JOIN {context} ctx ON (ctx.instanceid = e.courseid AND ctx.contextlevel = :courselevel)
370 JOIN {user_enrolments} ue ON (ue.enrolid = e.id)
371 LEFT JOIN ({course_categories} cc
372 JOIN {context} cctx ON (cctx.instanceid = cc.id AND cctx.contextlevel = :catlevel)
373 JOIN {role_assignments} ra ON (ra.contextid = cctx.id AND ra.roleid $roleids)
374 ) ON (ctx.path LIKE $parentcctx AND ra.userid = ue.userid)
375 WHERE e.enrol = 'category' AND cc.id IS NULL";
376 $rs = $DB->get_recordset_sql($sql, $params);
377 foreach($rs as $instance) {
378 $userid = $instance->userid;
379 unset($instance->userid);
380 $plugin->unenrol_user($instance, $userid);
382 mtrace(" unenrolling: user $userid ==> course $instance->courseid");
388 mtrace('...user enrolment synchronisation finished.');