MDL-34864 improve enrol_category phpdocs
[moodle.git] / enrol / category / locallib.php
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/>.
17 /**
18  * Local stuff for category enrolment plugin.
19  *
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
23  */
25 defined('MOODLE_INTERNAL') || die();
28 /**
29  * Event handler for category enrolment plugin.
30  *
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.
33  */
34 class enrol_category_handler {
35     /**
36      * Triggered when user is assigned a new role.
37      * @static
38      * @param stdClass $ra
39      * @return bool
40      */
41     public static function role_assigned($ra) {
42         global $DB;
44         if (!enrol_is_enabled('category')) {
45             return true;
46         }
48         // Only category level roles are interesting.
49         $parentcontext = get_context_instance_by_id($ra->contextid);
50         if ($parentcontext->contextlevel != CONTEXT_COURSECAT) {
51             return true;
52         }
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))) {
58             return true;
59         }
61         // Add necessary enrol instances.
62         $plugin = enrol_get_plugin('category');
63         $sql = "SELECT c.*
64                   FROM {course} c
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')
67                  WHERE e.id IS NULL";
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);
72         }
73         $rs->close();
75         // Now look for missing enrolments.
76         $sql = "SELECT e.*
77                   FROM {course} c
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)
81                  WHERE ue.id IS NULL";
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);
86         }
87         $rs->close();
89         return true;
90     }
92     /**
93      * Triggered when user role is unassigned.
94      * @static
95      * @param stdClass $ra
96      * @return bool
97      */
98     public static function role_unassigned($ra) {
99         global $DB;
101         if (!enrol_is_enabled('category')) {
102             return true;
103         }
105         // Only category level roles are interesting.
106         $parentcontext = get_context_instance_by_id($ra->contextid);
107         if ($parentcontext->contextlevel != CONTEXT_COURSECAT) {
108             return true;
109         }
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)) {
114             return true;
115         }
117         $plugin = enrol_get_plugin('category');
119         $sql = "SELECT e.*
120                   FROM {course} c
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);
138             $sql = "SELECT ra.id
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);
144             }
145         }
146         $rs->close();
148         return true;
149     }
152 /**
153  * Sync all category enrolments in one course
154  * @param stdClass $course
155  * @return void
156  */
157 function enrol_category_sync_course($course) {
158     global $DB;
160     if (!enrol_is_enabled('category')) {
161         return;
162     }
164     $plugin = enrol_get_plugin('category');
166     $syscontext = context_system::instance();
167     $roles = get_roles_with_capability('enrol/category:synchronised', CAP_ALLOW, $syscontext);
169     if (!$roles) {
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);
174             }
175         }
176         return;
177     }
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;
189     $sql = "SELECT 'x'
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);
197             }
198         }
199         return;
200     }
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;
207     } else {
208         $i = $plugin->add_instance($course);
209         $instance = $DB->get_record('enrol', array('id'=>$i));
210     }
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
217                   GROUP BY xra.userid
218                    ) ra
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);
225     }
226     $rs->close();
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);
236     }
237     $rs->close();
239     if ($delinstances) {
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);
243         }
244     }
247 /**
248  * Synchronise courses in all categories.
249  *
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
254  *
255  * @param bool $verbose
256  * @return int exit code - 0 is ok, 1 means error, 2 if plugin disabled
257  */
258 function enrol_category_sync_full($verbose = false) {
259     global $DB;
262     if (!enrol_is_enabled('category')) {
263         return 2;
264     }
266     // We may need a lot of time here.
267     @set_time_limit(0);
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
276         if ($verbose) {
277             mtrace("No roles with 'enrol/category:synchronised' capability found.");
278         }
279         if ($instances = $DB->get_records('enrol', array('enrol'=>'category'))) {
280             foreach ($instances as $instance) {
281                 if ($verbose) {
282                     mtrace("  deleting category enrol instance from course {$instance->courseid}");
283                 }
284                 $plugin->delete_instance($instance);
285             }
286         }
287         return 0;
288     }
289     $rolenames = role_fix_names($roles, null, ROLENAME_SHORT, true);
290     if ($verbose) {
291         mtrace('Synchronising category enrolments for roles: '.implode(', ', $rolenames).'...');
292     }
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
304     $sql = "SELECT c.*
305               FROM {course} c
306               JOIN (
307                 SELECT DISTINCT c.id
308                   FROM {course} c
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);
321     }
322     $rs->close();
324     // Now look for courses that do not have any interesting roles in parent contexts,
325     // but still have the instance and delete them.
326     $sql = "SELECT e.*
327               FROM {enrol} e
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);
338     }
339     $rs->close();
341     // Add missing enrolments.
342     $sql = "SELECT e.*, cat.userid, cat.estart
343               FROM {enrol} e
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);
360         if ($verbose) {
361             mtrace("  enrolling: user $userid ==> course $instance->courseid");
362         }
363     }
364     $rs->close();
366     // Remove stale enrolments.
367     $sql = "SELECT e.*, ue.userid
368               FROM {enrol} e
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);
381         if ($verbose) {
382             mtrace("  unenrolling: user $userid ==> course $instance->courseid");
383         }
384     }
385     $rs->close();
387     if ($verbose) {
388         mtrace('...user enrolment synchronisation finished.');
389     }
391     return 0;