MDL-69521 core: Move all comments in code from 4.1 to 3.11
[moodle.git] / enrol / ldap / lib.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  * LDAP enrolment plugin implementation.
19  *
20  * This plugin synchronises enrolment and roles with a LDAP server.
21  *
22  * @package    enrol_ldap
23  * @author     Iñaki Arenaza - based on code by Martin Dougiamas, Martin Langhoff and others
24  * @copyright  1999 onwards Martin Dougiamas {@link http://moodle.com}
25  * @copyright  2010 Iñaki Arenaza <iarenaza@eps.mondragon.edu>
26  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
27  */
29 defined('MOODLE_INTERNAL') || die();
31 class enrol_ldap_plugin extends enrol_plugin {
32     protected $enrol_localcoursefield = 'idnumber';
33     protected $enroltype = 'enrol_ldap';
34     protected $errorlogtag = '[ENROL LDAP] ';
36     /**
37      * The object class to use when finding users.
38      *
39      * @var string $userobjectclass
40      */
41     protected $userobjectclass;
43     /**
44      * Constructor for the plugin. In addition to calling the parent
45      * constructor, we define and 'fix' some settings depending on the
46      * real settings the admin defined.
47      */
48     public function __construct() {
49         global $CFG;
50         require_once($CFG->libdir.'/ldaplib.php');
52         // Do our own stuff to fix the config (it's easier to do it
53         // here than using the admin settings infrastructure). We
54         // don't call $this->set_config() for any of the 'fixups'
55         // (except the objectclass, as it's critical) because the user
56         // didn't specify any values and relied on the default values
57         // defined for the user type she chose.
58         $this->load_config();
60         // Make sure we get sane defaults for critical values.
61         $this->config->ldapencoding = $this->get_config('ldapencoding', 'utf-8');
62         $this->config->user_type = $this->get_config('user_type', 'default');
64         $ldap_usertypes = ldap_supported_usertypes();
65         $this->config->user_type_name = $ldap_usertypes[$this->config->user_type];
66         unset($ldap_usertypes);
68         $default = ldap_getdefaults();
70         // The objectclass in the defaults is for a user.
71         // This will be required later, but enrol_ldap uses 'objectclass' for its group objectclass.
72         // Save the normalised user objectclass for later.
73         $this->userobjectclass = ldap_normalise_objectclass($default['objectclass'][$this->get_config('user_type')]);
75         // Remove the objectclass default, as the values specified there are for users, and we are dealing with groups here.
76         unset($default['objectclass']);
78         // Use defaults if values not given. Dont use this->get_config()
79         // here to be able to check for 0 and false values too.
80         foreach ($default as $key => $value) {
81             // Watch out - 0, false are correct values too, so we can't use $this->get_config()
82             if (!isset($this->config->{$key}) or $this->config->{$key} == '') {
83                 $this->config->{$key} = $value[$this->config->user_type];
84             }
85         }
87         // Normalise the objectclass used for groups.
88         if (empty($this->config->objectclass)) {
89             // No objectclass set yet - set a default class.
90             $this->config->objectclass = ldap_normalise_objectclass(null, '*');
91             $this->set_config('objectclass', $this->config->objectclass);
92         } else {
93             $objectclass = ldap_normalise_objectclass($this->config->objectclass);
94             if ($objectclass !== $this->config->objectclass) {
95                 // The objectclass was changed during normalisation.
96                 // Save it in config, and update the local copy of config.
97                 $this->set_config('objectclass', $objectclass);
98                 $this->config->objectclass = $objectclass;
99             }
100         }
101     }
103     /**
104      * Is it possible to delete enrol instance via standard UI?
105      *
106      * @param object $instance
107      * @return bool
108      */
109     public function can_delete_instance($instance) {
110         $context = context_course::instance($instance->courseid);
111         if (!has_capability('enrol/ldap:manage', $context)) {
112             return false;
113         }
115         if (!enrol_is_enabled('ldap')) {
116             return true;
117         }
119         if (!$this->get_config('ldap_host') or !$this->get_config('objectclass') or !$this->get_config('course_idnumber')) {
120             return true;
121         }
123         // TODO: connect to external system and make sure no users are to be enrolled in this course
124         return false;
125     }
127     /**
128      * Is it possible to hide/show enrol instance via standard UI?
129      *
130      * @param stdClass $instance
131      * @return bool
132      */
133     public function can_hide_show_instance($instance) {
134         $context = context_course::instance($instance->courseid);
135         return has_capability('enrol/ldap:manage', $context);
136     }
138     /**
139      * Forces synchronisation of user enrolments with LDAP server.
140      * It creates courses if the plugin is configured to do so.
141      *
142      * @param object $user user record
143      * @return void
144      */
145     public function sync_user_enrolments($user) {
146         global $DB;
148         // Do not try to print anything to the output because this method is called during interactive login.
149         if (PHPUNIT_TEST) {
150             $trace = new null_progress_trace();
151         } else {
152             $trace = new error_log_progress_trace($this->errorlogtag);
153         }
155         if (!$this->ldap_connect($trace)) {
156             $trace->finished();
157             return;
158         }
160         if (!is_object($user) or !property_exists($user, 'id')) {
161             throw new coding_exception('Invalid $user parameter in sync_user_enrolments()');
162         }
164         if (!property_exists($user, 'idnumber')) {
165             debugging('Invalid $user parameter in sync_user_enrolments(), missing idnumber');
166             $user = $DB->get_record('user', array('id'=>$user->id));
167         }
169         // We may need a lot of memory here
170         core_php_time_limit::raise();
171         raise_memory_limit(MEMORY_HUGE);
173         // Get enrolments for each type of role.
174         $roles = get_all_roles();
175         $enrolments = array();
176         foreach($roles as $role) {
177             // Get external enrolments according to LDAP server
178             $enrolments[$role->id]['ext'] = $this->find_ext_enrolments($user->idnumber, $role);
180             // Get the list of current user enrolments that come from LDAP
181             $sql= "SELECT e.courseid, ue.status, e.id as enrolid, c.shortname
182                      FROM {user} u
183                      JOIN {role_assignments} ra ON (ra.userid = u.id AND ra.component = 'enrol_ldap' AND ra.roleid = :roleid)
184                      JOIN {user_enrolments} ue ON (ue.userid = u.id AND ue.enrolid = ra.itemid)
185                      JOIN {enrol} e ON (e.id = ue.enrolid)
186                      JOIN {course} c ON (c.id = e.courseid)
187                     WHERE u.deleted = 0 AND u.id = :userid";
188             $params = array ('roleid'=>$role->id, 'userid'=>$user->id);
189             $enrolments[$role->id]['current'] = $DB->get_records_sql($sql, $params);
190         }
192         $ignorehidden = $this->get_config('ignorehiddencourses');
193         $courseidnumber = $this->get_config('course_idnumber');
194         foreach($roles as $role) {
195             foreach ($enrolments[$role->id]['ext'] as $enrol) {
196                 $course_ext_id = $enrol[$courseidnumber][0];
197                 if (empty($course_ext_id)) {
198                     $trace->output(get_string('extcourseidinvalid', 'enrol_ldap'));
199                     continue; // Next; skip this one!
200                 }
202                 // Create the course if required
203                 $course = $DB->get_record('course', array($this->enrol_localcoursefield=>$course_ext_id));
204                 if (empty($course)) { // Course doesn't exist
205                     if ($this->get_config('autocreate')) { // Autocreate
206                         $trace->output(get_string('createcourseextid', 'enrol_ldap', array('courseextid'=>$course_ext_id)));
207                         if (!$newcourseid = $this->create_course($enrol, $trace)) {
208                             continue;
209                         }
210                         $course = $DB->get_record('course', array('id'=>$newcourseid));
211                     } else {
212                         $trace->output(get_string('createnotcourseextid', 'enrol_ldap', array('courseextid'=>$course_ext_id)));
213                         continue; // Next; skip this one!
214                     }
215                 }
217                 // Deal with enrolment in the moodle db
218                 // Add necessary enrol instance if not present yet;
219                 $sql = "SELECT c.id, c.visible, e.id as enrolid
220                           FROM {course} c
221                           JOIN {enrol} e ON (e.courseid = c.id AND e.enrol = 'ldap')
222                          WHERE c.id = :courseid";
223                 $params = array('courseid'=>$course->id);
224                 if (!($course_instance = $DB->get_record_sql($sql, $params, IGNORE_MULTIPLE))) {
225                     $course_instance = new stdClass();
226                     $course_instance->id = $course->id;
227                     $course_instance->visible = $course->visible;
228                     $course_instance->enrolid = $this->add_instance($course_instance);
229                 }
231                 if (!$instance = $DB->get_record('enrol', array('id'=>$course_instance->enrolid))) {
232                     continue; // Weird; skip this one.
233                 }
235                 if ($ignorehidden && !$course_instance->visible) {
236                     continue;
237                 }
239                 if (empty($enrolments[$role->id]['current'][$course->id])) {
240                     // Enrol the user in the given course, with that role.
241                     $this->enrol_user($instance, $user->id, $role->id);
242                     // Make sure we set the enrolment status to active. If the user wasn't
243                     // previously enrolled to the course, enrol_user() sets it. But if we
244                     // configured the plugin to suspend the user enrolments _AND_ remove
245                     // the role assignments on external unenrol, then enrol_user() doesn't
246                     // set it back to active on external re-enrolment. So set it
247                     // unconditionnally to cover both cases.
248                     $DB->set_field('user_enrolments', 'status', ENROL_USER_ACTIVE, array('enrolid'=>$instance->id, 'userid'=>$user->id));
249                     $trace->output(get_string('enroluser', 'enrol_ldap',
250                         array('user_username'=> $user->username,
251                               'course_shortname'=>$course->shortname,
252                               'course_id'=>$course->id)));
253                 } else {
254                     if ($enrolments[$role->id]['current'][$course->id]->status == ENROL_USER_SUSPENDED) {
255                         // Reenable enrolment that was previously disabled. Enrolment refreshed
256                         $DB->set_field('user_enrolments', 'status', ENROL_USER_ACTIVE, array('enrolid'=>$instance->id, 'userid'=>$user->id));
257                         $trace->output(get_string('enroluserenable', 'enrol_ldap',
258                             array('user_username'=> $user->username,
259                                   'course_shortname'=>$course->shortname,
260                                   'course_id'=>$course->id)));
261                     }
262                 }
264                 // Remove this course from the current courses, to be able to detect
265                 // which current courses should be unenroled from when we finish processing
266                 // external enrolments.
267                 unset($enrolments[$role->id]['current'][$course->id]);
268             }
270             // Deal with unenrolments.
271             $transaction = $DB->start_delegated_transaction();
272             foreach ($enrolments[$role->id]['current'] as $course) {
273                 $context = context_course::instance($course->courseid);
274                 $instance = $DB->get_record('enrol', array('id'=>$course->enrolid));
275                 switch ($this->get_config('unenrolaction')) {
276                     case ENROL_EXT_REMOVED_UNENROL:
277                         $this->unenrol_user($instance, $user->id);
278                         $trace->output(get_string('extremovedunenrol', 'enrol_ldap',
279                             array('user_username'=> $user->username,
280                                   'course_shortname'=>$course->shortname,
281                                   'course_id'=>$course->courseid)));
282                         break;
283                     case ENROL_EXT_REMOVED_KEEP:
284                         // Keep - only adding enrolments
285                         break;
286                     case ENROL_EXT_REMOVED_SUSPEND:
287                         if ($course->status != ENROL_USER_SUSPENDED) {
288                             $DB->set_field('user_enrolments', 'status', ENROL_USER_SUSPENDED, array('enrolid'=>$instance->id, 'userid'=>$user->id));
289                             $trace->output(get_string('extremovedsuspend', 'enrol_ldap',
290                                 array('user_username'=> $user->username,
291                                       'course_shortname'=>$course->shortname,
292                                       'course_id'=>$course->courseid)));
293                         }
294                         break;
295                     case ENROL_EXT_REMOVED_SUSPENDNOROLES:
296                         if ($course->status != ENROL_USER_SUSPENDED) {
297                             $DB->set_field('user_enrolments', 'status', ENROL_USER_SUSPENDED, array('enrolid'=>$instance->id, 'userid'=>$user->id));
298                         }
299                         role_unassign_all(array('contextid'=>$context->id, 'userid'=>$user->id, 'component'=>'enrol_ldap', 'itemid'=>$instance->id));
300                         $trace->output(get_string('extremovedsuspendnoroles', 'enrol_ldap',
301                             array('user_username'=> $user->username,
302                                   'course_shortname'=>$course->shortname,
303                                   'course_id'=>$course->courseid)));
304                         break;
305                 }
306             }
307             $transaction->allow_commit();
308         }
310         $this->ldap_close();
312         $trace->finished();
313     }
315     /**
316      * Forces synchronisation of all enrolments with LDAP server.
317      * It creates courses if the plugin is configured to do so.
318      *
319      * @param progress_trace $trace
320      * @param int|null $onecourse limit sync to one course->id, null if all courses
321      * @return void
322      */
323     public function sync_enrolments(progress_trace $trace, $onecourse = null) {
324         global $CFG, $DB;
326         if (!$this->ldap_connect($trace)) {
327             $trace->finished();
328             return;
329         }
331         $ldap_pagedresults = ldap_paged_results_supported($this->get_config('ldap_version'), $this->ldapconnection);
333         // we may need a lot of memory here
334         core_php_time_limit::raise();
335         raise_memory_limit(MEMORY_HUGE);
337         $oneidnumber = null;
338         if ($onecourse) {
339             if (!$course = $DB->get_record('course', array('id'=>$onecourse), 'id,'.$this->enrol_localcoursefield)) {
340                 // Course does not exist, nothing to do.
341                 $trace->output("Requested course $onecourse does not exist, no sync performed.");
342                 $trace->finished();
343                 return;
344             }
345             if (empty($course->{$this->enrol_localcoursefield})) {
346                 $trace->output("Requested course $onecourse does not have {$this->enrol_localcoursefield}, no sync performed.");
347                 $trace->finished();
348                 return;
349             }
350             $oneidnumber = ldap_filter_addslashes(core_text::convert($course->idnumber, 'utf-8', $this->get_config('ldapencoding')));
351         }
353         // Get enrolments for each type of role.
354         $roles = get_all_roles();
355         $enrolments = array();
356         foreach($roles as $role) {
357             // Get all contexts
358             $ldap_contexts = explode(';', $this->config->{'contexts_role'.$role->id});
360             // Get all the fields we will want for the potential course creation
361             // as they are light. Don't get membership -- potentially a lot of data.
362             $ldap_fields_wanted = array('dn', $this->config->course_idnumber);
363             if (!empty($this->config->course_fullname)) {
364                 array_push($ldap_fields_wanted, $this->config->course_fullname);
365             }
366             if (!empty($this->config->course_shortname)) {
367                 array_push($ldap_fields_wanted, $this->config->course_shortname);
368             }
369             if (!empty($this->config->course_summary)) {
370                 array_push($ldap_fields_wanted, $this->config->course_summary);
371             }
372             array_push($ldap_fields_wanted, $this->config->{'memberattribute_role'.$role->id});
374             // Define the search pattern
375             $ldap_search_pattern = $this->config->objectclass;
377             if ($oneidnumber !== null) {
378                 $ldap_search_pattern = "(&$ldap_search_pattern({$this->config->course_idnumber}=$oneidnumber))";
379             }
381             $ldap_cookie = '';
382             $servercontrols = array();
383             foreach ($ldap_contexts as $ldap_context) {
384                 $ldap_context = trim($ldap_context);
385                 if (empty($ldap_context)) {
386                     continue; // Next;
387                 }
389                 $flat_records = array();
390                 do {
391                     if ($ldap_pagedresults) {
392                         // TODO: Remove the old branch of code once PHP 7.3.0 becomes required (Moodle 3.11).
393                         if (version_compare(PHP_VERSION, '7.3.0', '<')) {
394                             // Before 7.3, use this function that was deprecated in PHP 7.4.
395                             ldap_control_paged_result($this->ldapconnection, $this->config->pagesize, true, $ldap_cookie);
396                         } else {
397                             // PHP 7.3 and up, use server controls.
398                             $servercontrols = array(array(
399                                 'oid' => LDAP_CONTROL_PAGEDRESULTS, 'value' => array(
400                                     'size' => $this->config->pagesize, 'cookie' => $ldap_cookie)));
401                         }
402                     }
404                     if ($this->config->course_search_sub) {
405                         // Use ldap_search to find first user from subtree
406                         // TODO: Remove the old branch of code once PHP 7.3.0 becomes required (Moodle 3.11).
407                         if (version_compare(PHP_VERSION, '7.3.0', '<')) {
408                             $ldap_result = @ldap_search($this->ldapconnection, $ldap_context,
409                                 $ldap_search_pattern, $ldap_fields_wanted);
410                         } else {
411                             $ldap_result = @ldap_search($this->ldapconnection, $ldap_context,
412                                 $ldap_search_pattern, $ldap_fields_wanted,
413                                 0, -1, -1, LDAP_DEREF_NEVER, $servercontrols);
414                         }
415                     } else {
416                         // Search only in this context
417                         // TODO: Remove the old branch of code once PHP 7.3.0 becomes required (Moodle 3.11).
418                         if (version_compare(PHP_VERSION, '7.3.0', '<')) {
419                             $ldap_result = @ldap_list($this->ldapconnection, $ldap_context,
420                                 $ldap_search_pattern, $ldap_fields_wanted);
421                         } else {
422                             $ldap_result = @ldap_list($this->ldapconnection, $ldap_context,
423                                 $ldap_search_pattern, $ldap_fields_wanted,
424                                 0, -1, -1, LDAP_DEREF_NEVER, $servercontrols);
425                         }
426                     }
427                     if (!$ldap_result) {
428                         continue; // Next
429                     }
431                     if ($ldap_pagedresults) {
432                         // Get next server cookie to know if we'll need to continue searching.
433                         $ldap_cookie = '';
434                         // TODO: Remove the old branch of code once PHP 7.3.0 becomes required (Moodle 3.11).
435                         if (version_compare(PHP_VERSION, '7.3.0', '<')) {
436                             // Before 7.3, use this function that was deprecated in PHP 7.4.
437                             ldap_control_paged_result_response($this->ldapconnection, $ldap_result, $ldap_cookie);
438                         } else {
439                             // Get next cookie from controls.
440                             ldap_parse_result($this->ldapconnection, $ldap_result, $errcode, $matcheddn,
441                                 $errmsg, $referrals, $controls);
442                             if (isset($controls[LDAP_CONTROL_PAGEDRESULTS]['value']['cookie'])) {
443                                 $ldap_cookie = $controls[LDAP_CONTROL_PAGEDRESULTS]['value']['cookie'];
444                             }
445                         }
446                     }
448                     // Check and push results
449                     $records = ldap_get_entries($this->ldapconnection, $ldap_result);
451                     // LDAP libraries return an odd array, really. fix it:
452                     for ($c = 0; $c < $records['count']; $c++) {
453                         array_push($flat_records, $records[$c]);
454                     }
455                     // Free some mem
456                     unset($records);
457                 } while ($ldap_pagedresults && !empty($ldap_cookie));
459                 // If LDAP paged results were used, the current connection must be completely
460                 // closed and a new one created, to work without paged results from here on.
461                 if ($ldap_pagedresults) {
462                     $this->ldap_close();
463                     $this->ldap_connect($trace);
464                 }
466                 if (count($flat_records)) {
467                     $ignorehidden = $this->get_config('ignorehiddencourses');
468                     foreach($flat_records as $course) {
469                         $course = array_change_key_case($course, CASE_LOWER);
470                         $idnumber = $course[$this->config->course_idnumber][0];
471                         $trace->output(get_string('synccourserole', 'enrol_ldap', array('idnumber'=>$idnumber, 'role_shortname'=>$role->shortname)));
473                         // Does the course exist in moodle already?
474                         $course_obj = $DB->get_record('course', array($this->enrol_localcoursefield=>$idnumber));
475                         if (empty($course_obj)) { // Course doesn't exist
476                             if ($this->get_config('autocreate')) { // Autocreate
477                                 $trace->output(get_string('createcourseextid', 'enrol_ldap', array('courseextid'=>$idnumber)));
478                                 if (!$newcourseid = $this->create_course($course, $trace)) {
479                                     continue;
480                                 }
481                                 $course_obj = $DB->get_record('course', array('id'=>$newcourseid));
482                             } else {
483                                 $trace->output(get_string('createnotcourseextid', 'enrol_ldap', array('courseextid'=>$idnumber)));
484                                 continue; // Next; skip this one!
485                             }
486                         } else {  // Check if course needs update & update as needed.
487                             $this->update_course($course_obj, $course, $trace);
488                         }
490                         // Enrol & unenrol
492                         // Pull the ldap membership into a nice array
493                         // this is an odd array -- mix of hash and array --
494                         $ldapmembers = array();
496                         if (property_exists($this->config, 'memberattribute_role'.$role->id)
497                             && !empty($this->config->{'memberattribute_role'.$role->id})
498                             && !empty($course[$this->config->{'memberattribute_role'.$role->id}])) { // May have no membership!
500                             $ldapmembers = $course[$this->config->{'memberattribute_role'.$role->id}];
501                             unset($ldapmembers['count']); // Remove oddity ;)
503                             // If we have enabled nested groups, we need to expand
504                             // the groups to get the real user list. We need to do
505                             // this before dealing with 'memberattribute_isdn'.
506                             if ($this->config->nested_groups) {
507                                 $users = array();
508                                 foreach ($ldapmembers as $ldapmember) {
509                                     $grpusers = $this->ldap_explode_group($ldapmember,
510                                                                           $this->config->{'memberattribute_role'.$role->id});
512                                     $users = array_merge($users, $grpusers);
513                                 }
514                                 $ldapmembers = array_unique($users); // There might be duplicates.
515                             }
517                             // Deal with the case where the member attribute holds distinguished names,
518                             // but only if the user attribute is not a distinguished name itself.
519                             if ($this->config->memberattribute_isdn
520                                 && ($this->config->idnumber_attribute !== 'dn')
521                                 && ($this->config->idnumber_attribute !== 'distinguishedname')) {
522                                 // We need to retrieve the idnumber for all the users in $ldapmembers,
523                                 // as the idnumber does not match their dn and we get dn's from membership.
524                                 $memberidnumbers = array();
525                                 foreach ($ldapmembers as $ldapmember) {
526                                     $result = ldap_read($this->ldapconnection, $ldapmember, $this->userobjectclass,
527                                                         array($this->config->idnumber_attribute));
528                                     $entry = ldap_first_entry($this->ldapconnection, $result);
529                                     $values = ldap_get_values($this->ldapconnection, $entry, $this->config->idnumber_attribute);
530                                     array_push($memberidnumbers, $values[0]);
531                                 }
533                                 $ldapmembers = $memberidnumbers;
534                             }
535                         }
537                         // Prune old ldap enrolments
538                         // hopefully they'll fit in the max buffer size for the RDBMS
539                         $sql= "SELECT u.id as userid, u.username, ue.status,
540                                       ra.contextid, ra.itemid as instanceid
541                                  FROM {user} u
542                                  JOIN {role_assignments} ra ON (ra.userid = u.id AND ra.component = 'enrol_ldap' AND ra.roleid = :roleid)
543                                  JOIN {user_enrolments} ue ON (ue.userid = u.id AND ue.enrolid = ra.itemid)
544                                  JOIN {enrol} e ON (e.id = ue.enrolid)
545                                 WHERE u.deleted = 0 AND e.courseid = :courseid ";
546                         $params = array('roleid'=>$role->id, 'courseid'=>$course_obj->id);
547                         $context = context_course::instance($course_obj->id);
548                         if (!empty($ldapmembers)) {
549                             list($ldapml, $params2) = $DB->get_in_or_equal($ldapmembers, SQL_PARAMS_NAMED, 'm', false);
550                             $sql .= "AND u.idnumber $ldapml";
551                             $params = array_merge($params, $params2);
552                             unset($params2);
553                         } else {
554                             $shortname = format_string($course_obj->shortname, true, array('context' => $context));
555                             $trace->output(get_string('emptyenrolment', 'enrol_ldap',
556                                          array('role_shortname'=> $role->shortname,
557                                                'course_shortname' => $shortname)));
558                         }
559                         $todelete = $DB->get_records_sql($sql, $params);
561                         if (!empty($todelete)) {
562                             $transaction = $DB->start_delegated_transaction();
563                             foreach ($todelete as $row) {
564                                 $instance = $DB->get_record('enrol', array('id'=>$row->instanceid));
565                                 switch ($this->get_config('unenrolaction')) {
566                                 case ENROL_EXT_REMOVED_UNENROL:
567                                     $this->unenrol_user($instance, $row->userid);
568                                     $trace->output(get_string('extremovedunenrol', 'enrol_ldap',
569                                         array('user_username'=> $row->username,
570                                               'course_shortname'=>$course_obj->shortname,
571                                               'course_id'=>$course_obj->id)));
572                                     break;
573                                 case ENROL_EXT_REMOVED_KEEP:
574                                     // Keep - only adding enrolments
575                                     break;
576                                 case ENROL_EXT_REMOVED_SUSPEND:
577                                     if ($row->status != ENROL_USER_SUSPENDED) {
578                                         $DB->set_field('user_enrolments', 'status', ENROL_USER_SUSPENDED, array('enrolid'=>$instance->id, 'userid'=>$row->userid));
579                                         $trace->output(get_string('extremovedsuspend', 'enrol_ldap',
580                                             array('user_username'=> $row->username,
581                                                   'course_shortname'=>$course_obj->shortname,
582                                                   'course_id'=>$course_obj->id)));
583                                     }
584                                     break;
585                                 case ENROL_EXT_REMOVED_SUSPENDNOROLES:
586                                     if ($row->status != ENROL_USER_SUSPENDED) {
587                                         $DB->set_field('user_enrolments', 'status', ENROL_USER_SUSPENDED, array('enrolid'=>$instance->id, 'userid'=>$row->userid));
588                                     }
589                                     role_unassign_all(array('contextid'=>$row->contextid, 'userid'=>$row->userid, 'component'=>'enrol_ldap', 'itemid'=>$instance->id));
590                                     $trace->output(get_string('extremovedsuspendnoroles', 'enrol_ldap',
591                                         array('user_username'=> $row->username,
592                                               'course_shortname'=>$course_obj->shortname,
593                                               'course_id'=>$course_obj->id)));
594                                     break;
595                                 }
596                             }
597                             $transaction->allow_commit();
598                         }
600                         // Insert current enrolments
601                         // bad we can't do INSERT IGNORE with postgres...
603                         // Add necessary enrol instance if not present yet;
604                         $sql = "SELECT c.id, c.visible, e.id as enrolid
605                                   FROM {course} c
606                                   JOIN {enrol} e ON (e.courseid = c.id AND e.enrol = 'ldap')
607                                  WHERE c.id = :courseid";
608                         $params = array('courseid'=>$course_obj->id);
609                         if (!($course_instance = $DB->get_record_sql($sql, $params, IGNORE_MULTIPLE))) {
610                             $course_instance = new stdClass();
611                             $course_instance->id = $course_obj->id;
612                             $course_instance->visible = $course_obj->visible;
613                             $course_instance->enrolid = $this->add_instance($course_instance);
614                         }
616                         if (!$instance = $DB->get_record('enrol', array('id'=>$course_instance->enrolid))) {
617                             continue; // Weird; skip this one.
618                         }
620                         if ($ignorehidden && !$course_instance->visible) {
621                             continue;
622                         }
624                         $transaction = $DB->start_delegated_transaction();
625                         foreach ($ldapmembers as $ldapmember) {
626                             $sql = 'SELECT id,username,1 FROM {user} WHERE idnumber = ? AND deleted = 0';
627                             $member = $DB->get_record_sql($sql, array($ldapmember));
628                             if(empty($member) || empty($member->id)){
629                                 $trace->output(get_string('couldnotfinduser', 'enrol_ldap', $ldapmember));
630                                 continue;
631                             }
633                             $sql= "SELECT ue.status
634                                      FROM {user_enrolments} ue
635                                      JOIN {enrol} e ON (e.id = ue.enrolid AND e.enrol = 'ldap')
636                                     WHERE e.courseid = :courseid AND ue.userid = :userid";
637                             $params = array('courseid'=>$course_obj->id, 'userid'=>$member->id);
638                             $userenrolment = $DB->get_record_sql($sql, $params, IGNORE_MULTIPLE);
640                             if (empty($userenrolment)) {
641                                 $this->enrol_user($instance, $member->id, $role->id);
642                                 // Make sure we set the enrolment status to active. If the user wasn't
643                                 // previously enrolled to the course, enrol_user() sets it. But if we
644                                 // configured the plugin to suspend the user enrolments _AND_ remove
645                                 // the role assignments on external unenrol, then enrol_user() doesn't
646                                 // set it back to active on external re-enrolment. So set it
647                                 // unconditionally to cover both cases.
648                                 $DB->set_field('user_enrolments', 'status', ENROL_USER_ACTIVE, array('enrolid'=>$instance->id, 'userid'=>$member->id));
649                                 $trace->output(get_string('enroluser', 'enrol_ldap',
650                                     array('user_username'=> $member->username,
651                                           'course_shortname'=>$course_obj->shortname,
652                                           'course_id'=>$course_obj->id)));
654                             } else {
655                                 if (!$DB->record_exists('role_assignments', array('roleid'=>$role->id, 'userid'=>$member->id, 'contextid'=>$context->id, 'component'=>'enrol_ldap', 'itemid'=>$instance->id))) {
656                                     // This happens when reviving users or when user has multiple roles in one course.
657                                     $context = context_course::instance($course_obj->id);
658                                     role_assign($role->id, $member->id, $context->id, 'enrol_ldap', $instance->id);
659                                     $trace->output("Assign role to user '$member->username' in course '$course_obj->shortname ($course_obj->id)'");
660                                 }
661                                 if ($userenrolment->status == ENROL_USER_SUSPENDED) {
662                                     // Reenable enrolment that was previously disabled. Enrolment refreshed
663                                     $DB->set_field('user_enrolments', 'status', ENROL_USER_ACTIVE, array('enrolid'=>$instance->id, 'userid'=>$member->id));
664                                     $trace->output(get_string('enroluserenable', 'enrol_ldap',
665                                         array('user_username'=> $member->username,
666                                               'course_shortname'=>$course_obj->shortname,
667                                               'course_id'=>$course_obj->id)));
668                                 }
669                             }
670                         }
671                         $transaction->allow_commit();
672                     }
673                 }
674             }
675         }
676         @$this->ldap_close();
677         $trace->finished();
678     }
680     /**
681      * Connect to the LDAP server, using the plugin configured
682      * settings. It's actually a wrapper around ldap_connect_moodle()
683      *
684      * @param progress_trace $trace
685      * @return bool success
686      */
687     protected function ldap_connect(progress_trace $trace = null) {
688         global $CFG;
689         require_once($CFG->libdir.'/ldaplib.php');
691         if (isset($this->ldapconnection)) {
692             return true;
693         }
695         if ($ldapconnection = ldap_connect_moodle($this->get_config('host_url'), $this->get_config('ldap_version'),
696                                                   $this->get_config('user_type'), $this->get_config('bind_dn'),
697                                                   $this->get_config('bind_pw'), $this->get_config('opt_deref'),
698                                                   $debuginfo, $this->get_config('start_tls'))) {
699             $this->ldapconnection = $ldapconnection;
700             return true;
701         }
703         if ($trace) {
704             $trace->output($debuginfo);
705         } else {
706             error_log($this->errorlogtag.$debuginfo);
707         }
709         return false;
710     }
712     /**
713      * Disconnects from a LDAP server
714      *
715      */
716     protected function ldap_close() {
717         if (isset($this->ldapconnection)) {
718             @ldap_close($this->ldapconnection);
719             $this->ldapconnection = null;
720         }
721         return;
722     }
724     /**
725      * Return multidimensional array with details of user courses (at
726      * least dn and idnumber).
727      *
728      * @param string $memberuid user idnumber (without magic quotes).
729      * @param object role is a record from the mdl_role table.
730      * @return array
731      */
732     protected function find_ext_enrolments($memberuid, $role) {
733         global $CFG;
734         require_once($CFG->libdir.'/ldaplib.php');
736         if (empty($memberuid)) {
737             // No "idnumber" stored for this user, so no LDAP enrolments
738             return array();
739         }
741         $ldap_contexts = trim($this->get_config('contexts_role'.$role->id));
742         if (empty($ldap_contexts)) {
743             // No role contexts, so no LDAP enrolments
744             return array();
745         }
747         $extmemberuid = core_text::convert($memberuid, 'utf-8', $this->get_config('ldapencoding'));
749         if($this->get_config('memberattribute_isdn')) {
750             if (!($extmemberuid = $this->ldap_find_userdn($extmemberuid))) {
751                 return array();
752             }
753         }
755         $ldap_search_pattern = '';
756         if($this->get_config('nested_groups')) {
757             $usergroups = $this->ldap_find_user_groups($extmemberuid);
758             if(count($usergroups) > 0) {
759                 foreach ($usergroups as $group) {
760                     $group = ldap_filter_addslashes($group);
761                     $ldap_search_pattern .= '('.$this->get_config('memberattribute_role'.$role->id).'='.$group.')';
762                 }
763             }
764         }
766         // Default return value
767         $courses = array();
769         // Get all the fields we will want for the potential course creation
770         // as they are light. don't get membership -- potentially a lot of data.
771         $ldap_fields_wanted = array('dn', $this->get_config('course_idnumber'));
772         $fullname  = $this->get_config('course_fullname');
773         $shortname = $this->get_config('course_shortname');
774         $summary   = $this->get_config('course_summary');
775         if (isset($fullname)) {
776             array_push($ldap_fields_wanted, $fullname);
777         }
778         if (isset($shortname)) {
779             array_push($ldap_fields_wanted, $shortname);
780         }
781         if (isset($summary)) {
782             array_push($ldap_fields_wanted, $summary);
783         }
785         // Define the search pattern
786         if (empty($ldap_search_pattern)) {
787             $ldap_search_pattern = '('.$this->get_config('memberattribute_role'.$role->id).'='.ldap_filter_addslashes($extmemberuid).')';
788         } else {
789             $ldap_search_pattern = '(|' . $ldap_search_pattern .
790                                        '('.$this->get_config('memberattribute_role'.$role->id).'='.ldap_filter_addslashes($extmemberuid).')' .
791                                    ')';
792         }
793         $ldap_search_pattern='(&'.$this->get_config('objectclass').$ldap_search_pattern.')';
795         // Get all contexts and look for first matching user
796         $ldap_contexts = explode(';', $ldap_contexts);
797         $ldap_pagedresults = ldap_paged_results_supported($this->get_config('ldap_version'), $this->ldapconnection);
798         foreach ($ldap_contexts as $context) {
799             $context = trim($context);
800             if (empty($context)) {
801                 continue;
802             }
804             $ldap_cookie = '';
805             $servercontrols = array();
806             $flat_records = array();
807             do {
808                 if ($ldap_pagedresults) {
809                     // TODO: Remove the old branch of code once PHP 7.3.0 becomes required (Moodle 3.11).
810                     if (version_compare(PHP_VERSION, '7.3.0', '<')) {
811                         // Before 7.3, use this function that was deprecated in PHP 7.4.
812                         ldap_control_paged_result($this->ldapconnection, $this->config->pagesize, true, $ldap_cookie);
813                     } else {
814                         // PHP 7.3 and up, use server controls.
815                         $servercontrols = array(array(
816                             'oid' => LDAP_CONTROL_PAGEDRESULTS, 'value' => array(
817                                 'size' => $this->config->pagesize, 'cookie' => $ldap_cookie)));
818                     }
819                 }
821                 if ($this->get_config('course_search_sub')) {
822                     // Use ldap_search to find first user from subtree
823                     // TODO: Remove the old branch of code once PHP 7.3.0 becomes required (Moodle 3.11).
824                     if (version_compare(PHP_VERSION, '7.3.0', '<')) {
825                         $ldap_result = @ldap_search($this->ldapconnection, $context,
826                             $ldap_search_pattern, $ldap_fields_wanted);
827                     } else {
828                         $ldap_result = @ldap_search($this->ldapconnection, $context,
829                             $ldap_search_pattern, $ldap_fields_wanted,
830                             0, -1, -1, LDAP_DEREF_NEVER, $servercontrols);
831                     }
832                 } else {
833                     // Search only in this context
834                     // TODO: Remove the old branch of code once PHP 7.3.0 becomes required (Moodle 3.11).
835                     if (version_compare(PHP_VERSION, '7.3.0', '<')) {
836                         $ldap_result = @ldap_list($this->ldapconnection, $context,
837                             $ldap_search_pattern, $ldap_fields_wanted);
838                     } else {
839                         $ldap_result = @ldap_list($this->ldapconnection, $context,
840                             $ldap_search_pattern, $ldap_fields_wanted,
841                             0, -1, -1, LDAP_DEREF_NEVER, $servercontrols);
842                     }
843                 }
845                 if (!$ldap_result) {
846                     continue;
847                 }
849                 if ($ldap_pagedresults) {
850                     // Get next server cookie to know if we'll need to continue searching.
851                     $ldap_cookie = '';
852                     // TODO: Remove the old branch of code once PHP 7.3.0 becomes required (Moodle 3.11).
853                     if (version_compare(PHP_VERSION, '7.3.0', '<')) {
854                         // Before 7.3, use this function that was deprecated in PHP 7.4.
855                         ldap_control_paged_result_response($this->ldapconnection, $ldap_result, $ldap_cookie);
856                     } else {
857                         // Get next cookie from controls.
858                         ldap_parse_result($this->ldapconnection, $ldap_result, $errcode, $matcheddn,
859                             $errmsg, $referrals, $controls);
860                         if (isset($controls[LDAP_CONTROL_PAGEDRESULTS]['value']['cookie'])) {
861                             $ldap_cookie = $controls[LDAP_CONTROL_PAGEDRESULTS]['value']['cookie'];
862                         }
863                     }
864                 }
866                 // Check and push results. ldap_get_entries() already
867                 // lowercases the attribute index, so there's no need to
868                 // use array_change_key_case() later.
869                 $records = ldap_get_entries($this->ldapconnection, $ldap_result);
871                 // LDAP libraries return an odd array, really. Fix it.
872                 for ($c = 0; $c < $records['count']; $c++) {
873                     array_push($flat_records, $records[$c]);
874                 }
875                 // Free some mem
876                 unset($records);
877             } while ($ldap_pagedresults && !empty($ldap_cookie));
879             // If LDAP paged results were used, the current connection must be completely
880             // closed and a new one created, to work without paged results from here on.
881             if ($ldap_pagedresults) {
882                 $this->ldap_close();
883                 $this->ldap_connect();
884             }
886             if (count($flat_records)) {
887                 $courses = array_merge($courses, $flat_records);
888             }
889         }
891         return $courses;
892     }
894     /**
895      * Search specified contexts for the specified userid and return the
896      * user dn like: cn=username,ou=suborg,o=org. It's actually a wrapper
897      * around ldap_find_userdn().
898      *
899      * @param string $userid the userid to search for (in external LDAP encoding, no magic quotes).
900      * @return mixed the user dn or false
901      */
902     protected function ldap_find_userdn($userid) {
903         global $CFG;
904         require_once($CFG->libdir.'/ldaplib.php');
906         $ldap_contexts = explode(';', $this->get_config('user_contexts'));
908         return ldap_find_userdn($this->ldapconnection, $userid, $ldap_contexts,
909                                 $this->userobjectclass,
910                                 $this->get_config('idnumber_attribute'), $this->get_config('user_search_sub'));
911     }
913     /**
914      * Find the groups a given distinguished name belongs to, both directly
915      * and indirectly via nested groups membership.
916      *
917      * @param string $memberdn distinguished name to search
918      * @return array with member groups' distinguished names (can be emtpy)
919      */
920     protected function ldap_find_user_groups($memberdn) {
921         $groups = array();
923         $this->ldap_find_user_groups_recursively($memberdn, $groups);
924         return $groups;
925     }
927     /**
928      * Recursively process the groups the given member distinguished name
929      * belongs to, adding them to the already processed groups array.
930      *
931      * @param string $memberdn distinguished name to search
932      * @param array reference &$membergroups array with already found
933      *                        groups, where we'll put the newly found
934      *                        groups.
935      */
936     protected function ldap_find_user_groups_recursively($memberdn, &$membergroups) {
937         $result = @ldap_read($this->ldapconnection, $memberdn, '(objectClass=*)', array($this->get_config('group_memberofattribute')));
938         if (!$result) {
939             return;
940         }
942         if ($entry = ldap_first_entry($this->ldapconnection, $result)) {
943             do {
944                 $attributes = ldap_get_attributes($this->ldapconnection, $entry);
945                 for ($j = 0; $j < $attributes['count']; $j++) {
946                     $groups = ldap_get_values_len($this->ldapconnection, $entry, $attributes[$j]);
947                     foreach ($groups as $key => $group) {
948                         if ($key === 'count') {  // Skip the entries count
949                             continue;
950                         }
951                         if(!in_array($group, $membergroups)) {
952                             // Only push and recurse if we haven't 'seen' this group before
953                             // to prevent loops (MS Active Directory allows them!!).
954                             array_push($membergroups, $group);
955                             $this->ldap_find_user_groups_recursively($group, $membergroups);
956                         }
957                     }
958                 }
959             }
960             while ($entry = ldap_next_entry($this->ldapconnection, $entry));
961         }
962     }
964     /**
965      * Given a group name (either a RDN or a DN), get the list of users
966      * belonging to that group. If the group has nested groups, expand all
967      * the intermediate groups and return the full list of users that
968      * directly or indirectly belong to the group.
969      *
970      * @param string $group the group name to search
971      * @param string $memberattibute the attribute that holds the members of the group
972      * @return array the list of users belonging to the group. If $group
973      *         is not actually a group, returns array($group).
974      */
975     protected function ldap_explode_group($group, $memberattribute) {
976         switch ($this->get_config('user_type')) {
977             case 'ad':
978                 // $group is already the distinguished name to search.
979                 $dn = $group;
981                 $result = ldap_read($this->ldapconnection, $dn, '(objectClass=*)', array('objectClass'));
982                 $entry = ldap_first_entry($this->ldapconnection, $result);
983                 $objectclass = ldap_get_values($this->ldapconnection, $entry, 'objectClass');
985                 if (!in_array('group', $objectclass)) {
986                     // Not a group, so return immediately.
987                     return array($group);
988                 }
990                 $result = ldap_read($this->ldapconnection, $dn, '(objectClass=*)', array($memberattribute));
991                 $entry = ldap_first_entry($this->ldapconnection, $result);
992                 $members = @ldap_get_values($this->ldapconnection, $entry, $memberattribute); // Can be empty and throws a warning
993                 if ($members['count'] == 0) {
994                     // There are no members in this group, return nothing.
995                     return array();
996                 }
997                 unset($members['count']);
999                 $users = array();
1000                 foreach ($members as $member) {
1001                     $group_members = $this->ldap_explode_group($member, $memberattribute);
1002                     $users = array_merge($users, $group_members);
1003                 }
1005                 return ($users);
1006                 break;
1007             default:
1008                 error_log($this->errorlogtag.get_string('explodegroupusertypenotsupported', 'enrol_ldap',
1009                                                         $this->get_config('user_type_name')));
1011                 return array($group);
1012         }
1013     }
1015     /**
1016      * Will create the moodle course from the template
1017      * course_ext is an array as obtained from ldap -- flattened somewhat
1018      *
1019      * @param array $course_ext
1020      * @param progress_trace $trace
1021      * @return mixed false on error, id for the newly created course otherwise.
1022      */
1023     function create_course($course_ext, progress_trace $trace) {
1024         global $CFG, $DB;
1026         require_once("$CFG->dirroot/course/lib.php");
1028         // Override defaults with template course
1029         $template = false;
1030         if ($this->get_config('template')) {
1031             if ($template = $DB->get_record('course', array('shortname'=>$this->get_config('template')))) {
1032                 $template = fullclone(course_get_format($template)->get_course());
1033                 unset($template->id); // So we are clear to reinsert the record
1034                 unset($template->fullname);
1035                 unset($template->shortname);
1036                 unset($template->idnumber);
1037             }
1038         }
1039         if (!$template) {
1040             $courseconfig = get_config('moodlecourse');
1041             $template = new stdClass();
1042             $template->summary        = '';
1043             $template->summaryformat  = FORMAT_HTML;
1044             $template->format         = $courseconfig->format;
1045             $template->newsitems      = $courseconfig->newsitems;
1046             $template->showgrades     = $courseconfig->showgrades;
1047             $template->showreports    = $courseconfig->showreports;
1048             $template->maxbytes       = $courseconfig->maxbytes;
1049             $template->groupmode      = $courseconfig->groupmode;
1050             $template->groupmodeforce = $courseconfig->groupmodeforce;
1051             $template->visible        = $courseconfig->visible;
1052             $template->lang           = $courseconfig->lang;
1053             $template->enablecompletion = $courseconfig->enablecompletion;
1054         }
1055         $course = $template;
1057         $course->category = $this->get_config('category');
1058         if (!$DB->record_exists('course_categories', array('id'=>$this->get_config('category')))) {
1059             $categories = $DB->get_records('course_categories', array(), 'sortorder', 'id', 0, 1);
1060             $first = reset($categories);
1061             $course->category = $first->id;
1062         }
1064         // Override with required ext data
1065         $course->idnumber  = $course_ext[$this->get_config('course_idnumber')][0];
1066         $course->fullname  = $course_ext[$this->get_config('course_fullname')][0];
1067         $course->shortname = $course_ext[$this->get_config('course_shortname')][0];
1068         if (empty($course->idnumber) || empty($course->fullname) || empty($course->shortname)) {
1069             // We are in trouble!
1070             $trace->output(get_string('cannotcreatecourse', 'enrol_ldap').' '.var_export($course, true));
1071             return false;
1072         }
1074         $summary = $this->get_config('course_summary');
1075         if (!isset($summary) || empty($course_ext[$summary][0])) {
1076             $course->summary = '';
1077         } else {
1078             $course->summary = $course_ext[$this->get_config('course_summary')][0];
1079         }
1081         // Check if the shortname already exists if it does - skip course creation.
1082         if ($DB->record_exists('course', array('shortname' => $course->shortname))) {
1083             $trace->output(get_string('duplicateshortname', 'enrol_ldap', $course));
1084             return false;
1085         }
1087         $newcourse = create_course($course);
1088         return $newcourse->id;
1089     }
1091     /**
1092      * Will update a moodle course with new values from LDAP
1093      * A field will be updated only if it is marked to be updated
1094      * on sync in plugin settings
1095      *
1096      * @param object $course
1097      * @param array $externalcourse
1098      * @param progress_trace $trace
1099      * @return bool
1100      */
1101     protected function update_course($course, $externalcourse, progress_trace $trace) {
1102         global $CFG, $DB;
1104         $coursefields = array ('shortname', 'fullname', 'summary');
1105         static $shouldupdate;
1107         // Initialize $shouldupdate variable. Set to true if one or more fields are marked for update.
1108         if (!isset($shouldupdate)) {
1109             $shouldupdate = false;
1110             foreach ($coursefields as $field) {
1111                 $shouldupdate = $shouldupdate || $this->get_config('course_'.$field.'_updateonsync');
1112             }
1113         }
1115         // If we should not update return immediately.
1116         if (!$shouldupdate) {
1117             return false;
1118         }
1120         require_once("$CFG->dirroot/course/lib.php");
1121         $courseupdated = false;
1122         $updatedcourse = new stdClass();
1123         $updatedcourse->id = $course->id;
1125         // Update course fields if necessary.
1126         foreach ($coursefields as $field) {
1127             // If field is marked to be updated on sync && field data was changed update it.
1128             if ($this->get_config('course_'.$field.'_updateonsync')
1129                     && isset($externalcourse[$this->get_config('course_'.$field)][0])
1130                     && $course->{$field} != $externalcourse[$this->get_config('course_'.$field)][0]) {
1131                 $updatedcourse->{$field} = $externalcourse[$this->get_config('course_'.$field)][0];
1132                 $courseupdated = true;
1133             }
1134         }
1136         if (!$courseupdated) {
1137             $trace->output(get_string('courseupdateskipped', 'enrol_ldap', $course));
1138             return false;
1139         }
1141         // Do not allow empty fullname or shortname.
1142         if ((isset($updatedcourse->fullname) && empty($updatedcourse->fullname))
1143                 || (isset($updatedcourse->shortname) && empty($updatedcourse->shortname))) {
1144             // We are in trouble!
1145             $trace->output(get_string('cannotupdatecourse', 'enrol_ldap', $course));
1146             return false;
1147         }
1149         // Check if the shortname already exists if it does - skip course updating.
1150         if (isset($updatedcourse->shortname)
1151                 && $DB->record_exists('course', array('shortname' => $updatedcourse->shortname))) {
1152             $trace->output(get_string('cannotupdatecourse_duplicateshortname', 'enrol_ldap', $course));
1153             return false;
1154         }
1156         // Finally - update course in DB.
1157         update_course($updatedcourse);
1158         $trace->output(get_string('courseupdated', 'enrol_ldap', $course));
1160         return true;
1161     }
1163     /**
1164      * Automatic enrol sync executed during restore.
1165      * Useful for automatic sync by course->idnumber or course category.
1166      * @param stdClass $course course record
1167      */
1168     public function restore_sync_course($course) {
1169         // TODO: this can not work because restore always nukes the course->idnumber, do not ask me why (MDL-37312)
1170         // NOTE: for now restore does not do any real logging yet, let's do the same here...
1171         $trace = new error_log_progress_trace();
1172         $this->sync_enrolments($trace, $course->id);
1173     }
1175     /**
1176      * Restore instance and map settings.
1177      *
1178      * @param restore_enrolments_structure_step $step
1179      * @param stdClass $data
1180      * @param stdClass $course
1181      * @param int $oldid
1182      */
1183     public function restore_instance(restore_enrolments_structure_step $step, stdClass $data, $course, $oldid) {
1184         global $DB;
1185         // There is only 1 ldap enrol instance per course.
1186         if ($instances = $DB->get_records('enrol', array('courseid'=>$data->courseid, 'enrol'=>'ldap'), 'id')) {
1187             $instance = reset($instances);
1188             $instanceid = $instance->id;
1189         } else {
1190             $instanceid = $this->add_instance($course, (array)$data);
1191         }
1192         $step->set_mapping('enrol', $oldid, $instanceid);
1193     }
1195     /**
1196      * Restore user enrolment.
1197      *
1198      * @param restore_enrolments_structure_step $step
1199      * @param stdClass $data
1200      * @param stdClass $instance
1201      * @param int $oldinstancestatus
1202      * @param int $userid
1203      */
1204     public function restore_user_enrolment(restore_enrolments_structure_step $step, $data, $instance, $userid, $oldinstancestatus) {
1205         global $DB;
1207         if ($this->get_config('unenrolaction') == ENROL_EXT_REMOVED_UNENROL) {
1208             // Enrolments were already synchronised in restore_instance(), we do not want any suspended leftovers.
1210         } else if ($this->get_config('unenrolaction') == ENROL_EXT_REMOVED_KEEP) {
1211             if (!$DB->record_exists('user_enrolments', array('enrolid'=>$instance->id, 'userid'=>$userid))) {
1212                 $this->enrol_user($instance, $userid, null, 0, 0, $data->status);
1213             }
1215         } else {
1216             if (!$DB->record_exists('user_enrolments', array('enrolid'=>$instance->id, 'userid'=>$userid))) {
1217                 $this->enrol_user($instance, $userid, null, 0, 0, ENROL_USER_SUSPENDED);
1218             }
1219         }
1220     }
1222     /**
1223      * Restore role assignment.
1224      *
1225      * @param stdClass $instance
1226      * @param int $roleid
1227      * @param int $userid
1228      * @param int $contextid
1229      */
1230     public function restore_role_assignment($instance, $roleid, $userid, $contextid) {
1231         global $DB;
1233         if ($this->get_config('unenrolaction') == ENROL_EXT_REMOVED_UNENROL or $this->get_config('unenrolaction') == ENROL_EXT_REMOVED_SUSPENDNOROLES) {
1234             // Skip any roles restore, they should be already synced automatically.
1235             return;
1236         }
1238         // Just restore every role.
1239         if ($DB->record_exists('user_enrolments', array('enrolid'=>$instance->id, 'userid'=>$userid))) {
1240             role_assign($roleid, $userid, $contextid, 'enrol_'.$instance->enrol, $instance->id);
1241         }
1242     }