MDL-69521 core: Move all comments in code from 4.1 to 3.11
[moodle.git] / enrol / ldap / lib.php
CommitLineData
5704585c 1<?php
5704585c
I
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/>.
16
17/**
18 * LDAP enrolment plugin implementation.
19 *
20 * This plugin synchronises enrolment and roles with a LDAP server.
21 *
31ac2aef 22 * @package enrol_ldap
4a77c443
PS
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
5704585c
I
27 */
28
4a77c443
PS
29defined('MOODLE_INTERNAL') || die();
30
5704585c
I
31class enrol_ldap_plugin extends enrol_plugin {
32 protected $enrol_localcoursefield = 'idnumber';
33 protected $enroltype = 'enrol_ldap';
34 protected $errorlogtag = '[ENROL LDAP] ';
35
abedeb8c
AN
36 /**
37 * The object class to use when finding users.
38 *
39 * @var string $userobjectclass
40 */
41 protected $userobjectclass;
42
5704585c
I
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');
51
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();
59
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');
63
64 $ldap_usertypes = ldap_supported_usertypes();
65 $this->config->user_type_name = $ldap_usertypes[$this->config->user_type];
66 unset($ldap_usertypes);
67
68 $default = ldap_getdefaults();
abedeb8c
AN
69
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')]);
74
75 // Remove the objectclass default, as the values specified there are for users, and we are dealing with groups here.
5704585c
I
76 unset($default['objectclass']);
77
4a77c443 78 // Use defaults if values not given. Dont use this->get_config()
5704585c
I
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 }
86
abedeb8c 87 // Normalise the objectclass used for groups.
5704585c 88 if (empty($this->config->objectclass)) {
abedeb8c
AN
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);
5704585c 92 } else {
abedeb8c
AN
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 }
5704585c
I
100 }
101 }
102
103 /**
104 * Is it possible to delete enrol instance via standard UI?
105 *
106 * @param object $instance
107 * @return bool
108 */
ee9e079d
DN
109 public function can_delete_instance($instance) {
110 $context = context_course::instance($instance->courseid);
0594336d 111 if (!has_capability('enrol/ldap:manage', $context)) {
ee9e079d
DN
112 return false;
113 }
114
5704585c
I
115 if (!enrol_is_enabled('ldap')) {
116 return true;
117 }
118
119 if (!$this->get_config('ldap_host') or !$this->get_config('objectclass') or !$this->get_config('course_idnumber')) {
120 return true;
121 }
122
123 // TODO: connect to external system and make sure no users are to be enrolled in this course
124 return false;
125 }
126
b5a289c4
DNA
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);
935f428a 135 return has_capability('enrol/ldap:manage', $context);
b5a289c4
DNA
136 }
137
5704585c
I
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;
147
e34beca1 148 // Do not try to print anything to the output because this method is called during interactive login.
e790a3ec
RT
149 if (PHPUNIT_TEST) {
150 $trace = new null_progress_trace();
151 } else {
152 $trace = new error_log_progress_trace($this->errorlogtag);
153 }
e34beca1 154
23f36d50 155 if (!$this->ldap_connect($trace)) {
e34beca1 156 $trace->finished();
5704585c
I
157 return;
158 }
159
07cd46d8
PS
160 if (!is_object($user) or !property_exists($user, 'id')) {
161 throw new coding_exception('Invalid $user parameter in sync_user_enrolments()');
162 }
163
07f31e9f 164 if (!property_exists($user, 'idnumber')) {
07cd46d8
PS
165 debugging('Invalid $user parameter in sync_user_enrolments(), missing idnumber');
166 $user = $DB->get_record('user', array('id'=>$user->id));
167 }
168
5704585c 169 // We may need a lot of memory here
3ef7279f 170 core_php_time_limit::raise();
346c5887 171 raise_memory_limit(MEMORY_HUGE);
5704585c
I
172
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
23f36d50 178 $enrolments[$role->id]['ext'] = $this->find_ext_enrolments($user->idnumber, $role);
5704585c
I
179
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 }
191
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)) {
e34beca1 198 $trace->output(get_string('extcourseidinvalid', 'enrol_ldap'));
5704585c
I
199 continue; // Next; skip this one!
200 }
201
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
e34beca1
PS
206 $trace->output(get_string('createcourseextid', 'enrol_ldap', array('courseextid'=>$course_ext_id)));
207 if (!$newcourseid = $this->create_course($enrol, $trace)) {
208 continue;
5704585c 209 }
e34beca1 210 $course = $DB->get_record('course', array('id'=>$newcourseid));
5704585c 211 } else {
e34beca1 212 $trace->output(get_string('createnotcourseextid', 'enrol_ldap', array('courseextid'=>$course_ext_id)));
5704585c
I
213 continue; // Next; skip this one!
214 }
215 }
216
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))) {
94b9c2e8 225 $course_instance = new stdClass();
5704585c
I
226 $course_instance->id = $course->id;
227 $course_instance->visible = $course->visible;
228 $course_instance->enrolid = $this->add_instance($course_instance);
229 }
230
231 if (!$instance = $DB->get_record('enrol', array('id'=>$course_instance->enrolid))) {
232 continue; // Weird; skip this one.
233 }
234
235 if ($ignorehidden && !$course_instance->visible) {
236 continue;
237 }
238
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));
e34beca1
PS
249 $trace->output(get_string('enroluser', 'enrol_ldap',
250 array('user_username'=> $user->username,
251 'course_shortname'=>$course->shortname,
252 'course_id'=>$course->id)));
5704585c
I
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));
e34beca1
PS
257 $trace->output(get_string('enroluserenable', 'enrol_ldap',
258 array('user_username'=> $user->username,
259 'course_shortname'=>$course->shortname,
260 'course_id'=>$course->id)));
5704585c
I
261 }
262 }
263
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 }
269
270 // Deal with unenrolments.
271 $transaction = $DB->start_delegated_transaction();
272 foreach ($enrolments[$role->id]['current'] as $course) {
55bcef29 273 $context = context_course::instance($course->courseid);
5704585c
I
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);
e34beca1
PS
278 $trace->output(get_string('extremovedunenrol', 'enrol_ldap',
279 array('user_username'=> $user->username,
280 'course_shortname'=>$course->shortname,
281 'course_id'=>$course->courseid)));
5704585c
I
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));
e34beca1
PS
289 $trace->output(get_string('extremovedsuspend', 'enrol_ldap',
290 array('user_username'=> $user->username,
291 'course_shortname'=>$course->shortname,
292 'course_id'=>$course->courseid)));
5704585c
I
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));
e34beca1
PS
300 $trace->output(get_string('extremovedsuspendnoroles', 'enrol_ldap',
301 array('user_username'=> $user->username,
302 'course_shortname'=>$course->shortname,
303 'course_id'=>$course->courseid)));
5704585c
I
304 break;
305 }
306 }
307 $transaction->allow_commit();
308 }
309
23f36d50 310 $this->ldap_close();
e34beca1
PS
311
312 $trace->finished();
5704585c
I
313 }
314
315 /**
316 * Forces synchronisation of all enrolments with LDAP server.
317 * It creates courses if the plugin is configured to do so.
318 *
e34beca1 319 * @param progress_trace $trace
1b20e008 320 * @param int|null $onecourse limit sync to one course->id, null if all courses
5704585c
I
321 * @return void
322 */
e34beca1 323 public function sync_enrolments(progress_trace $trace, $onecourse = null) {
5704585c
I
324 global $CFG, $DB;
325
23f36d50 326 if (!$this->ldap_connect($trace)) {
e34beca1 327 $trace->finished();
5704585c
I
328 return;
329 }
330
d349b0f0 331 $ldap_pagedresults = ldap_paged_results_supported($this->get_config('ldap_version'), $this->ldapconnection);
c090d7c9 332
5704585c 333 // we may need a lot of memory here
3ef7279f 334 core_php_time_limit::raise();
346c5887 335 raise_memory_limit(MEMORY_HUGE);
5704585c 336
e34beca1
PS
337 $oneidnumber = null;
338 if ($onecourse) {
1b20e008
PS
339 if (!$course = $DB->get_record('course', array('id'=>$onecourse), 'id,'.$this->enrol_localcoursefield)) {
340 // Course does not exist, nothing to do.
e34beca1
PS
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 }
2f1e464a 350 $oneidnumber = ldap_filter_addslashes(core_text::convert($course->idnumber, 'utf-8', $this->get_config('ldapencoding')));
e34beca1
PS
351 }
352
5704585c
I
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});
359
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});
373
374 // Define the search pattern
375 $ldap_search_pattern = $this->config->objectclass;
376
e34beca1
PS
377 if ($oneidnumber !== null) {
378 $ldap_search_pattern = "(&$ldap_search_pattern({$this->config->course_idnumber}=$oneidnumber))";
379 }
380
c090d7c9 381 $ldap_cookie = '';
ba62f54e 382 $servercontrols = array();
5704585c
I
383 foreach ($ldap_contexts as $ldap_context) {
384 $ldap_context = trim($ldap_context);
385 if (empty($ldap_context)) {
386 continue; // Next;
387 }
388
afa23468 389 $flat_records = array();
c090d7c9
IA
390 do {
391 if ($ldap_pagedresults) {
74ee34fd 392 // TODO: Remove the old branch of code once PHP 7.3.0 becomes required (Moodle 3.11).
ba62f54e
EL
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 }
c090d7c9 402 }
5704585c 403
c090d7c9
IA
404 if ($this->config->course_search_sub) {
405 // Use ldap_search to find first user from subtree
74ee34fd 406 // TODO: Remove the old branch of code once PHP 7.3.0 becomes required (Moodle 3.11).
ba62f54e
EL
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 }
c090d7c9
IA
415 } else {
416 // Search only in this context
74ee34fd 417 // TODO: Remove the old branch of code once PHP 7.3.0 becomes required (Moodle 3.11).
ba62f54e
EL
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 }
c090d7c9
IA
426 }
427 if (!$ldap_result) {
428 continue; // Next
429 }
5704585c 430
c090d7c9 431 if ($ldap_pagedresults) {
ba62f54e
EL
432 // Get next server cookie to know if we'll need to continue searching.
433 $ldap_cookie = '';
74ee34fd 434 // TODO: Remove the old branch of code once PHP 7.3.0 becomes required (Moodle 3.11).
ba62f54e
EL
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 }
c090d7c9
IA
446 }
447
448 // Check and push results
23f36d50 449 $records = ldap_get_entries($this->ldapconnection, $ldap_result);
c090d7c9
IA
450
451 // LDAP libraries return an odd array, really. fix it:
c090d7c9
IA
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));
458
ee943e73 459 // If LDAP paged results were used, the current connection must be completely
c090d7c9
IA
460 // closed and a new one created, to work without paged results from here on.
461 if ($ldap_pagedresults) {
23f36d50
PS
462 $this->ldap_close();
463 $this->ldap_connect($trace);
5704585c 464 }
5704585c
I
465
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);
88d29a1b 470 $idnumber = $course[$this->config->course_idnumber][0];
e34beca1 471 $trace->output(get_string('synccourserole', 'enrol_ldap', array('idnumber'=>$idnumber, 'role_shortname'=>$role->shortname)));
5704585c
I
472
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
e34beca1
PS
477 $trace->output(get_string('createcourseextid', 'enrol_ldap', array('courseextid'=>$idnumber)));
478 if (!$newcourseid = $this->create_course($course, $trace)) {
479 continue;
5704585c 480 }
e34beca1 481 $course_obj = $DB->get_record('course', array('id'=>$newcourseid));
5704585c 482 } else {
e34beca1 483 $trace->output(get_string('createnotcourseextid', 'enrol_ldap', array('courseextid'=>$idnumber)));
5704585c
I
484 continue; // Next; skip this one!
485 }
d8a55f59
NB
486 } else { // Check if course needs update & update as needed.
487 $this->update_course($course_obj, $course, $trace);
5704585c
I
488 }
489
490 // Enrol & unenrol
491
492 // Pull the ldap membership into a nice array
493 // this is an odd array -- mix of hash and array --
494 $ldapmembers = array();
495
f4feabb8 496 if (property_exists($this->config, 'memberattribute_role'.$role->id)
5704585c
I
497 && !empty($this->config->{'memberattribute_role'.$role->id})
498 && !empty($course[$this->config->{'memberattribute_role'.$role->id}])) { // May have no membership!
499
500 $ldapmembers = $course[$this->config->{'memberattribute_role'.$role->id}];
501 unset($ldapmembers['count']); // Remove oddity ;)
502
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) {
23f36d50 509 $grpusers = $this->ldap_explode_group($ldapmember,
5704585c
I
510 $this->config->{'memberattribute_role'.$role->id});
511
512 $users = array_merge($users, $grpusers);
513 }
514 $ldapmembers = array_unique($users); // There might be duplicates.
515 }
516
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) {
abedeb8c 526 $result = ldap_read($this->ldapconnection, $ldapmember, $this->userobjectclass,
5704585c 527 array($this->config->idnumber_attribute));
23f36d50
PS
528 $entry = ldap_first_entry($this->ldapconnection, $result);
529 $values = ldap_get_values($this->ldapconnection, $entry, $this->config->idnumber_attribute);
5704585c
I
530 array_push($memberidnumbers, $values[0]);
531 }
532
533 $ldapmembers = $memberidnumbers;
534 }
535 }
536
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);
55bcef29 547 $context = context_course::instance($course_obj->id);
5704585c 548 if (!empty($ldapmembers)) {
cf717dc2 549 list($ldapml, $params2) = $DB->get_in_or_equal($ldapmembers, SQL_PARAMS_NAMED, 'm', false);
5704585c
I
550 $sql .= "AND u.idnumber $ldapml";
551 $params = array_merge($params, $params2);
552 unset($params2);
553 } else {
8ebbb06a 554 $shortname = format_string($course_obj->shortname, true, array('context' => $context));
34183020 555 $trace->output(get_string('emptyenrolment', 'enrol_ldap',
5704585c 556 array('role_shortname'=> $role->shortname,
34183020 557 'course_shortname' => $shortname)));
5704585c
I
558 }
559 $todelete = $DB->get_records_sql($sql, $params);
560
5704585c
I
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);
e34beca1
PS
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)));
5704585c
I
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));
e34beca1
PS
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)));
5704585c
I
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));
e34beca1
PS
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)));
5704585c
I
594 break;
595 }
596 }
597 $transaction->allow_commit();
598 }
599
600 // Insert current enrolments
601 // bad we can't do INSERT IGNORE with postgres...
602
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))) {
94b9c2e8 610 $course_instance = new stdClass();
5704585c
I
611 $course_instance->id = $course_obj->id;
612 $course_instance->visible = $course_obj->visible;
613 $course_instance->enrolid = $this->add_instance($course_instance);
614 }
615
616 if (!$instance = $DB->get_record('enrol', array('id'=>$course_instance->enrolid))) {
617 continue; // Weird; skip this one.
618 }
619
620 if ($ignorehidden && !$course_instance->visible) {
621 continue;
622 }
623
4a77c443 624 $transaction = $DB->start_delegated_transaction();
5704585c
I
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)){
34183020 629 $trace->output(get_string('couldnotfinduser', 'enrol_ldap', $ldapmember));
5704585c
I
630 continue;
631 }
632
633 $sql= "SELECT ue.status
634 FROM {user_enrolments} ue
34183020 635 JOIN {enrol} e ON (e.id = ue.enrolid AND e.enrol = 'ldap')
5704585c
I
636 WHERE e.courseid = :courseid AND ue.userid = :userid";
637 $params = array('courseid'=>$course_obj->id, 'userid'=>$member->id);
e34beca1 638 $userenrolment = $DB->get_record_sql($sql, $params, IGNORE_MULTIPLE);
5704585c 639
34183020 640 if (empty($userenrolment)) {
5704585c
I
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
34183020 647 // unconditionally to cover both cases.
5704585c 648 $DB->set_field('user_enrolments', 'status', ENROL_USER_ACTIVE, array('enrolid'=>$instance->id, 'userid'=>$member->id));
e34beca1
PS
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)));
5704585c
I
653
654 } else {
34183020
PS
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 }
5704585c
I
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));
e34beca1
PS
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)));
5704585c
I
668 }
669 }
670 }
671 $transaction->allow_commit();
672 }
673 }
674 }
675 }
676 @$this->ldap_close();
e34beca1 677 $trace->finished();
5704585c
I
678 }
679
680 /**
681 * Connect to the LDAP server, using the plugin configured
682 * settings. It's actually a wrapper around ldap_connect_moodle()
683 *
e34beca1 684 * @param progress_trace $trace
23f36d50 685 * @return bool success
5704585c 686 */
e34beca1 687 protected function ldap_connect(progress_trace $trace = null) {
5704585c
I
688 global $CFG;
689 require_once($CFG->libdir.'/ldaplib.php');
690
23f36d50
PS
691 if (isset($this->ldapconnection)) {
692 return true;
5704585c
I
693 }
694
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'),
326929d5 698 $debuginfo, $this->get_config('start_tls'))) {
5704585c 699 $this->ldapconnection = $ldapconnection;
23f36d50 700 return true;
5704585c
I
701 }
702
e34beca1
PS
703 if ($trace) {
704 $trace->output($debuginfo);
705 } else {
706 error_log($this->errorlogtag.$debuginfo);
707 }
5704585c
I
708
709 return false;
710 }
711
712 /**
713 * Disconnects from a LDAP server
714 *
715 */
716 protected function ldap_close() {
23f36d50 717 if (isset($this->ldapconnection)) {
5704585c 718 @ldap_close($this->ldapconnection);
23f36d50 719 $this->ldapconnection = null;
5704585c 720 }
23f36d50 721 return;
5704585c
I
722 }
723
724 /**
725 * Return multidimensional array with details of user courses (at
726 * least dn and idnumber).
727 *
5704585c
I
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 */
23f36d50 732 protected function find_ext_enrolments($memberuid, $role) {
5704585c
I
733 global $CFG;
734 require_once($CFG->libdir.'/ldaplib.php');
735
736 if (empty($memberuid)) {
737 // No "idnumber" stored for this user, so no LDAP enrolments
738 return array();
739 }
740
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 }
746
2f1e464a 747 $extmemberuid = core_text::convert($memberuid, 'utf-8', $this->get_config('ldapencoding'));
5704585c
I
748
749 if($this->get_config('memberattribute_isdn')) {
23f36d50 750 if (!($extmemberuid = $this->ldap_find_userdn($extmemberuid))) {
5704585c
I
751 return array();
752 }
753 }
754
755 $ldap_search_pattern = '';
756 if($this->get_config('nested_groups')) {
23f36d50 757 $usergroups = $this->ldap_find_user_groups($extmemberuid);
5704585c
I
758 if(count($usergroups) > 0) {
759 foreach ($usergroups as $group) {
21302343 760 $group = ldap_filter_addslashes($group);
5704585c
I
761 $ldap_search_pattern .= '('.$this->get_config('memberattribute_role'.$role->id).'='.$group.')';
762 }
763 }
764 }
765
766 // Default return value
767 $courses = array();
768
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 }
784
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.')';
794
795 // Get all contexts and look for first matching user
796 $ldap_contexts = explode(';', $ldap_contexts);
d349b0f0 797 $ldap_pagedresults = ldap_paged_results_supported($this->get_config('ldap_version'), $this->ldapconnection);
5704585c
I
798 foreach ($ldap_contexts as $context) {
799 $context = trim($context);
800 if (empty($context)) {
801 continue;
802 }
803
ed05ee6b 804 $ldap_cookie = '';
ba62f54e 805 $servercontrols = array();
afa23468 806 $flat_records = array();
c090d7c9
IA
807 do {
808 if ($ldap_pagedresults) {
74ee34fd 809 // TODO: Remove the old branch of code once PHP 7.3.0 becomes required (Moodle 3.11).
ba62f54e
EL
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 }
c090d7c9 819 }
5704585c 820
c090d7c9
IA
821 if ($this->get_config('course_search_sub')) {
822 // Use ldap_search to find first user from subtree
74ee34fd 823 // TODO: Remove the old branch of code once PHP 7.3.0 becomes required (Moodle 3.11).
ba62f54e
EL
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 }
c090d7c9
IA
832 } else {
833 // Search only in this context
74ee34fd 834 // TODO: Remove the old branch of code once PHP 7.3.0 becomes required (Moodle 3.11).
ba62f54e
EL
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 }
c090d7c9 843 }
5704585c 844
c090d7c9
IA
845 if (!$ldap_result) {
846 continue;
847 }
848
849 if ($ldap_pagedresults) {
ba62f54e
EL
850 // Get next server cookie to know if we'll need to continue searching.
851 $ldap_cookie = '';
74ee34fd 852 // TODO: Remove the old branch of code once PHP 7.3.0 becomes required (Moodle 3.11).
ba62f54e
EL
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 }
c090d7c9
IA
864 }
865
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.
23f36d50 869 $records = ldap_get_entries($this->ldapconnection, $ldap_result);
c090d7c9
IA
870
871 // LDAP libraries return an odd array, really. Fix it.
c090d7c9
IA
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));
5704585c 878
ee943e73 879 // If LDAP paged results were used, the current connection must be completely
c090d7c9
IA
880 // closed and a new one created, to work without paged results from here on.
881 if ($ldap_pagedresults) {
23f36d50
PS
882 $this->ldap_close();
883 $this->ldap_connect();
5704585c 884 }
5704585c
I
885
886 if (count($flat_records)) {
887 $courses = array_merge($courses, $flat_records);
888 }
889 }
890
891 return $courses;
892 }
893
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 *
5704585c
I
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 */
23f36d50 902 protected function ldap_find_userdn($userid) {
5704585c
I
903 global $CFG;
904 require_once($CFG->libdir.'/ldaplib.php');
905
906 $ldap_contexts = explode(';', $this->get_config('user_contexts'));
5704585c 907
23f36d50 908 return ldap_find_userdn($this->ldapconnection, $userid, $ldap_contexts,
abedeb8c 909 $this->userobjectclass,
5704585c
I
910 $this->get_config('idnumber_attribute'), $this->get_config('user_search_sub'));
911 }
912
913 /**
914 * Find the groups a given distinguished name belongs to, both directly
915 * and indirectly via nested groups membership.
916 *
5704585c
I
917 * @param string $memberdn distinguished name to search
918 * @return array with member groups' distinguished names (can be emtpy)
919 */
23f36d50 920 protected function ldap_find_user_groups($memberdn) {
5704585c
I
921 $groups = array();
922
23f36d50 923 $this->ldap_find_user_groups_recursively($memberdn, $groups);
5704585c
I
924 return $groups;
925 }
926
927 /**
928 * Recursively process the groups the given member distinguished name
929 * belongs to, adding them to the already processed groups array.
930 *
5704585c
I
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 */
23f36d50
PS
936 protected function ldap_find_user_groups_recursively($memberdn, &$membergroups) {
937 $result = @ldap_read($this->ldapconnection, $memberdn, '(objectClass=*)', array($this->get_config('group_memberofattribute')));
5704585c
I
938 if (!$result) {
939 return;
940 }
941
23f36d50 942 if ($entry = ldap_first_entry($this->ldapconnection, $result)) {
5704585c 943 do {
23f36d50 944 $attributes = ldap_get_attributes($this->ldapconnection, $entry);
5704585c 945 for ($j = 0; $j < $attributes['count']; $j++) {
23f36d50 946 $groups = ldap_get_values_len($this->ldapconnection, $entry, $attributes[$j]);
5704585c
I
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);
23f36d50 955 $this->ldap_find_user_groups_recursively($group, $membergroups);
5704585c
I
956 }
957 }
958 }
959 }
23f36d50 960 while ($entry = ldap_next_entry($this->ldapconnection, $entry));
5704585c
I
961 }
962 }
963
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 *
5704585c
I
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 */
23f36d50 975 protected function ldap_explode_group($group, $memberattribute) {
5704585c
I
976 switch ($this->get_config('user_type')) {
977 case 'ad':
978 // $group is already the distinguished name to search.
979 $dn = $group;
980
23f36d50
PS
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');
5704585c
I
984
985 if (!in_array('group', $objectclass)) {
034ef761 986 // Not a group, so return immediately.
5704585c
I
987 return array($group);
988 }
989
23f36d50
PS
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
5704585c
I
993 if ($members['count'] == 0) {
994 // There are no members in this group, return nothing.
995 return array();
996 }
997 unset($members['count']);
998
999 $users = array();
1000 foreach ($members as $member) {
23f36d50 1001 $group_members = $this->ldap_explode_group($member, $memberattribute);
5704585c
I
1002 $users = array_merge($users, $group_members);
1003 }
1004
1005 return ($users);
1006 break;
1007 default:
1008 error_log($this->errorlogtag.get_string('explodegroupusertypenotsupported', 'enrol_ldap',
1009 $this->get_config('user_type_name')));
1010
1011 return array($group);
1012 }
1013 }
1014
1015 /**
1016 * Will create the moodle course from the template
1017 * course_ext is an array as obtained from ldap -- flattened somewhat
5704585c
I
1018 *
1019 * @param array $course_ext
e34beca1 1020 * @param progress_trace $trace
5704585c
I
1021 * @return mixed false on error, id for the newly created course otherwise.
1022 */
e34beca1 1023 function create_course($course_ext, progress_trace $trace) {
5704585c
I
1024 global $CFG, $DB;
1025
1026 require_once("$CFG->dirroot/course/lib.php");
1027
1028 // Override defaults with template course
433150f6 1029 $template = false;
5704585c 1030 if ($this->get_config('template')) {
433150f6 1031 if ($template = $DB->get_record('course', array('shortname'=>$this->get_config('template')))) {
fc79ede5 1032 $template = fullclone(course_get_format($template)->get_course());
5704585c
I
1033 unset($template->id); // So we are clear to reinsert the record
1034 unset($template->fullname);
1035 unset($template->shortname);
1036 unset($template->idnumber);
5704585c
I
1037 }
1038 }
433150f6
PS
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;
433150f6
PS
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;
ee2d1451 1053 $template->enablecompletion = $courseconfig->enablecompletion;
433150f6
PS
1054 }
1055 $course = $template;
5704585c
I
1056
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 }
1063
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!
e34beca1 1070 $trace->output(get_string('cannotcreatecourse', 'enrol_ldap').' '.var_export($course, true));
5704585c
I
1071 return false;
1072 }
1073
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 }
1080
fdb04f62
NB
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 }
1086
5704585c
I
1087 $newcourse = create_course($course);
1088 return $newcourse->id;
1089 }
e34beca1 1090
d8a55f59
NB
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;
1103
1104 $coursefields = array ('shortname', 'fullname', 'summary');
1105 static $shouldupdate;
1106
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 }
1114
1115 // If we should not update return immediately.
1116 if (!$shouldupdate) {
1117 return false;
1118 }
1119
1120 require_once("$CFG->dirroot/course/lib.php");
1121 $courseupdated = false;
1122 $updatedcourse = new stdClass();
1123 $updatedcourse->id = $course->id;
1124
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 }
1135
1136 if (!$courseupdated) {
1137 $trace->output(get_string('courseupdateskipped', 'enrol_ldap', $course));
1138 return false;
1139 }
1140
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 }
1148
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 }
1155
1156 // Finally - update course in DB.
1157 update_course($updatedcourse);
1158 $trace->output(get_string('courseupdated', 'enrol_ldap', $course));
1159
1160 return true;
1161 }
1162
e34beca1
PS
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 }
1174
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 }
1194
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;
1206
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.
1209
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 }
1214
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 }
1221
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;
1232
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 }
1237
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 }
5704585c 1243}