[ Index ] |
PHP Cross Reference of Unnamed Project |
[Summary view] [Print] [Text view]
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:config', $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')); 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 foreach ($ldap_contexts as $ldap_context) { 383 $ldap_context = trim($ldap_context); 384 if (empty($ldap_context)) { 385 continue; // Next; 386 } 387 388 $flat_records = array(); 389 do { 390 if ($ldap_pagedresults) { 391 ldap_control_paged_result($this->ldapconnection, $this->config->pagesize, true, $ldap_cookie); 392 } 393 394 if ($this->config->course_search_sub) { 395 // Use ldap_search to find first user from subtree 396 $ldap_result = @ldap_search($this->ldapconnection, 397 $ldap_context, 398 $ldap_search_pattern, 399 $ldap_fields_wanted); 400 } else { 401 // Search only in this context 402 $ldap_result = @ldap_list($this->ldapconnection, 403 $ldap_context, 404 $ldap_search_pattern, 405 $ldap_fields_wanted); 406 } 407 if (!$ldap_result) { 408 continue; // Next 409 } 410 411 if ($ldap_pagedresults) { 412 ldap_control_paged_result_response($this->ldapconnection, $ldap_result, $ldap_cookie); 413 } 414 415 // Check and push results 416 $records = ldap_get_entries($this->ldapconnection, $ldap_result); 417 418 // LDAP libraries return an odd array, really. fix it: 419 for ($c = 0; $c < $records['count']; $c++) { 420 array_push($flat_records, $records[$c]); 421 } 422 // Free some mem 423 unset($records); 424 } while ($ldap_pagedresults && !empty($ldap_cookie)); 425 426 // If LDAP paged results were used, the current connection must be completely 427 // closed and a new one created, to work without paged results from here on. 428 if ($ldap_pagedresults) { 429 $this->ldap_close(); 430 $this->ldap_connect($trace); 431 } 432 433 if (count($flat_records)) { 434 $ignorehidden = $this->get_config('ignorehiddencourses'); 435 foreach($flat_records as $course) { 436 $course = array_change_key_case($course, CASE_LOWER); 437 $idnumber = $course{$this->config->course_idnumber}[0]; 438 $trace->output(get_string('synccourserole', 'enrol_ldap', array('idnumber'=>$idnumber, 'role_shortname'=>$role->shortname))); 439 440 // Does the course exist in moodle already? 441 $course_obj = $DB->get_record('course', array($this->enrol_localcoursefield=>$idnumber)); 442 if (empty($course_obj)) { // Course doesn't exist 443 if ($this->get_config('autocreate')) { // Autocreate 444 $trace->output(get_string('createcourseextid', 'enrol_ldap', array('courseextid'=>$idnumber))); 445 if (!$newcourseid = $this->create_course($course, $trace)) { 446 continue; 447 } 448 $course_obj = $DB->get_record('course', array('id'=>$newcourseid)); 449 } else { 450 $trace->output(get_string('createnotcourseextid', 'enrol_ldap', array('courseextid'=>$idnumber))); 451 continue; // Next; skip this one! 452 } 453 } else { // Check if course needs update & update as needed. 454 $this->update_course($course_obj, $course, $trace); 455 } 456 457 // Enrol & unenrol 458 459 // Pull the ldap membership into a nice array 460 // this is an odd array -- mix of hash and array -- 461 $ldapmembers = array(); 462 463 if (array_key_exists('memberattribute_role'.$role->id, $this->config) 464 && !empty($this->config->{'memberattribute_role'.$role->id}) 465 && !empty($course[$this->config->{'memberattribute_role'.$role->id}])) { // May have no membership! 466 467 $ldapmembers = $course[$this->config->{'memberattribute_role'.$role->id}]; 468 unset($ldapmembers['count']); // Remove oddity ;) 469 470 // If we have enabled nested groups, we need to expand 471 // the groups to get the real user list. We need to do 472 // this before dealing with 'memberattribute_isdn'. 473 if ($this->config->nested_groups) { 474 $users = array(); 475 foreach ($ldapmembers as $ldapmember) { 476 $grpusers = $this->ldap_explode_group($ldapmember, 477 $this->config->{'memberattribute_role'.$role->id}); 478 479 $users = array_merge($users, $grpusers); 480 } 481 $ldapmembers = array_unique($users); // There might be duplicates. 482 } 483 484 // Deal with the case where the member attribute holds distinguished names, 485 // but only if the user attribute is not a distinguished name itself. 486 if ($this->config->memberattribute_isdn 487 && ($this->config->idnumber_attribute !== 'dn') 488 && ($this->config->idnumber_attribute !== 'distinguishedname')) { 489 // We need to retrieve the idnumber for all the users in $ldapmembers, 490 // as the idnumber does not match their dn and we get dn's from membership. 491 $memberidnumbers = array(); 492 foreach ($ldapmembers as $ldapmember) { 493 $result = ldap_read($this->ldapconnection, $ldapmember, $this->userobjectclass, 494 array($this->config->idnumber_attribute)); 495 $entry = ldap_first_entry($this->ldapconnection, $result); 496 $values = ldap_get_values($this->ldapconnection, $entry, $this->config->idnumber_attribute); 497 array_push($memberidnumbers, $values[0]); 498 } 499 500 $ldapmembers = $memberidnumbers; 501 } 502 } 503 504 // Prune old ldap enrolments 505 // hopefully they'll fit in the max buffer size for the RDBMS 506 $sql= "SELECT u.id as userid, u.username, ue.status, 507 ra.contextid, ra.itemid as instanceid 508 FROM {user} u 509 JOIN {role_assignments} ra ON (ra.userid = u.id AND ra.component = 'enrol_ldap' AND ra.roleid = :roleid) 510 JOIN {user_enrolments} ue ON (ue.userid = u.id AND ue.enrolid = ra.itemid) 511 JOIN {enrol} e ON (e.id = ue.enrolid) 512 WHERE u.deleted = 0 AND e.courseid = :courseid "; 513 $params = array('roleid'=>$role->id, 'courseid'=>$course_obj->id); 514 $context = context_course::instance($course_obj->id); 515 if (!empty($ldapmembers)) { 516 list($ldapml, $params2) = $DB->get_in_or_equal($ldapmembers, SQL_PARAMS_NAMED, 'm', false); 517 $sql .= "AND u.idnumber $ldapml"; 518 $params = array_merge($params, $params2); 519 unset($params2); 520 } else { 521 $shortname = format_string($course_obj->shortname, true, array('context' => $context)); 522 $trace->output(get_string('emptyenrolment', 'enrol_ldap', 523 array('role_shortname'=> $role->shortname, 524 'course_shortname' => $shortname))); 525 } 526 $todelete = $DB->get_records_sql($sql, $params); 527 528 if (!empty($todelete)) { 529 $transaction = $DB->start_delegated_transaction(); 530 foreach ($todelete as $row) { 531 $instance = $DB->get_record('enrol', array('id'=>$row->instanceid)); 532 switch ($this->get_config('unenrolaction')) { 533 case ENROL_EXT_REMOVED_UNENROL: 534 $this->unenrol_user($instance, $row->userid); 535 $trace->output(get_string('extremovedunenrol', 'enrol_ldap', 536 array('user_username'=> $row->username, 537 'course_shortname'=>$course_obj->shortname, 538 'course_id'=>$course_obj->id))); 539 break; 540 case ENROL_EXT_REMOVED_KEEP: 541 // Keep - only adding enrolments 542 break; 543 case ENROL_EXT_REMOVED_SUSPEND: 544 if ($row->status != ENROL_USER_SUSPENDED) { 545 $DB->set_field('user_enrolments', 'status', ENROL_USER_SUSPENDED, array('enrolid'=>$instance->id, 'userid'=>$row->userid)); 546 $trace->output(get_string('extremovedsuspend', 'enrol_ldap', 547 array('user_username'=> $row->username, 548 'course_shortname'=>$course_obj->shortname, 549 'course_id'=>$course_obj->id))); 550 } 551 break; 552 case ENROL_EXT_REMOVED_SUSPENDNOROLES: 553 if ($row->status != ENROL_USER_SUSPENDED) { 554 $DB->set_field('user_enrolments', 'status', ENROL_USER_SUSPENDED, array('enrolid'=>$instance->id, 'userid'=>$row->userid)); 555 } 556 role_unassign_all(array('contextid'=>$row->contextid, 'userid'=>$row->userid, 'component'=>'enrol_ldap', 'itemid'=>$instance->id)); 557 $trace->output(get_string('extremovedsuspendnoroles', 'enrol_ldap', 558 array('user_username'=> $row->username, 559 'course_shortname'=>$course_obj->shortname, 560 'course_id'=>$course_obj->id))); 561 break; 562 } 563 } 564 $transaction->allow_commit(); 565 } 566 567 // Insert current enrolments 568 // bad we can't do INSERT IGNORE with postgres... 569 570 // Add necessary enrol instance if not present yet; 571 $sql = "SELECT c.id, c.visible, e.id as enrolid 572 FROM {course} c 573 JOIN {enrol} e ON (e.courseid = c.id AND e.enrol = 'ldap') 574 WHERE c.id = :courseid"; 575 $params = array('courseid'=>$course_obj->id); 576 if (!($course_instance = $DB->get_record_sql($sql, $params, IGNORE_MULTIPLE))) { 577 $course_instance = new stdClass(); 578 $course_instance->id = $course_obj->id; 579 $course_instance->visible = $course_obj->visible; 580 $course_instance->enrolid = $this->add_instance($course_instance); 581 } 582 583 if (!$instance = $DB->get_record('enrol', array('id'=>$course_instance->enrolid))) { 584 continue; // Weird; skip this one. 585 } 586 587 if ($ignorehidden && !$course_instance->visible) { 588 continue; 589 } 590 591 $transaction = $DB->start_delegated_transaction(); 592 foreach ($ldapmembers as $ldapmember) { 593 $sql = 'SELECT id,username,1 FROM {user} WHERE idnumber = ? AND deleted = 0'; 594 $member = $DB->get_record_sql($sql, array($ldapmember)); 595 if(empty($member) || empty($member->id)){ 596 $trace->output(get_string('couldnotfinduser', 'enrol_ldap', $ldapmember)); 597 continue; 598 } 599 600 $sql= "SELECT ue.status 601 FROM {user_enrolments} ue 602 JOIN {enrol} e ON (e.id = ue.enrolid AND e.enrol = 'ldap') 603 WHERE e.courseid = :courseid AND ue.userid = :userid"; 604 $params = array('courseid'=>$course_obj->id, 'userid'=>$member->id); 605 $userenrolment = $DB->get_record_sql($sql, $params, IGNORE_MULTIPLE); 606 607 if (empty($userenrolment)) { 608 $this->enrol_user($instance, $member->id, $role->id); 609 // Make sure we set the enrolment status to active. If the user wasn't 610 // previously enrolled to the course, enrol_user() sets it. But if we 611 // configured the plugin to suspend the user enrolments _AND_ remove 612 // the role assignments on external unenrol, then enrol_user() doesn't 613 // set it back to active on external re-enrolment. So set it 614 // unconditionally to cover both cases. 615 $DB->set_field('user_enrolments', 'status', ENROL_USER_ACTIVE, array('enrolid'=>$instance->id, 'userid'=>$member->id)); 616 $trace->output(get_string('enroluser', 'enrol_ldap', 617 array('user_username'=> $member->username, 618 'course_shortname'=>$course_obj->shortname, 619 'course_id'=>$course_obj->id))); 620 621 } else { 622 if (!$DB->record_exists('role_assignments', array('roleid'=>$role->id, 'userid'=>$member->id, 'contextid'=>$context->id, 'component'=>'enrol_ldap', 'itemid'=>$instance->id))) { 623 // This happens when reviving users or when user has multiple roles in one course. 624 $context = context_course::instance($course_obj->id); 625 role_assign($role->id, $member->id, $context->id, 'enrol_ldap', $instance->id); 626 $trace->output("Assign role to user '$member->username' in course '$course_obj->shortname ($course_obj->id)'"); 627 } 628 if ($userenrolment->status == ENROL_USER_SUSPENDED) { 629 // Reenable enrolment that was previously disabled. Enrolment refreshed 630 $DB->set_field('user_enrolments', 'status', ENROL_USER_ACTIVE, array('enrolid'=>$instance->id, 'userid'=>$member->id)); 631 $trace->output(get_string('enroluserenable', 'enrol_ldap', 632 array('user_username'=> $member->username, 633 'course_shortname'=>$course_obj->shortname, 634 'course_id'=>$course_obj->id))); 635 } 636 } 637 } 638 $transaction->allow_commit(); 639 } 640 } 641 } 642 } 643 @$this->ldap_close(); 644 $trace->finished(); 645 } 646 647 /** 648 * Connect to the LDAP server, using the plugin configured 649 * settings. It's actually a wrapper around ldap_connect_moodle() 650 * 651 * @param progress_trace $trace 652 * @return bool success 653 */ 654 protected function ldap_connect(progress_trace $trace = null) { 655 global $CFG; 656 require_once($CFG->libdir.'/ldaplib.php'); 657 658 if (isset($this->ldapconnection)) { 659 return true; 660 } 661 662 if ($ldapconnection = ldap_connect_moodle($this->get_config('host_url'), $this->get_config('ldap_version'), 663 $this->get_config('user_type'), $this->get_config('bind_dn'), 664 $this->get_config('bind_pw'), $this->get_config('opt_deref'), 665 $debuginfo, $this->get_config('start_tls'))) { 666 $this->ldapconnection = $ldapconnection; 667 return true; 668 } 669 670 if ($trace) { 671 $trace->output($debuginfo); 672 } else { 673 error_log($this->errorlogtag.$debuginfo); 674 } 675 676 return false; 677 } 678 679 /** 680 * Disconnects from a LDAP server 681 * 682 */ 683 protected function ldap_close() { 684 if (isset($this->ldapconnection)) { 685 @ldap_close($this->ldapconnection); 686 $this->ldapconnection = null; 687 } 688 return; 689 } 690 691 /** 692 * Return multidimensional array with details of user courses (at 693 * least dn and idnumber). 694 * 695 * @param string $memberuid user idnumber (without magic quotes). 696 * @param object role is a record from the mdl_role table. 697 * @return array 698 */ 699 protected function find_ext_enrolments($memberuid, $role) { 700 global $CFG; 701 require_once($CFG->libdir.'/ldaplib.php'); 702 703 if (empty($memberuid)) { 704 // No "idnumber" stored for this user, so no LDAP enrolments 705 return array(); 706 } 707 708 $ldap_contexts = trim($this->get_config('contexts_role'.$role->id)); 709 if (empty($ldap_contexts)) { 710 // No role contexts, so no LDAP enrolments 711 return array(); 712 } 713 714 $extmemberuid = core_text::convert($memberuid, 'utf-8', $this->get_config('ldapencoding')); 715 716 if($this->get_config('memberattribute_isdn')) { 717 if (!($extmemberuid = $this->ldap_find_userdn($extmemberuid))) { 718 return array(); 719 } 720 } 721 722 $ldap_search_pattern = ''; 723 if($this->get_config('nested_groups')) { 724 $usergroups = $this->ldap_find_user_groups($extmemberuid); 725 if(count($usergroups) > 0) { 726 foreach ($usergroups as $group) { 727 $group = ldap_filter_addslashes($group); 728 $ldap_search_pattern .= '('.$this->get_config('memberattribute_role'.$role->id).'='.$group.')'; 729 } 730 } 731 } 732 733 // Default return value 734 $courses = array(); 735 736 // Get all the fields we will want for the potential course creation 737 // as they are light. don't get membership -- potentially a lot of data. 738 $ldap_fields_wanted = array('dn', $this->get_config('course_idnumber')); 739 $fullname = $this->get_config('course_fullname'); 740 $shortname = $this->get_config('course_shortname'); 741 $summary = $this->get_config('course_summary'); 742 if (isset($fullname)) { 743 array_push($ldap_fields_wanted, $fullname); 744 } 745 if (isset($shortname)) { 746 array_push($ldap_fields_wanted, $shortname); 747 } 748 if (isset($summary)) { 749 array_push($ldap_fields_wanted, $summary); 750 } 751 752 // Define the search pattern 753 if (empty($ldap_search_pattern)) { 754 $ldap_search_pattern = '('.$this->get_config('memberattribute_role'.$role->id).'='.ldap_filter_addslashes($extmemberuid).')'; 755 } else { 756 $ldap_search_pattern = '(|' . $ldap_search_pattern . 757 '('.$this->get_config('memberattribute_role'.$role->id).'='.ldap_filter_addslashes($extmemberuid).')' . 758 ')'; 759 } 760 $ldap_search_pattern='(&'.$this->get_config('objectclass').$ldap_search_pattern.')'; 761 762 // Get all contexts and look for first matching user 763 $ldap_contexts = explode(';', $ldap_contexts); 764 $ldap_pagedresults = ldap_paged_results_supported($this->get_config('ldap_version')); 765 foreach ($ldap_contexts as $context) { 766 $context = trim($context); 767 if (empty($context)) { 768 continue; 769 } 770 771 $ldap_cookie = ''; 772 $flat_records = array(); 773 do { 774 if ($ldap_pagedresults) { 775 ldap_control_paged_result($this->ldapconnection, $this->config->pagesize, true, $ldap_cookie); 776 } 777 778 if ($this->get_config('course_search_sub')) { 779 // Use ldap_search to find first user from subtree 780 $ldap_result = @ldap_search($this->ldapconnection, 781 $context, 782 $ldap_search_pattern, 783 $ldap_fields_wanted); 784 } else { 785 // Search only in this context 786 $ldap_result = @ldap_list($this->ldapconnection, 787 $context, 788 $ldap_search_pattern, 789 $ldap_fields_wanted); 790 } 791 792 if (!$ldap_result) { 793 continue; 794 } 795 796 if ($ldap_pagedresults) { 797 ldap_control_paged_result_response($this->ldapconnection, $ldap_result, $ldap_cookie); 798 } 799 800 // Check and push results. ldap_get_entries() already 801 // lowercases the attribute index, so there's no need to 802 // use array_change_key_case() later. 803 $records = ldap_get_entries($this->ldapconnection, $ldap_result); 804 805 // LDAP libraries return an odd array, really. Fix it. 806 for ($c = 0; $c < $records['count']; $c++) { 807 array_push($flat_records, $records[$c]); 808 } 809 // Free some mem 810 unset($records); 811 } while ($ldap_pagedresults && !empty($ldap_cookie)); 812 813 // If LDAP paged results were used, the current connection must be completely 814 // closed and a new one created, to work without paged results from here on. 815 if ($ldap_pagedresults) { 816 $this->ldap_close(); 817 $this->ldap_connect(); 818 } 819 820 if (count($flat_records)) { 821 $courses = array_merge($courses, $flat_records); 822 } 823 } 824 825 return $courses; 826 } 827 828 /** 829 * Search specified contexts for the specified userid and return the 830 * user dn like: cn=username,ou=suborg,o=org. It's actually a wrapper 831 * around ldap_find_userdn(). 832 * 833 * @param string $userid the userid to search for (in external LDAP encoding, no magic quotes). 834 * @return mixed the user dn or false 835 */ 836 protected function ldap_find_userdn($userid) { 837 global $CFG; 838 require_once($CFG->libdir.'/ldaplib.php'); 839 840 $ldap_contexts = explode(';', $this->get_config('user_contexts')); 841 842 return ldap_find_userdn($this->ldapconnection, $userid, $ldap_contexts, 843 $this->userobjectclass, 844 $this->get_config('idnumber_attribute'), $this->get_config('user_search_sub')); 845 } 846 847 /** 848 * Find the groups a given distinguished name belongs to, both directly 849 * and indirectly via nested groups membership. 850 * 851 * @param string $memberdn distinguished name to search 852 * @return array with member groups' distinguished names (can be emtpy) 853 */ 854 protected function ldap_find_user_groups($memberdn) { 855 $groups = array(); 856 857 $this->ldap_find_user_groups_recursively($memberdn, $groups); 858 return $groups; 859 } 860 861 /** 862 * Recursively process the groups the given member distinguished name 863 * belongs to, adding them to the already processed groups array. 864 * 865 * @param string $memberdn distinguished name to search 866 * @param array reference &$membergroups array with already found 867 * groups, where we'll put the newly found 868 * groups. 869 */ 870 protected function ldap_find_user_groups_recursively($memberdn, &$membergroups) { 871 $result = @ldap_read($this->ldapconnection, $memberdn, '(objectClass=*)', array($this->get_config('group_memberofattribute'))); 872 if (!$result) { 873 return; 874 } 875 876 if ($entry = ldap_first_entry($this->ldapconnection, $result)) { 877 do { 878 $attributes = ldap_get_attributes($this->ldapconnection, $entry); 879 for ($j = 0; $j < $attributes['count']; $j++) { 880 $groups = ldap_get_values_len($this->ldapconnection, $entry, $attributes[$j]); 881 foreach ($groups as $key => $group) { 882 if ($key === 'count') { // Skip the entries count 883 continue; 884 } 885 if(!in_array($group, $membergroups)) { 886 // Only push and recurse if we haven't 'seen' this group before 887 // to prevent loops (MS Active Directory allows them!!). 888 array_push($membergroups, $group); 889 $this->ldap_find_user_groups_recursively($group, $membergroups); 890 } 891 } 892 } 893 } 894 while ($entry = ldap_next_entry($this->ldapconnection, $entry)); 895 } 896 } 897 898 /** 899 * Given a group name (either a RDN or a DN), get the list of users 900 * belonging to that group. If the group has nested groups, expand all 901 * the intermediate groups and return the full list of users that 902 * directly or indirectly belong to the group. 903 * 904 * @param string $group the group name to search 905 * @param string $memberattibute the attribute that holds the members of the group 906 * @return array the list of users belonging to the group. If $group 907 * is not actually a group, returns array($group). 908 */ 909 protected function ldap_explode_group($group, $memberattribute) { 910 switch ($this->get_config('user_type')) { 911 case 'ad': 912 // $group is already the distinguished name to search. 913 $dn = $group; 914 915 $result = ldap_read($this->ldapconnection, $dn, '(objectClass=*)', array('objectClass')); 916 $entry = ldap_first_entry($this->ldapconnection, $result); 917 $objectclass = ldap_get_values($this->ldapconnection, $entry, 'objectClass'); 918 919 if (!in_array('group', $objectclass)) { 920 // Not a group, so return immediately. 921 return array($group); 922 } 923 924 $result = ldap_read($this->ldapconnection, $dn, '(objectClass=*)', array($memberattribute)); 925 $entry = ldap_first_entry($this->ldapconnection, $result); 926 $members = @ldap_get_values($this->ldapconnection, $entry, $memberattribute); // Can be empty and throws a warning 927 if ($members['count'] == 0) { 928 // There are no members in this group, return nothing. 929 return array(); 930 } 931 unset($members['count']); 932 933 $users = array(); 934 foreach ($members as $member) { 935 $group_members = $this->ldap_explode_group($member, $memberattribute); 936 $users = array_merge($users, $group_members); 937 } 938 939 return ($users); 940 break; 941 default: 942 error_log($this->errorlogtag.get_string('explodegroupusertypenotsupported', 'enrol_ldap', 943 $this->get_config('user_type_name'))); 944 945 return array($group); 946 } 947 } 948 949 /** 950 * Will create the moodle course from the template 951 * course_ext is an array as obtained from ldap -- flattened somewhat 952 * 953 * @param array $course_ext 954 * @param progress_trace $trace 955 * @return mixed false on error, id for the newly created course otherwise. 956 */ 957 function create_course($course_ext, progress_trace $trace) { 958 global $CFG, $DB; 959 960 require_once("$CFG->dirroot/course/lib.php"); 961 962 // Override defaults with template course 963 $template = false; 964 if ($this->get_config('template')) { 965 if ($template = $DB->get_record('course', array('shortname'=>$this->get_config('template')))) { 966 $template = fullclone(course_get_format($template)->get_course()); 967 unset($template->id); // So we are clear to reinsert the record 968 unset($template->fullname); 969 unset($template->shortname); 970 unset($template->idnumber); 971 } 972 } 973 if (!$template) { 974 $courseconfig = get_config('moodlecourse'); 975 $template = new stdClass(); 976 $template->summary = ''; 977 $template->summaryformat = FORMAT_HTML; 978 $template->format = $courseconfig->format; 979 $template->newsitems = $courseconfig->newsitems; 980 $template->showgrades = $courseconfig->showgrades; 981 $template->showreports = $courseconfig->showreports; 982 $template->maxbytes = $courseconfig->maxbytes; 983 $template->groupmode = $courseconfig->groupmode; 984 $template->groupmodeforce = $courseconfig->groupmodeforce; 985 $template->visible = $courseconfig->visible; 986 $template->lang = $courseconfig->lang; 987 $template->enablecompletion = $courseconfig->enablecompletion; 988 } 989 $course = $template; 990 991 $course->category = $this->get_config('category'); 992 if (!$DB->record_exists('course_categories', array('id'=>$this->get_config('category')))) { 993 $categories = $DB->get_records('course_categories', array(), 'sortorder', 'id', 0, 1); 994 $first = reset($categories); 995 $course->category = $first->id; 996 } 997 998 // Override with required ext data 999 $course->idnumber = $course_ext[$this->get_config('course_idnumber')][0]; 1000 $course->fullname = $course_ext[$this->get_config('course_fullname')][0]; 1001 $course->shortname = $course_ext[$this->get_config('course_shortname')][0]; 1002 if (empty($course->idnumber) || empty($course->fullname) || empty($course->shortname)) { 1003 // We are in trouble! 1004 $trace->output(get_string('cannotcreatecourse', 'enrol_ldap').' '.var_export($course, true)); 1005 return false; 1006 } 1007 1008 $summary = $this->get_config('course_summary'); 1009 if (!isset($summary) || empty($course_ext[$summary][0])) { 1010 $course->summary = ''; 1011 } else { 1012 $course->summary = $course_ext[$this->get_config('course_summary')][0]; 1013 } 1014 1015 // Check if the shortname already exists if it does - skip course creation. 1016 if ($DB->record_exists('course', array('shortname' => $course->shortname))) { 1017 $trace->output(get_string('duplicateshortname', 'enrol_ldap', $course)); 1018 return false; 1019 } 1020 1021 $newcourse = create_course($course); 1022 return $newcourse->id; 1023 } 1024 1025 /** 1026 * Will update a moodle course with new values from LDAP 1027 * A field will be updated only if it is marked to be updated 1028 * on sync in plugin settings 1029 * 1030 * @param object $course 1031 * @param array $externalcourse 1032 * @param progress_trace $trace 1033 * @return bool 1034 */ 1035 protected function update_course($course, $externalcourse, progress_trace $trace) { 1036 global $CFG, $DB; 1037 1038 $coursefields = array ('shortname', 'fullname', 'summary'); 1039 static $shouldupdate; 1040 1041 // Initialize $shouldupdate variable. Set to true if one or more fields are marked for update. 1042 if (!isset($shouldupdate)) { 1043 $shouldupdate = false; 1044 foreach ($coursefields as $field) { 1045 $shouldupdate = $shouldupdate || $this->get_config('course_'.$field.'_updateonsync'); 1046 } 1047 } 1048 1049 // If we should not update return immediately. 1050 if (!$shouldupdate) { 1051 return false; 1052 } 1053 1054 require_once("$CFG->dirroot/course/lib.php"); 1055 $courseupdated = false; 1056 $updatedcourse = new stdClass(); 1057 $updatedcourse->id = $course->id; 1058 1059 // Update course fields if necessary. 1060 foreach ($coursefields as $field) { 1061 // If field is marked to be updated on sync && field data was changed update it. 1062 if ($this->get_config('course_'.$field.'_updateonsync') 1063 && isset($externalcourse[$this->get_config('course_'.$field)][0]) 1064 && $course->{$field} != $externalcourse[$this->get_config('course_'.$field)][0]) { 1065 $updatedcourse->{$field} = $externalcourse[$this->get_config('course_'.$field)][0]; 1066 $courseupdated = true; 1067 } 1068 } 1069 1070 if (!$courseupdated) { 1071 $trace->output(get_string('courseupdateskipped', 'enrol_ldap', $course)); 1072 return false; 1073 } 1074 1075 // Do not allow empty fullname or shortname. 1076 if ((isset($updatedcourse->fullname) && empty($updatedcourse->fullname)) 1077 || (isset($updatedcourse->shortname) && empty($updatedcourse->shortname))) { 1078 // We are in trouble! 1079 $trace->output(get_string('cannotupdatecourse', 'enrol_ldap', $course)); 1080 return false; 1081 } 1082 1083 // Check if the shortname already exists if it does - skip course updating. 1084 if (isset($updatedcourse->shortname) 1085 && $DB->record_exists('course', array('shortname' => $updatedcourse->shortname))) { 1086 $trace->output(get_string('cannotupdatecourse_duplicateshortname', 'enrol_ldap', $course)); 1087 return false; 1088 } 1089 1090 // Finally - update course in DB. 1091 update_course($updatedcourse); 1092 $trace->output(get_string('courseupdated', 'enrol_ldap', $course)); 1093 1094 return true; 1095 } 1096 1097 /** 1098 * Automatic enrol sync executed during restore. 1099 * Useful for automatic sync by course->idnumber or course category. 1100 * @param stdClass $course course record 1101 */ 1102 public function restore_sync_course($course) { 1103 // TODO: this can not work because restore always nukes the course->idnumber, do not ask me why (MDL-37312) 1104 // NOTE: for now restore does not do any real logging yet, let's do the same here... 1105 $trace = new error_log_progress_trace(); 1106 $this->sync_enrolments($trace, $course->id); 1107 } 1108 1109 /** 1110 * Restore instance and map settings. 1111 * 1112 * @param restore_enrolments_structure_step $step 1113 * @param stdClass $data 1114 * @param stdClass $course 1115 * @param int $oldid 1116 */ 1117 public function restore_instance(restore_enrolments_structure_step $step, stdClass $data, $course, $oldid) { 1118 global $DB; 1119 // There is only 1 ldap enrol instance per course. 1120 if ($instances = $DB->get_records('enrol', array('courseid'=>$data->courseid, 'enrol'=>'ldap'), 'id')) { 1121 $instance = reset($instances); 1122 $instanceid = $instance->id; 1123 } else { 1124 $instanceid = $this->add_instance($course, (array)$data); 1125 } 1126 $step->set_mapping('enrol', $oldid, $instanceid); 1127 } 1128 1129 /** 1130 * Restore user enrolment. 1131 * 1132 * @param restore_enrolments_structure_step $step 1133 * @param stdClass $data 1134 * @param stdClass $instance 1135 * @param int $oldinstancestatus 1136 * @param int $userid 1137 */ 1138 public function restore_user_enrolment(restore_enrolments_structure_step $step, $data, $instance, $userid, $oldinstancestatus) { 1139 global $DB; 1140 1141 if ($this->get_config('unenrolaction') == ENROL_EXT_REMOVED_UNENROL) { 1142 // Enrolments were already synchronised in restore_instance(), we do not want any suspended leftovers. 1143 1144 } else if ($this->get_config('unenrolaction') == ENROL_EXT_REMOVED_KEEP) { 1145 if (!$DB->record_exists('user_enrolments', array('enrolid'=>$instance->id, 'userid'=>$userid))) { 1146 $this->enrol_user($instance, $userid, null, 0, 0, $data->status); 1147 } 1148 1149 } else { 1150 if (!$DB->record_exists('user_enrolments', array('enrolid'=>$instance->id, 'userid'=>$userid))) { 1151 $this->enrol_user($instance, $userid, null, 0, 0, ENROL_USER_SUSPENDED); 1152 } 1153 } 1154 } 1155 1156 /** 1157 * Restore role assignment. 1158 * 1159 * @param stdClass $instance 1160 * @param int $roleid 1161 * @param int $userid 1162 * @param int $contextid 1163 */ 1164 public function restore_role_assignment($instance, $roleid, $userid, $contextid) { 1165 global $DB; 1166 1167 if ($this->get_config('unenrolaction') == ENROL_EXT_REMOVED_UNENROL or $this->get_config('unenrolaction') == ENROL_EXT_REMOVED_SUSPENDNOROLES) { 1168 // Skip any roles restore, they should be already synced automatically. 1169 return; 1170 } 1171 1172 // Just restore every role. 1173 if ($DB->record_exists('user_enrolments', array('enrolid'=>$instance->id, 'userid'=>$userid))) { 1174 role_assign($roleid, $userid, $contextid, 'enrol_'.$instance->enrol, $instance->id); 1175 } 1176 } 1177 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
Generated: Thu Aug 11 10:00:09 2016 | Cross-referenced by PHPXref 0.7.1 |