MDL-34864 add verbose option to CLI enrol_category sync
[moodle.git] / enrol / category / locallib.php
CommitLineData
df997f84
PS
1<?php
2
3// This file is part of Moodle - http://moodle.org/
4//
5// Moodle is free software: you can redistribute it and/or modify
6// it under the terms of the GNU General Public License as published by
7// the Free Software Foundation, either version 3 of the License, or
8// (at your option) any later version.
9//
10// Moodle is distributed in the hope that it will be useful,
11// but WITHOUT ANY WARRANTY; without even the implied warranty of
12// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13// GNU General Public License for more details.
14//
15// You should have received a copy of the GNU General Public License
16// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
17
18/**
19 * Local stuff for category enrolment plugin.
20 *
fe441933
PS
21 * @package enrol
22 * @subpackage category
23 * @copyright 2010 Petr Skoda {@link http://skodak.org}
24 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
df997f84
PS
25 */
26
97795859 27defined('MOODLE_INTERNAL') || die();
df997f84
PS
28
29/**
30 * Event handler for category enrolment plugin.
31 *
32 * We try to keep everything in sync via listening to events,
33 * it may fail sometimes, so we always do a full sync in cron too.
34 */
35class enrol_category_handler {
e807c0be 36 public static function role_assigned($ra) {
df997f84
PS
37 global $DB;
38
39 if (!enrol_is_enabled('category')) {
40 return true;
41 }
42
43 //only category level roles are interesting
44 $parentcontext = get_context_instance_by_id($ra->contextid);
45 if ($parentcontext->contextlevel != CONTEXT_COURSECAT) {
46 return true;
47 }
48
49 // make sure the role is to be actually synchronised
50 // please note we are ignoring overrides of the synchronised capability (for performance reasons in full sync)
51 $syscontext = get_context_instance(CONTEXT_SYSTEM);
52 if (!$DB->record_exists('role_capabilities', array('contextid'=>$syscontext->id, 'roleid'=>$ra->roleid, 'capability'=>'enrol/category:synchronised', 'permission'=>CAP_ALLOW))) {
53 return true;
54 }
55
56 // add necessary enrol instances
57 $plugin = enrol_get_plugin('category');
58 $sql = "SELECT c.*
59 FROM {course} c
60 JOIN {context} ctx ON (ctx.instanceid = c.id AND ctx.contextlevel = :courselevel AND ctx.path LIKE :match)
61 LEFT JOIN {enrol} e ON (e.courseid = c.id AND e.enrol = 'category')
62 WHERE e.id IS NULL";
63 $params = array('courselevel'=>CONTEXT_COURSE, 'match'=>$parentcontext->path.'/%');
64 $rs = $DB->get_recordset_sql($sql, $params);
65 foreach ($rs as $course) {
66 $plugin->add_instance($course);
67 }
68 $rs->close();
69
70 // now look for missing enrols
71 $sql = "SELECT e.*
72 FROM {course} c
73 JOIN {context} ctx ON (ctx.instanceid = c.id AND ctx.contextlevel = :courselevel AND ctx.path LIKE :match)
74 JOIN {enrol} e ON (e.courseid = c.id AND e.enrol = 'category')
75 LEFT JOIN {user_enrolments} ue ON (ue.enrolid = e.id AND ue.userid = :userid)
76 WHERE ue.id IS NULL";
77 $params = array('courselevel'=>CONTEXT_COURSE, 'match'=>$parentcontext->path.'/%', 'userid'=>$ra->userid);
78 $rs = $DB->get_recordset_sql($sql, $params);
79 foreach ($rs as $instance) {
2a6dcb72 80 $plugin->enrol_user($instance, $ra->userid, null, $ra->timemodified);
df997f84
PS
81 }
82 $rs->close();
83
84 return true;
85 }
86
e807c0be 87 public static function role_unassigned($ra) {
df997f84
PS
88 global $DB;
89
90 if (!enrol_is_enabled('category')) {
91 return true;
92 }
93
94 // only category level roles are interesting
95 $parentcontext = get_context_instance_by_id($ra->contextid);
96 if ($parentcontext->contextlevel != CONTEXT_COURSECAT) {
97 return true;
98 }
99
100 // now this is going to be a bit slow, take all enrolments in child courses and verify each separately
101 $syscontext = get_context_instance(CONTEXT_SYSTEM);
c2d6b4f9
PS
102 if (!$roles = get_roles_with_capability('enrol/category:synchronised', CAP_ALLOW, $syscontext)) {
103 return true;
104 }
df997f84
PS
105
106 $plugin = enrol_get_plugin('category');
107
108 $sql = "SELECT e.*
109 FROM {course} c
110 JOIN {context} ctx ON (ctx.instanceid = c.id AND ctx.contextlevel = :courselevel AND ctx.path LIKE :match)
111 JOIN {enrol} e ON (e.courseid = c.id AND e.enrol = 'category')
112 JOIN {user_enrolments} ue ON (ue.enrolid = e.id AND ue.userid = :userid)";
113 $params = array('courselevel'=>CONTEXT_COURSE, 'match'=>$parentcontext->path.'/%', 'userid'=>$ra->userid);
114 $rs = $DB->get_recordset_sql($sql, $params);
115
cf717dc2 116 list($roleids, $params) = $DB->get_in_or_equal(array_keys($roles), SQL_PARAMS_NAMED, 'r');
df997f84
PS
117 $params['userid'] = $ra->userid;
118
119 foreach ($rs as $instance) {
120 $coursecontext = get_context_instance(CONTEXT_COURSE, $instance->courseid);
121 $contextids = get_parent_contexts($coursecontext);
122 array_pop($contextids); // remove system context, we are interested in categories only
123
cf717dc2 124 list($contextids, $contextparams) = $DB->get_in_or_equal($contextids, SQL_PARAMS_NAMED, 'c');
df997f84
PS
125 $params = array_merge($params, $contextparams);
126
127 $sql = "SELECT ra.id
128 FROM {role_assignments} ra
129 WHERE ra.userid = :userid AND ra.contextid $contextids AND ra.roleid $roleids";
130 if (!$DB->record_exists_sql($sql, $params)) {
131 // user does not have any interesting role in any parent context, let's unenrol
132 $plugin->unenrol_user($instance, $ra->userid);
133 }
134 }
135 $rs->close();
136
137 return true;
138 }
139}
140
141/**
142 * Sync all category enrolments in one course
143 * @param int $courseid course id
144 * @return void
145 */
146function enrol_category_sync_course($course) {
147 global $DB;
148
149 if (!enrol_is_enabled('category')) {
150 return;
151 }
152
153 $plugin = enrol_get_plugin('category');
154
155 $syscontext = get_context_instance(CONTEXT_SYSTEM);
156 $roles = get_roles_with_capability('enrol/category:synchronised', CAP_ALLOW, $syscontext);
157
ff3af0a5
PS
158 if (!$roles) {
159 //nothing to sync, so remove the instance completely if exists
160 if ($instances = $DB->get_records('enrol', array('courseid'=>$course->id, 'enrol'=>'category'))) {
161 foreach ($instances as $instance) {
162 $plugin->delete_instance($instance);
163 }
164 }
165 return;
166 }
167
df997f84
PS
168 // first find out if any parent category context contains interesting role assignments
169 $coursecontext = get_context_instance(CONTEXT_COURSE, $course->id);
170 $contextids = get_parent_contexts($coursecontext);
171 array_pop($contextids); // remove system context, we are interested in categories only
172
cf717dc2
PS
173 list($roleids, $params) = $DB->get_in_or_equal(array_keys($roles), SQL_PARAMS_NAMED, 'r');
174 list($contextids, $contextparams) = $DB->get_in_or_equal($contextids, SQL_PARAMS_NAMED, 'c');
df997f84
PS
175 $params = array_merge($params, $contextparams);
176 $params['courseid'] = $course->id;
177
178 $sql = "SELECT 'x'
179 FROM {role_assignments}
180 WHERE roleid $roleids AND contextid $contextids";
181 if (!$DB->record_exists_sql($sql, $params)) {
182 if ($instances = $DB->get_records('enrol', array('courseid'=>$course->id, 'enrol'=>'category'))) {
183 // should be max one instance, but anyway
184 foreach ($instances as $instance) {
185 $plugin->delete_instance($instance);
186 }
187 }
188 return;
189 }
190
191 // make sure the enrol instance exists - there should be always only one instance
192 $delinstances = array();
193 if ($instances = $DB->get_records('enrol', array('courseid'=>$course->id, 'enrol'=>'category'))) {
194 $instance = array_shift($instances);
195 $delinstances = $instances;
196 } else {
197 $i = $plugin->add_instance($course);
198 $instance = $DB->get_record('enrol', array('id'=>$i));
199 }
200
201 // add new enrolments
2a6dcb72
PS
202 $sql = "SELECT ra.userid, ra.estart
203 FROM (SELECT xra.userid, MIN(xra.timemodified) AS estart
df997f84
PS
204 FROM {role_assignments} xra
205 WHERE xra.roleid $roleids AND xra.contextid $contextids
2a6dcb72 206 GROUP BY xra.userid
df997f84
PS
207 ) ra
208 LEFT JOIN {user_enrolments} ue ON (ue.enrolid = :instanceid AND ue.userid = ra.userid)
209 WHERE ue.id IS NULL";
210 $params['instanceid'] = $instance->id;
211 $rs = $DB->get_recordset_sql($sql, $params);
212 foreach ($rs as $ra) {
2a6dcb72 213 $plugin->enrol_user($instance, $ra->userid, null, $ra->estart);
df997f84
PS
214 }
215 $rs->close();
216
217 // remove unwanted enrolments
218 $sql = "SELECT DISTINCT ue.userid
219 FROM {user_enrolments} ue
220 LEFT JOIN {role_assignments} ra ON (ra.roleid $roleids AND ra.contextid $contextids AND ra.userid = ue.userid)
221 WHERE ue.enrolid = :instanceid AND ra.id IS NULL";
222 $rs = $DB->get_recordset_sql($sql, $params);
223 foreach ($rs as $ra) {
224 $plugin->unenrol_user($instance, $ra->userid);
225 }
226 $rs->close();
227
228 if ($delinstances) {
229 // we have to do this as the last step in order to prevent temporary unenrolment
230 foreach ($delinstances as $delinstance) {
231 $plugin->delete_instance($delinstance);
232 }
233 }
234}
235
a2043a31 236function enrol_category_sync_full($verbose = false) {
df997f84
PS
237 global $DB;
238
239
240 if (!enrol_is_enabled('category')) {
a2043a31 241 return 2;
df997f84
PS
242 }
243
244 // we may need a lot of time here
245 @set_time_limit(0);
246
247 $plugin = enrol_get_plugin('category');
248
249 $syscontext = get_context_instance(CONTEXT_SYSTEM);
250
251 // any interesting roles worth synchronising?
252 if (!$roles = get_roles_with_capability('enrol/category:synchronised', CAP_ALLOW, $syscontext)) {
253 // yay, nothing to do, so let's remove all leftovers
a2043a31
PS
254 if ($verbose) {
255 mtrace("No roles with 'enrol/category:synchronised' capability found.");
256 }
df997f84 257 if ($instances = $DB->get_records('enrol', array('enrol'=>'category'))) {
ac86edd0 258 foreach ($instances as $instance) {
a2043a31
PS
259 if ($verbose) {
260 mtrace(" deleting category enrol instance from course {$instance->courseid}");
261 }
ac86edd0
PS
262 $plugin->delete_instance($instance);
263 }
df997f84 264 }
a2043a31
PS
265 return 0;
266 }
267 $rolenames = role_fix_names($roles, null, ROLENAME_SHORT, true);
268 if ($verbose) {
269 mtrace('Synchronising category enrolments for roles: '.implode(', ', $rolenames).'...');
df997f84
PS
270 }
271
cf717dc2 272 list($roleids, $params) = $DB->get_in_or_equal(array_keys($roles), SQL_PARAMS_NAMED, 'r');
df997f84
PS
273 $params['courselevel'] = CONTEXT_COURSE;
274 $params['catlevel'] = CONTEXT_COURSECAT;
275
034ef761 276 // first of all add necessary enrol instances to all courses
df997f84 277 $parentcat = $DB->sql_concat("cat.path", "'/%'");
3d7f23c5 278 $parentcctx = $DB->sql_concat("cctx.path", "'/%'");
f320cb76
EL
279 // need whole course records to be used by add_instance(), use inner view (ci) to
280 // get distinct records only.
281 // TODO: Moodle 2.1. Improve enrol API to accept courseid / courserec
282 $sql = "SELECT c.*
df997f84 283 FROM {course} c
f320cb76
EL
284 JOIN (
285 SELECT DISTINCT c.id
286 FROM {course} c
287 JOIN {context} ctx ON (ctx.instanceid = c.id AND ctx.contextlevel = :courselevel)
288 JOIN (SELECT DISTINCT cctx.path
289 FROM {course_categories} cc
290 JOIN {context} cctx ON (cctx.instanceid = cc.id AND cctx.contextlevel = :catlevel)
291 JOIN {role_assignments} ra ON (ra.contextid = cctx.id AND ra.roleid $roleids)
292 ) cat ON (ctx.path LIKE $parentcat)
293 LEFT JOIN {enrol} e ON (e.courseid = c.id AND e.enrol = 'category')
294 WHERE e.id IS NULL) ci ON (c.id = ci.id)";
df997f84
PS
295
296 $rs = $DB->get_recordset_sql($sql, $params);
297 foreach($rs as $course) {
298 $plugin->add_instance($course);
299 }
300 $rs->close();
301
302 // now look for courses that do not have any interesting roles in parent contexts,
303 // but still have the instance and delete them
304 $sql = "SELECT e.*
305 FROM {enrol} e
306 JOIN {context} ctx ON (ctx.instanceid = e.courseid AND ctx.contextlevel = :courselevel)
3d7f23c5 307 LEFT JOIN ({course_categories} cc
df997f84
PS
308 JOIN {context} cctx ON (cctx.instanceid = cc.id AND cctx.contextlevel = :catlevel)
309 JOIN {role_assignments} ra ON (ra.contextid = cctx.id AND ra.roleid $roleids)
3d7f23c5
PS
310 ) ON (ctx.path LIKE $parentcctx)
311 WHERE e.enrol = 'category' AND cc.id IS NULL";
df997f84
PS
312
313 $rs = $DB->get_recordset_sql($sql, $params);
314 foreach($rs as $instance) {
315 $plugin->delete_instance($instance);
316 }
317 $rs->close();
318
319 // add missing enrolments
2a6dcb72 320 $sql = "SELECT e.*, cat.userid, cat.estart
df997f84
PS
321 FROM {enrol} e
322 JOIN {context} ctx ON (ctx.instanceid = e.courseid AND ctx.contextlevel = :courselevel)
2a6dcb72 323 JOIN (SELECT cctx.path, ra.userid, MIN(ra.timemodified) AS estart
df997f84
PS
324 FROM {course_categories} cc
325 JOIN {context} cctx ON (cctx.instanceid = cc.id AND cctx.contextlevel = :catlevel)
326 JOIN {role_assignments} ra ON (ra.contextid = cctx.id AND ra.roleid $roleids)
2a6dcb72 327 GROUP BY cctx.path, ra.userid
df997f84
PS
328 ) cat ON (ctx.path LIKE $parentcat)
329 LEFT JOIN {user_enrolments} ue ON (ue.enrolid = e.id AND ue.userid = cat.userid)
330 WHERE e.enrol = 'category' AND ue.id IS NULL";
331 $rs = $DB->get_recordset_sql($sql, $params);
332 foreach($rs as $instance) {
333 $userid = $instance->userid;
2a6dcb72 334 $estart = $instance->estart;
df997f84 335 unset($instance->userid);
2a6dcb72
PS
336 unset($instance->estart);
337 $plugin->enrol_user($instance, $userid, null, $estart);
a2043a31
PS
338 if ($verbose) {
339 mtrace(" enrolling: user $userid ==> course $instance->courseid");
340 }
df997f84
PS
341 }
342 $rs->close();
343
344 // remove stale enrolments
345 $sql = "SELECT e.*, ue.userid
346 FROM {enrol} e
347 JOIN {context} ctx ON (ctx.instanceid = e.courseid AND ctx.contextlevel = :courselevel)
348 JOIN {user_enrolments} ue ON (ue.enrolid = e.id)
3d7f23c5 349 LEFT JOIN ({course_categories} cc
df997f84
PS
350 JOIN {context} cctx ON (cctx.instanceid = cc.id AND cctx.contextlevel = :catlevel)
351 JOIN {role_assignments} ra ON (ra.contextid = cctx.id AND ra.roleid $roleids)
3d7f23c5
PS
352 ) ON (ctx.path LIKE $parentcctx AND ra.userid = ue.userid)
353 WHERE e.enrol = 'category' AND cc.id IS NULL";
df997f84
PS
354 $rs = $DB->get_recordset_sql($sql, $params);
355 foreach($rs as $instance) {
356 $userid = $instance->userid;
357 unset($instance->userid);
358 $plugin->unenrol_user($instance, $userid);
a2043a31
PS
359 if ($verbose) {
360 mtrace(" unenrolling: user $userid ==> course $instance->courseid");
361 }
df997f84
PS
362 }
363 $rs->close();
a2043a31
PS
364
365 if ($verbose) {
366 mtrace('...user enrolment synchronisation finished.');
367 }
368
369 return 0;
df997f84 370}