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