MDL-29688 add support for status sync in enrol meta plugin
[moodle.git] / enrol / meta / 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 meta course enrolment plugin.
19  *
20  * @package    enrol
21  * @subpackage meta
22  * @copyright  2010 Petr Skoda {@link http://skodak.org}
23  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
24  */
26 defined('MOODLE_INTERNAL') || die();
28 /**
29  * Event handler for meta 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_meta_handler {
35     public function role_assigned($ra) {
36         global $DB;
38         if (!enrol_is_enabled('meta')) {
39             return true;
40         }
42         // prevent circular dependencies - we can not sync meta roles recursively
43         if ($ra->component === 'enrol_meta') {
44             return true;
45         }
47         // only course level roles are interesting
48         $parentcontext = get_context_instance_by_id($ra->contextid);
49         if ($parentcontext->contextlevel != CONTEXT_COURSE) {
50             return true;
51         }
53         // does anything want to sync with this parent?
54         if (!$enrols = $DB->get_records('enrol', array('customint1'=>$parentcontext->instanceid, 'enrol'=>'meta'), 'id ASC')) {
55             return true;
56         }
58         // make sure the role sync is not prevented
59         $plugin = enrol_get_plugin('meta');
60         if ($disabled = $plugin->get_config('nosyncroleids')) {
61             if (in_array($ra->roleid, explode(',', $disabled))) {
62                 return true;
63             }
64         }
66         foreach ($enrols as $enrol) {
67             // Is the user enrolled? We want to sync only really enrolled users
68             if (!$DB->record_exists('user_enrolments', array('userid'=>$ra->userid, 'enrolid'=>$enrol->id))) {
69                 continue;
70             }
71             $context = get_context_instance(CONTEXT_COURSE, $enrol->courseid);
73             // just try to assign role, no problem if role assignment already exists
74             role_assign($ra->roleid, $ra->userid, $context->id, 'enrol_meta', $enrol->id);
75         }
77         return true;
78     }
80     public function role_unassigned($ra) {
81         global $DB;
83         // note: do not test if plugin enabled, we want to keep removing previous roles
85         // prevent circular dependencies - we can not sync meta roles recursively
86         if ($ra->component === 'enrol_meta') {
87             return true;
88         }
90         // only course level roles are interesting
91         $parentcontext = get_context_instance_by_id($ra->contextid);
92         if ($parentcontext->contextlevel != CONTEXT_COURSE) {
93             return true;
94         }
96         // does anything want to sync with this parent?
97         if (!$enrols = $DB->get_records('enrol', array('customint1'=>$parentcontext->instanceid, 'enrol'=>'meta'), 'id ASC')) {
98             return true;
99         }
101         // note: do not check 'nosyncroleids', somebody might have just enabled it, we want to get rid of nosync roles gradually
103         foreach ($enrols as $enrol) {
104             // Is the user enrolled? We want to sync only really enrolled users
105             if (!$DB->record_exists('user_enrolments', array('userid'=>$ra->userid, 'enrolid'=>$enrol->id))) {
106                 continue;
107             }
108             $context = get_context_instance(CONTEXT_COURSE, $enrol->courseid);
110             // now make sure the user does not have the role through some other enrol plugin
111             $params = array('contextid'=>$ra->contextid, 'roleid'=>$ra->roleid, 'userid'=>$ra->userid);
112             if ($DB->record_exists_select('role_assignments', "contextid = :contextid AND roleid = :roleid AND userid = :userid AND component <> 'enrol_meta'", $params)) {
113                 continue;
114             }
116             // unassign role, there is no other role assignment in parent course
117             role_unassign($ra->roleid, $ra->userid, $context->id, 'enrol_meta', $enrol->id);
118         }
120         return true;
121     }
123     public function user_enrolled($ue) {
124         global $DB;
126         if (!enrol_is_enabled('meta')) {
127             return true;
128         }
130         if ($ue->enrol === 'meta') {
131             // prevent circular dependencies - we can not sync meta enrolments recursively
132             return true;
133         }
135         // does anything want to sync with this parent?
136         if (!$enrols = $DB->get_records('enrol', array('customint1'=>$ue->courseid, 'enrol'=>'meta'), 'id ASC')) {
137             return true;
138         }
140         $plugin = enrol_get_plugin('meta');
141         foreach ($enrols as $enrol) {
142             if ($ue->status == ENROL_USER_ACTIVE) {
143                 $status = ENROL_USER_ACTIVE;
144             } else {
145                 $context = get_context_instance(CONTEXT_COURSE, $enrol->courseid);
146                 if (is_enrolled($context, $ue->userid)) {
147                     // user already has active enrolment, do not change it
148                     $status = ENROL_USER_ACTIVE;
149                 } else {
150                     $status = $ue->status;
151                 }
153             }
154             // no problem if already enrolled
155             $plugin->enrol_user($enrol, $ue->userid, $status);
156         }
158         return true;
159     }
161     public function user_unenrolled($ue) {
162         global $DB;
164         //note: do not test if plugin enabled, we want to keep removing previously linked courses
166         // look for unenrolment candidates - it may be possible that user has multiple enrolments...
167         $sql = "SELECT e.*
168                   FROM {enrol} e
169                   JOIN {user_enrolments} ue ON (ue.enrolid = e.id AND ue.userid = :userid)
170                   JOIN {enrol} pe ON (pe.courseid = e.customint1 AND pe.enrol <> 'meta' AND pe.courseid = :courseid)
171              LEFT JOIN {user_enrolments} pue ON (pue.enrolid = pe.id AND pue.userid = ue.userid)
172                  WHERE pue.id IS NULL AND e.enrol = 'meta'";
173         $params = array('courseid'=>$ue->courseid, 'userid'=>$ue->userid);
175         $rs = $DB->get_recordset_sql($sql, $params);
177         $plugin = enrol_get_plugin('meta');
178         foreach ($rs as $enrol) {
179             $plugin->unenrol_user($enrol, $ue->userid);
180         }
181         $rs->close();
183         return true;
184     }
186     public function user_enrol_modified($ue) {
187         global $DB;
189         // update enrolment status if necessary
191         if (!enrol_is_enabled('meta')) {
192             return true;
193         }
195         if ($ue->enrol === 'meta') {
196             // prevent circular dependencies - we can not sync meta enrolments recursively
197             return true;
198         }
200         // does anything want to sync with this parent?
201         if (!$enrols = $DB->get_records('enrol', array('customint1'=>$ue->courseid, 'enrol'=>'meta'), 'id ASC')) {
202             return true;
203         }
205         $plugin = enrol_get_plugin('meta');
206         foreach ($enrols as $enrol) {
207             $plugin->update_user_enrol($enrol, $ue->userid, $ue->status);
208         }
210         return true;
211     }
213     public function course_deleted($course) {
214         global $DB;
216         // note: do not test if plugin enabled, we want to keep removing previously linked courses
218         // does anything want to sync with this parent?
219         if (!$enrols = $DB->get_records('enrol', array('customint1'=>$course->id, 'enrol'=>'meta'), 'id ASC')) {
220             return true;
221         }
223         $plugin = enrol_get_plugin('meta');
224         foreach ($enrols as $enrol) {
225             // unenrol all users
226             $ues = $DB->get_recordset('user_enrolments', array('enrolid'=>$enrol->id));
227             foreach ($ues as $ue) {
228                 $plugin->unenrol_user($enrol, $ue->userid);
229             }
230             $ues->close();
231         }
233         return true;
234     }
238 /**
239  * Sync all meta course links.
240  * @param int $courseid one course, empty mean all
241  * @return void
242  */
243 function enrol_meta_sync($courseid = NULL) {
244     global $CFG, $DB;
246     // unfortunately this may take a loooong time
247     @set_time_limit(0); //if this fails during upgrade we can continue from cron, no big deal
249     $instances = array(); //cache
251     $meta = enrol_get_plugin('meta');
253     $onecourse = $courseid ? "AND e.courseid = :courseid" : "";
255     // iterate through all not enrolled yet users
256     if (enrol_is_enabled('meta')) {
257         list($enabled, $params) = $DB->get_in_or_equal(explode(',', $CFG->enrol_plugins_enabled), SQL_PARAMS_NAMED, 'e');
258         if ($courseid) {
259             $params['courseid'] = $courseid;
260         }
261         $sql = "SELECT pue.userid, e.id AS enrolid
262                   FROM {user_enrolments} pue
263                   JOIN {enrol} pe ON (pe.id = pue.enrolid AND pe.enrol <> 'meta' AND pe.enrol $enabled )
264                   JOIN {enrol} e ON (e.customint1 = pe.courseid AND e.enrol = 'meta' AND e.status = :statusenabled $onecourse)
265              LEFT JOIN {user_enrolments} ue ON (ue.enrolid = e.id AND ue.userid = pue.userid)
266                  WHERE ue.id IS NULL";
267         $params['statusenabled'] = ENROL_INSTANCE_ENABLED;
268         $params['courseid'] = $courseid;
270         $rs = $DB->get_recordset_sql($sql, $params);
271         foreach($rs as $ue) {
272             if (!isset($instances[$ue->enrolid])) {
273                 $instances[$ue->enrolid] = $DB->get_record('enrol', array('id'=>$ue->enrolid));
274             }
275             $meta->enrol_user($instances[$ue->enrolid], $ue->userid);
276         }
277         $rs->close();
278     }
280     // unenrol as necessary - ignore enabled flag, we want to get rid of all
281     $sql = "SELECT ue.userid, e.id AS enrolid
282               FROM {user_enrolments} ue
283               JOIN {enrol} e ON (e.id = ue.enrolid AND e.enrol = 'meta' $onecourse)
284          LEFT JOIN (SELECT xue.userid, xe.courseid
285                       FROM {enrol} xe
286                       JOIN {user_enrolments} xue ON (xue.enrolid = xe.id)
287                    ) pue ON (pue.courseid = e.customint1 AND pue.userid = ue.userid)
288              WHERE pue.courseid IS NULL";
289     //TODO: this may use a bit of SQL optimisation
290     $rs = $DB->get_recordset_sql($sql, array('courseid'=>$courseid));
291     foreach($rs as $ue) {
292         if (!isset($instances[$ue->enrolid])) {
293             $instances[$ue->enrolid] = $DB->get_record('enrol', array('id'=>$ue->enrolid));
294         }
295         $meta->unenrol_user($instances[$ue->enrolid], $ue->userid);
296     }
297     $rs->close();
299     // now assign all necessary roles
300     if (enrol_is_enabled('meta')) {
301         $enabled = explode(',', $CFG->enrol_plugins_enabled);
302         foreach($enabled as $k=>$v) {
303             if ($v === 'meta') {
304                 continue; // no meta sync of meta roles
305             }
306             $enabled[$k] = 'enrol_'.$v;
307         }
308         $enabled[] = $DB->sql_empty(); // manual assignments are replicated too
310         list($enabled, $params) = $DB->get_in_or_equal($enabled, SQL_PARAMS_NAMED, 'e');
311         $sql = "SELECT DISTINCT pra.roleid, pra.userid, c.id AS contextid, e.id AS enrolid
312                   FROM {role_assignments} pra
313                   JOIN {user} u ON (u.id = pra.userid AND u.deleted = 0)
314                   JOIN {context} pc ON (pc.id = pra.contextid AND pc.contextlevel = :coursecontext AND pra.component $enabled)
315                   JOIN {enrol} e ON (e.customint1 = pc.instanceid AND e.enrol = 'meta' AND e.status = :statusenabled $onecourse)
316                   JOIN {context} c ON (c.contextlevel = pc.contextlevel AND c.instanceid = e.courseid)
317              LEFT JOIN {role_assignments} ra ON (ra.contextid = c.id AND ra.userid = pra.userid AND ra.roleid = pra.itemid AND ra.itemid = e.id AND ra.component = 'enrol_meta')
318                  WHERE ra.id IS NULL";
319         $params['statusenabled'] = ENROL_INSTANCE_ENABLED;
320         $params['coursecontext'] = CONTEXT_COURSE;
321         $params['courseid'] = $courseid;
323         if ($ignored = $meta->get_config('nosyncroleids')) {
324             list($notignored, $xparams) = $DB->get_in_or_equal(explode(',', $ignored), SQL_PARAMS_NAMED, 'ig', false);
325             $params = array_merge($params, $xparams);
326             $sql = "$sql AND pra.roleid $notignored";
327         }
329         $rs = $DB->get_recordset_sql($sql, $params);
330         foreach($rs as $ra) {
331             role_assign($ra->roleid, $ra->userid, $ra->contextid, 'enrol_meta', $ra->enrolid);
332         }
333         $rs->close();
334     }
336     // remove unwanted roles - include ignored roles and disabled plugins too
337     $params = array('coursecontext' => CONTEXT_COURSE, 'courseid' => $courseid);
338     if ($ignored = $meta->get_config('nosyncroleids')) {
339         list($notignored, $xparams) = $DB->get_in_or_equal(explode(',', $ignored), SQL_PARAMS_NAMED, 'ig', false);
340         $params = array_merge($params, $xparams);
341         $notignored = "AND pra.roleid $notignored";
342     } else {
343         $notignored = "";
344     }
345     $sql = "SELECT ra.roleid, ra.userid, ra.contextid, ra.itemid
346               FROM {role_assignments} ra
347               JOIN {enrol} e ON (e.id = ra.itemid AND ra.component = 'enrol_meta' AND e.enrol = 'meta' $onecourse)
348               JOIN {context} pc ON (pc.instanceid = e.customint1 AND pc.contextlevel = :coursecontext)
349          LEFT JOIN {role_assignments} pra ON (pra.contextid = pc.id AND pra.userid = ra.userid AND pra.roleid = ra.roleid AND pra.component <> 'enrol_meta' $notignored)
350              WHERE pra.id IS NULL";
352     $rs = $DB->get_recordset_sql($sql, $params);
353     foreach($rs as $ra) {
354         role_unassign($ra->roleid, $ra->userid, $ra->contextid, 'enrol_meta', $ra->itemid);
355     }
356     $rs->close();
358     // sync enrolment status
359     if (enrol_is_enabled('meta')) {
360         list($enabled, $params) = $DB->get_in_or_equal(explode(',', $CFG->enrol_plugins_enabled), SQL_PARAMS_NAMED, 'e');
361         if ($courseid) {
362             $params['courseid'] = $courseid;
363         }
364         //note: this will probably take a long time on mysql...
365         $sql = "SELECT ue.userid, e.id AS enrolid
366                   FROM {user_enrolments} ue
367                   JOIN {enrol} e ON (e.id = ue.enrolid AND e.enrol = 'meta' AND e.status = :statusenabled $onecourse)
368                  WHERE ue.status = :activestatus1
369                        AND NOT EXISTS (SELECT 'x'
370                                          FROM {user_enrolments} pue
371                                          JOIN {enrol} pe ON (pe.courseid = e.customint1 AND pe.enrol <> 'meta' AND pe.enrol $enabled)
372                                         WHERE pue.enrolid = pe.id AND pue.userid = ue.userid AND pue.status = :activestatus2)";
373         $params['statusenabled'] = ENROL_INSTANCE_ENABLED;
374         $params['activestatus1'] = ENROL_USER_ACTIVE;
375         $params['activestatus2'] = ENROL_USER_ACTIVE;
376         $params['courseid'] = $courseid;
378         $rs = $DB->get_recordset_sql($sql, $params);
379         foreach($rs as $ue) {
380             if (!isset($instances[$ue->enrolid])) {
381                 $instances[$ue->enrolid] = $DB->get_record('enrol', array('id'=>$ue->enrolid));
382             }
383             $meta->update_user_enrol($instances[$ue->enrolid], $ue->userid, ENROL_USER_SUSPENDED);
384         }
385         $rs->close();
387         list($enabled, $params) = $DB->get_in_or_equal(explode(',', $CFG->enrol_plugins_enabled), SQL_PARAMS_NAMED, 'e');
388         if ($courseid) {
389             $params['courseid'] = $courseid;
390         }
391         //note: this will probably take a long time on mysql...
392         $sql = "SELECT ue.userid, e.id AS enrolid
393                   FROM {user_enrolments} ue
394                   JOIN {enrol} e ON (e.id = ue.enrolid AND e.enrol = 'meta' AND e.status = :statusenabled $onecourse)
395                  WHERE ue.status = :suspendedstatus
396                        AND EXISTS (SELECT 'x'
397                                      FROM {user_enrolments} pue
398                                      JOIN {enrol} pe ON (pe.courseid = e.customint1 AND pe.enrol <> 'meta' AND pe.enrol $enabled)
399                                     WHERE pue.enrolid = pe.id AND pue.userid = ue.userid AND pue.status = :activestatus)";
400         $params['statusenabled'] = ENROL_INSTANCE_ENABLED;
401         $params['suspendedstatus'] = ENROL_USER_SUSPENDED;
402         $params['activestatus'] = ENROL_USER_ACTIVE;
403         $params['courseid'] = $courseid;
404         $rs = $DB->get_recordset_sql($sql, $params);
405         foreach($rs as $ue) {
406             if (!isset($instances[$ue->enrolid])) {
407                 $instances[$ue->enrolid] = $DB->get_record('enrol', array('id'=>$ue->enrolid));
408             }
409             $meta->update_user_enrol($instances[$ue->enrolid], $ue->userid, ENROL_USER_ACTIVE);
410         }
411         $rs->close();
413     }