See Release Notes
Long Term Support Release
Differences Between: [Versions 39 and 310] [Versions 39 and 311] [Versions 39 and 400] [Versions 39 and 401] [Versions 39 and 402] [Versions 39 and 403]
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/>. 16 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 */ 28 29 defined('MOODLE_INTERNAL') || die(); 30 31 class enrol_ldap_plugin extends enrol_plugin { 32 protected $enrol_localcoursefield = 'idnumber'; 33 protected $enroltype = 'enrol_ldap'; 34 protected $errorlogtag = '[ENROL LDAP] '; 35 36 /** 37 * The object class to use when finding users. 38 * 39 * @var string $userobjectclass 40 */ 41 protected $userobjectclass; 42 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(); 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. 76 unset($default['objectclass']); 77 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 } 86 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 } 102 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 } 114 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 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 } 137 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 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 } 154 155 if (!$this->ldap_connect($trace)) { 156 $trace->finished(); 157 return; 158 } 159 160 if (!is_object($user) or !property_exists($user, 'id')) { 161 throw new coding_exception('Invalid $user parameter in sync_user_enrolments()'); 162 } 163 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 } 168 169 // We may need a lot of memory here 170 core_php_time_limit::raise(); 171 raise_memory_limit(MEMORY_HUGE); 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 178 $enrolments[$role->id]['ext'] = $this->find_ext_enrolments($user->idnumber, $role); 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)) { 198 $trace->output(get_string('extcourseidinvalid', 'enrol_ldap')); 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 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 } 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))) { 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 } 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)); 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 } 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) { 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 } 309 310 $this->ldap_close(); 311 312 $trace->finished(); 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 * 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; 325 326 if (!$this->ldap_connect($trace)) { 327 $trace->finished(); 328 return; 329 } 330 331 $ldap_pagedresults = ldap_paged_results_supported($this->get_config('ldap_version'), $this->ldapconnection); 332 333 // we may need a lot of memory here 334 core_php_time_limit::raise(); 335 raise_memory_limit(MEMORY_HUGE); 336 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 } 352 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 377 if ($oneidnumber !== null) { 378 $ldap_search_pattern = "(&$ldap_search_pattern({$this->config->course_idnumber}=$oneidnumber))"; 379 } 380 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 } 388 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 4.1). 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 } 403 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 4.1). 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 4.1). 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 } 430 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 4.1). 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 } 447 448 // Check and push results 449 $records = ldap_get_entries($this->ldapconnection, $ldap_result); 450 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)); 458 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 } 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); 470 $idnumber = $course[$this->config->course_idnumber][0]; 471 $trace->output(get_string('synccourserole', 'enrol_ldap', array('idnumber'=>$idnumber, 'role_shortname'=>$role->shortname))); 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 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 } 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 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! 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) { 509 $grpusers = $this->ldap_explode_group($ldapmember, 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) { 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 } 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); 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); 560 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 } 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))) { 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 } 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 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 } 632 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); 639 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))); 653 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 } 679 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'); 690 691 if (isset($this->ldapconnection)) { 692 return true; 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'), 698 $debuginfo, $this->get_config('start_tls'))) { 699 $this->ldapconnection = $ldapconnection; 700 return true; 701 } 702 703 if ($trace) { 704 $trace->output($debuginfo); 705 } else { 706 error_log($this->errorlogtag.$debuginfo); 707 } 708 709 return false; 710 } 711 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 } 723 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'); 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 747 $extmemberuid = core_text::convert($memberuid, 'utf-8', $this->get_config('ldapencoding')); 748 749 if($this->get_config('memberattribute_isdn')) { 750 if (!($extmemberuid = $this->ldap_find_userdn($extmemberuid))) { 751 return array(); 752 } 753 } 754 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 } 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); 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 } 803 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 4.1). 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 } 820 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 4.1). 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 4.1). 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 } 844 845 if (!$ldap_result) { 846 continue; 847 } 848 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 4.1). 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 } 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. 869 $records = ldap_get_entries($this->ldapconnection, $ldap_result); 870 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)); 878 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 } 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 * 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'); 905 906 $ldap_contexts = explode(';', $this->get_config('user_contexts')); 907 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 } 912 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(); 922 923 $this->ldap_find_user_groups_recursively($memberdn, $groups); 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 * 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 } 941 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 } 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 * 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; 980 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'); 984 985 if (!in_array('group', $objectclass)) { 986 // Not a group, so return immediately. 987 return array($group); 988 } 989 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']); 998 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 } 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 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; 1025 1026 require_once("$CFG->dirroot/course/lib.php"); 1027 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; 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! 1070 $trace->output(get_string('cannotcreatecourse', 'enrol_ldap').' '.var_export($course, true)); 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 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 1087 $newcourse = create_course($course); 1088 return $newcourse->id; 1089 } 1090 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 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 } 1243 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body