4cd7c5c8dc398ddd47b5a3087995efd57d70e8f1
[moodle.git] / enrol / category / locallib.php
1 <?php
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/>.
18 /**
19  * Local stuff for category enrolment plugin.
20  *
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
25  */
27 defined('MOODLE_INTERNAL') || die();
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  */
35 class enrol_category_handler {
36     public static function role_assigned($ra) {
37         global $DB;
39         if (!enrol_is_enabled('category')) {
40             return true;
41         }
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         }
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         }
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();
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) {
80             $plugin->enrol_user($instance, $ra->userid, null, $ra->timemodified);
81         }
82         $rs->close();
84         return true;
85     }
87     public static function role_unassigned($ra) {
88         global $DB;
90         if (!enrol_is_enabled('category')) {
91             return true;
92         }
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         }
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);
102         if (!$roles = get_roles_with_capability('enrol/category:synchronised', CAP_ALLOW, $syscontext)) {
103             return true;
104         }
106         $plugin = enrol_get_plugin('category');
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);
116         list($roleids, $params) = $DB->get_in_or_equal(array_keys($roles), SQL_PARAMS_NAMED, 'r');
117         $params['userid'] = $ra->userid;
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
124             list($contextids, $contextparams) = $DB->get_in_or_equal($contextids, SQL_PARAMS_NAMED, 'c');
125             $params = array_merge($params, $contextparams);
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();
137         return true;
138     }
141 /**
142  * Sync all category enrolments in one course
143  * @param int $courseid course id
144  * @return void
145  */
146 function enrol_category_sync_course($course) {
147     global $DB;
149     if (!enrol_is_enabled('category')) {
150         return;
151     }
153     $plugin = enrol_get_plugin('category');
155     $syscontext = get_context_instance(CONTEXT_SYSTEM);
156     $roles = get_roles_with_capability('enrol/category:synchronised', CAP_ALLOW, $syscontext);
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     }
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
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');
175     $params = array_merge($params, $contextparams);
176     $params['courseid'] = $course->id;
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     }
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     }
201     // add new enrolments
202     $sql = "SELECT ra.userid, ra.estart
203               FROM (SELECT xra.userid, MIN(xra.timemodified) AS estart
204                       FROM {role_assignments} xra
205                      WHERE xra.roleid $roleids AND xra.contextid $contextids
206                   GROUP BY xra.userid
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) {
213         $plugin->enrol_user($instance, $ra->userid, null, $ra->estart);
214     }
215     $rs->close();
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();
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     }
236 function enrol_category_sync_full($verbose = false) {
237     global $DB;
240     if (!enrol_is_enabled('category')) {
241         return 2;
242     }
244     // we may need a lot of time here
245     @set_time_limit(0);
247     $plugin = enrol_get_plugin('category');
249     $syscontext = get_context_instance(CONTEXT_SYSTEM);
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
254         if ($verbose) {
255             mtrace("No roles with 'enrol/category:synchronised' capability found.");
256         }
257         if ($instances = $DB->get_records('enrol', array('enrol'=>'category'))) {
258             foreach ($instances as $instance) {
259                 if ($verbose) {
260                     mtrace("  deleting category enrol instance from course {$instance->courseid}");
261                 }
262                 $plugin->delete_instance($instance);
263             }
264         }
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).'...');
270     }
272     list($roleids, $params) = $DB->get_in_or_equal(array_keys($roles), SQL_PARAMS_NAMED, 'r');
273     $params['courselevel'] = CONTEXT_COURSE;
274     $params['catlevel'] = CONTEXT_COURSECAT;
276     // first of all add necessary enrol instances to all courses
277     $parentcat = $DB->sql_concat("cat.path", "'/%'");
278     $parentcctx = $DB->sql_concat("cctx.path", "'/%'");
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.*
283               FROM {course} c
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)";
296     $rs = $DB->get_recordset_sql($sql, $params);
297     foreach($rs as $course) {
298         $plugin->add_instance($course);
299     }
300     $rs->close();
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)
307          LEFT JOIN ({course_categories} cc
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)
310                    ) ON (ctx.path LIKE $parentcctx)
311              WHERE e.enrol = 'category' AND cc.id IS NULL";
313     $rs = $DB->get_recordset_sql($sql, $params);
314     foreach($rs as $instance) {
315         $plugin->delete_instance($instance);
316     }
317     $rs->close();
319     // add missing enrolments
320     $sql = "SELECT e.*, cat.userid, cat.estart
321               FROM {enrol} e
322               JOIN {context} ctx ON (ctx.instanceid = e.courseid AND ctx.contextlevel = :courselevel)
323               JOIN (SELECT cctx.path, ra.userid, MIN(ra.timemodified) AS estart
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)
327                   GROUP BY cctx.path, ra.userid
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;
334         $estart = $instance->estart;
335         unset($instance->userid);
336         unset($instance->estart);
337         $plugin->enrol_user($instance, $userid, null, $estart);
338         if ($verbose) {
339             mtrace("  enrolling: user $userid ==> course $instance->courseid");
340         }
341     }
342     $rs->close();
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)
349          LEFT JOIN ({course_categories} cc
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)
352                    ) ON (ctx.path LIKE $parentcctx AND ra.userid = ue.userid)
353              WHERE e.enrol = 'category' AND cc.id IS NULL";
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);
359         if ($verbose) {
360             mtrace("  unenrolling: user $userid ==> course $instance->courseid");
361         }
362     }
363     $rs->close();
365     if ($verbose) {
366         mtrace('...user enrolment synchronisation finished.');
367     }
369     return 0;