[ Index ] |
PHP Cross Reference of moodle-2.8 |
[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 <[email protected]> 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 * Constructor for the plugin. In addition to calling the parent 38 * constructor, we define and 'fix' some settings depending on the 39 * real settings the admin defined. 40 */ 41 public function __construct() { 42 global $CFG; 43 require_once($CFG->libdir.'/ldaplib.php'); 44 45 // Do our own stuff to fix the config (it's easier to do it 46 // here than using the admin settings infrastructure). We 47 // don't call $this->set_config() for any of the 'fixups' 48 // (except the objectclass, as it's critical) because the user 49 // didn't specify any values and relied on the default values 50 // defined for the user type she chose. 51 $this->load_config(); 52 53 // Make sure we get sane defaults for critical values. 54 $this->config->ldapencoding = $this->get_config('ldapencoding', 'utf-8'); 55 $this->config->user_type = $this->get_config('user_type', 'default'); 56 57 $ldap_usertypes = ldap_supported_usertypes(); 58 $this->config->user_type_name = $ldap_usertypes[$this->config->user_type]; 59 unset($ldap_usertypes); 60 61 $default = ldap_getdefaults(); 62 // Remove the objectclass default, as the values specified there are for 63 // users, and we are dealing with groups here. 64 unset($default['objectclass']); 65 66 // Use defaults if values not given. Dont use this->get_config() 67 // here to be able to check for 0 and false values too. 68 foreach ($default as $key => $value) { 69 // Watch out - 0, false are correct values too, so we can't use $this->get_config() 70 if (!isset($this->config->{$key}) or $this->config->{$key} == '') { 71 $this->config->{$key} = $value[$this->config->user_type]; 72 } 73 } 74 75 if (empty($this->config->objectclass)) { 76 // Can't send empty filter. Fix it for now and future occasions 77 $this->set_config('objectclass', '(objectClass=*)'); 78 } else if (stripos($this->config->objectclass, 'objectClass=') === 0) { 79 // Value is 'objectClass=some-string-here', so just add () 80 // around the value (filter _must_ have them). 81 // Fix it for now and future occasions 82 $this->set_config('objectclass', '('.$this->config->objectclass.')'); 83 } else if (stripos($this->config->objectclass, '(') !== 0) { 84 // Value is 'some-string-not-starting-with-left-parentheses', 85 // which is assumed to be the objectClass matching value. 86 // So build a valid filter with it. 87 $this->set_config('objectclass', '(objectClass='.$this->config->objectclass.')'); 88 } else { 89 // There is an additional possible value 90 // '(some-string-here)', that can be used to specify any 91 // valid filter string, to select subsets of users based 92 // on any criteria. For example, we could select the users 93 // whose objectClass is 'user' and have the 94 // 'enabledMoodleUser' attribute, with something like: 95 // 96 // (&(objectClass=user)(enabledMoodleUser=1)) 97 // 98 // In this particular case we don't need to do anything, 99 // so leave $this->config->objectclass as is. 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, '(objectClass=*)', 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 $ldap_defaults = ldap_getdefaults(); 842 843 return ldap_find_userdn($this->ldapconnection, $userid, $ldap_contexts, 844 '(objectClass='.$ldap_defaults['objectclass'][$this->get_config('user_type')].')', 845 $this->get_config('idnumber_attribute'), $this->get_config('user_search_sub')); 846 } 847 848 /** 849 * Find the groups a given distinguished name belongs to, both directly 850 * and indirectly via nested groups membership. 851 * 852 * @param string $memberdn distinguished name to search 853 * @return array with member groups' distinguished names (can be emtpy) 854 */ 855 protected function ldap_find_user_groups($memberdn) { 856 $groups = array(); 857 858 $this->ldap_find_user_groups_recursively($memberdn, $groups); 859 return $groups; 860 } 861 862 /** 863 * Recursively process the groups the given member distinguished name 864 * belongs to, adding them to the already processed groups array. 865 * 866 * @param string $memberdn distinguished name to search 867 * @param array reference &$membergroups array with already found 868 * groups, where we'll put the newly found 869 * groups. 870 */ 871 protected function ldap_find_user_groups_recursively($memberdn, &$membergroups) { 872 $result = @ldap_read($this->ldapconnection, $memberdn, '(objectClass=*)', array($this->get_config('group_memberofattribute'))); 873 if (!$result) { 874 return; 875 } 876 877 if ($entry = ldap_first_entry($this->ldapconnection, $result)) { 878 do { 879 $attributes = ldap_get_attributes($this->ldapconnection, $entry); 880 for ($j = 0; $j < $attributes['count']; $j++) { 881 $groups = ldap_get_values_len($this->ldapconnection, $entry, $attributes[$j]); 882 foreach ($groups as $key => $group) { 883 if ($key === 'count') { // Skip the entries count 884 continue; 885 } 886 if(!in_array($group, $membergroups)) { 887 // Only push and recurse if we haven't 'seen' this group before 888 // to prevent loops (MS Active Directory allows them!!). 889 array_push($membergroups, $group); 890 $this->ldap_find_user_groups_recursively($group, $membergroups); 891 } 892 } 893 } 894 } 895 while ($entry = ldap_next_entry($this->ldapconnection, $entry)); 896 } 897 } 898 899 /** 900 * Given a group name (either a RDN or a DN), get the list of users 901 * belonging to that group. If the group has nested groups, expand all 902 * the intermediate groups and return the full list of users that 903 * directly or indirectly belong to the group. 904 * 905 * @param string $group the group name to search 906 * @param string $memberattibute the attribute that holds the members of the group 907 * @return array the list of users belonging to the group. If $group 908 * is not actually a group, returns array($group). 909 */ 910 protected function ldap_explode_group($group, $memberattribute) { 911 switch ($this->get_config('user_type')) { 912 case 'ad': 913 // $group is already the distinguished name to search. 914 $dn = $group; 915 916 $result = ldap_read($this->ldapconnection, $dn, '(objectClass=*)', array('objectClass')); 917 $entry = ldap_first_entry($this->ldapconnection, $result); 918 $objectclass = ldap_get_values($this->ldapconnection, $entry, 'objectClass'); 919 920 if (!in_array('group', $objectclass)) { 921 // Not a group, so return immediately. 922 return array($group); 923 } 924 925 $result = ldap_read($this->ldapconnection, $dn, '(objectClass=*)', array($memberattribute)); 926 $entry = ldap_first_entry($this->ldapconnection, $result); 927 $members = @ldap_get_values($this->ldapconnection, $entry, $memberattribute); // Can be empty and throws a warning 928 if ($members['count'] == 0) { 929 // There are no members in this group, return nothing. 930 return array(); 931 } 932 unset($members['count']); 933 934 $users = array(); 935 foreach ($members as $member) { 936 $group_members = $this->ldap_explode_group($member, $memberattribute); 937 $users = array_merge($users, $group_members); 938 } 939 940 return ($users); 941 break; 942 default: 943 error_log($this->errorlogtag.get_string('explodegroupusertypenotsupported', 'enrol_ldap', 944 $this->get_config('user_type_name'))); 945 946 return array($group); 947 } 948 } 949 950 /** 951 * Will create the moodle course from the template 952 * course_ext is an array as obtained from ldap -- flattened somewhat 953 * 954 * @param array $course_ext 955 * @param progress_trace $trace 956 * @return mixed false on error, id for the newly created course otherwise. 957 */ 958 function create_course($course_ext, progress_trace $trace) { 959 global $CFG, $DB; 960 961 require_once("$CFG->dirroot/course/lib.php"); 962 963 // Override defaults with template course 964 $template = false; 965 if ($this->get_config('template')) { 966 if ($template = $DB->get_record('course', array('shortname'=>$this->get_config('template')))) { 967 $template = fullclone(course_get_format($template)->get_course()); 968 unset($template->id); // So we are clear to reinsert the record 969 unset($template->fullname); 970 unset($template->shortname); 971 unset($template->idnumber); 972 } 973 } 974 if (!$template) { 975 $courseconfig = get_config('moodlecourse'); 976 $template = new stdClass(); 977 $template->summary = ''; 978 $template->summaryformat = FORMAT_HTML; 979 $template->format = $courseconfig->format; 980 $template->newsitems = $courseconfig->newsitems; 981 $template->showgrades = $courseconfig->showgrades; 982 $template->showreports = $courseconfig->showreports; 983 $template->maxbytes = $courseconfig->maxbytes; 984 $template->groupmode = $courseconfig->groupmode; 985 $template->groupmodeforce = $courseconfig->groupmodeforce; 986 $template->visible = $courseconfig->visible; 987 $template->lang = $courseconfig->lang; 988 $template->enablecompletion = $courseconfig->enablecompletion; 989 } 990 $course = $template; 991 992 $course->category = $this->get_config('category'); 993 if (!$DB->record_exists('course_categories', array('id'=>$this->get_config('category')))) { 994 $categories = $DB->get_records('course_categories', array(), 'sortorder', 'id', 0, 1); 995 $first = reset($categories); 996 $course->category = $first->id; 997 } 998 999 // Override with required ext data 1000 $course->idnumber = $course_ext[$this->get_config('course_idnumber')][0]; 1001 $course->fullname = $course_ext[$this->get_config('course_fullname')][0]; 1002 $course->shortname = $course_ext[$this->get_config('course_shortname')][0]; 1003 if (empty($course->idnumber) || empty($course->fullname) || empty($course->shortname)) { 1004 // We are in trouble! 1005 $trace->output(get_string('cannotcreatecourse', 'enrol_ldap').' '.var_export($course, true)); 1006 return false; 1007 } 1008 1009 $summary = $this->get_config('course_summary'); 1010 if (!isset($summary) || empty($course_ext[$summary][0])) { 1011 $course->summary = ''; 1012 } else { 1013 $course->summary = $course_ext[$this->get_config('course_summary')][0]; 1014 } 1015 1016 // Check if the shortname already exists if it does - skip course creation. 1017 if ($DB->record_exists('course', array('shortname' => $course->shortname))) { 1018 $trace->output(get_string('duplicateshortname', 'enrol_ldap', $course)); 1019 return false; 1020 } 1021 1022 $newcourse = create_course($course); 1023 return $newcourse->id; 1024 } 1025 1026 /** 1027 * Will update a moodle course with new values from LDAP 1028 * A field will be updated only if it is marked to be updated 1029 * on sync in plugin settings 1030 * 1031 * @param object $course 1032 * @param array $externalcourse 1033 * @param progress_trace $trace 1034 * @return bool 1035 */ 1036 protected function update_course($course, $externalcourse, progress_trace $trace) { 1037 global $CFG, $DB; 1038 1039 $coursefields = array ('shortname', 'fullname', 'summary'); 1040 static $shouldupdate; 1041 1042 // Initialize $shouldupdate variable. Set to true if one or more fields are marked for update. 1043 if (!isset($shouldupdate)) { 1044 $shouldupdate = false; 1045 foreach ($coursefields as $field) { 1046 $shouldupdate = $shouldupdate || $this->get_config('course_'.$field.'_updateonsync'); 1047 } 1048 } 1049 1050 // If we should not update return immediately. 1051 if (!$shouldupdate) { 1052 return false; 1053 } 1054 1055 require_once("$CFG->dirroot/course/lib.php"); 1056 $courseupdated = false; 1057 $updatedcourse = new stdClass(); 1058 $updatedcourse->id = $course->id; 1059 1060 // Update course fields if necessary. 1061 foreach ($coursefields as $field) { 1062 // If field is marked to be updated on sync && field data was changed update it. 1063 if ($this->get_config('course_'.$field.'_updateonsync') 1064 && isset($externalcourse[$this->get_config('course_'.$field)][0]) 1065 && $course->{$field} != $externalcourse[$this->get_config('course_'.$field)][0]) { 1066 $updatedcourse->{$field} = $externalcourse[$this->get_config('course_'.$field)][0]; 1067 $courseupdated = true; 1068 } 1069 } 1070 1071 if (!$courseupdated) { 1072 $trace->output(get_string('courseupdateskipped', 'enrol_ldap', $course)); 1073 return false; 1074 } 1075 1076 // Do not allow empty fullname or shortname. 1077 if ((isset($updatedcourse->fullname) && empty($updatedcourse->fullname)) 1078 || (isset($updatedcourse->shortname) && empty($updatedcourse->shortname))) { 1079 // We are in trouble! 1080 $trace->output(get_string('cannotupdatecourse', 'enrol_ldap', $course)); 1081 return false; 1082 } 1083 1084 // Check if the shortname already exists if it does - skip course updating. 1085 if (isset($updatedcourse->shortname) 1086 && $DB->record_exists('course', array('shortname' => $updatedcourse->shortname))) { 1087 $trace->output(get_string('cannotupdatecourse_duplicateshortname', 'enrol_ldap', $course)); 1088 return false; 1089 } 1090 1091 // Finally - update course in DB. 1092 update_course($updatedcourse); 1093 $trace->output(get_string('courseupdated', 'enrol_ldap', $course)); 1094 1095 return true; 1096 } 1097 1098 /** 1099 * Automatic enrol sync executed during restore. 1100 * Useful for automatic sync by course->idnumber or course category. 1101 * @param stdClass $course course record 1102 */ 1103 public function restore_sync_course($course) { 1104 // TODO: this can not work because restore always nukes the course->idnumber, do not ask me why (MDL-37312) 1105 // NOTE: for now restore does not do any real logging yet, let's do the same here... 1106 $trace = new error_log_progress_trace(); 1107 $this->sync_enrolments($trace, $course->id); 1108 } 1109 1110 /** 1111 * Restore instance and map settings. 1112 * 1113 * @param restore_enrolments_structure_step $step 1114 * @param stdClass $data 1115 * @param stdClass $course 1116 * @param int $oldid 1117 */ 1118 public function restore_instance(restore_enrolments_structure_step $step, stdClass $data, $course, $oldid) { 1119 global $DB; 1120 // There is only 1 ldap enrol instance per course. 1121 if ($instances = $DB->get_records('enrol', array('courseid'=>$data->courseid, 'enrol'=>'ldap'), 'id')) { 1122 $instance = reset($instances); 1123 $instanceid = $instance->id; 1124 } else { 1125 $instanceid = $this->add_instance($course, (array)$data); 1126 } 1127 $step->set_mapping('enrol', $oldid, $instanceid); 1128 } 1129 1130 /** 1131 * Restore user enrolment. 1132 * 1133 * @param restore_enrolments_structure_step $step 1134 * @param stdClass $data 1135 * @param stdClass $instance 1136 * @param int $oldinstancestatus 1137 * @param int $userid 1138 */ 1139 public function restore_user_enrolment(restore_enrolments_structure_step $step, $data, $instance, $userid, $oldinstancestatus) { 1140 global $DB; 1141 1142 if ($this->get_config('unenrolaction') == ENROL_EXT_REMOVED_UNENROL) { 1143 // Enrolments were already synchronised in restore_instance(), we do not want any suspended leftovers. 1144 1145 } else if ($this->get_config('unenrolaction') == ENROL_EXT_REMOVED_KEEP) { 1146 if (!$DB->record_exists('user_enrolments', array('enrolid'=>$instance->id, 'userid'=>$userid))) { 1147 $this->enrol_user($instance, $userid, null, 0, 0, $data->status); 1148 } 1149 1150 } else { 1151 if (!$DB->record_exists('user_enrolments', array('enrolid'=>$instance->id, 'userid'=>$userid))) { 1152 $this->enrol_user($instance, $userid, null, 0, 0, ENROL_USER_SUSPENDED); 1153 } 1154 } 1155 } 1156 1157 /** 1158 * Restore role assignment. 1159 * 1160 * @param stdClass $instance 1161 * @param int $roleid 1162 * @param int $userid 1163 * @param int $contextid 1164 */ 1165 public function restore_role_assignment($instance, $roleid, $userid, $contextid) { 1166 global $DB; 1167 1168 if ($this->get_config('unenrolaction') == ENROL_EXT_REMOVED_UNENROL or $this->get_config('unenrolaction') == ENROL_EXT_REMOVED_SUSPENDNOROLES) { 1169 // Skip any roles restore, they should be already synced automatically. 1170 return; 1171 } 1172 1173 // Just restore every role. 1174 if ($DB->record_exists('user_enrolments', array('enrolid'=>$instance->id, 'userid'=>$userid))) { 1175 role_assign($roleid, $userid, $contextid, 'enrol_'.$instance->enrol, $instance->id); 1176 } 1177 } 1178 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
Generated: Fri Nov 28 20:29:05 2014 | Cross-referenced by PHPXref 0.7.1 |