|
Moodle
2.2.1
http://www.collinsharper.com
|
00001 <?php 00002 00003 // This file is part of Moodle - http://moodle.org/ 00004 // 00005 // Moodle is free software: you can redistribute it and/or modify 00006 // it under the terms of the GNU General Public License as published by 00007 // the Free Software Foundation, either version 3 of the License, or 00008 // (at your option) any later version. 00009 // 00010 // Moodle is distributed in the hope that it will be useful, 00011 // but WITHOUT ANY WARRANTY; without even the implied warranty of 00012 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 00013 // GNU General Public License for more details. 00014 // 00015 // You should have received a copy of the GNU General Public License 00016 // along with Moodle. If not, see <http://www.gnu.org/licenses/>. 00017 00031 defined('MOODLE_INTERNAL') || die(); 00032 00033 class enrol_ldap_plugin extends enrol_plugin { 00034 protected $enrol_localcoursefield = 'idnumber'; 00035 protected $enroltype = 'enrol_ldap'; 00036 protected $errorlogtag = '[ENROL LDAP] '; 00037 00043 public function __construct() { 00044 global $CFG; 00045 require_once($CFG->libdir.'/ldaplib.php'); 00046 00047 // Do our own stuff to fix the config (it's easier to do it 00048 // here than using the admin settings infrastructure). We 00049 // don't call $this->set_config() for any of the 'fixups' 00050 // (except the objectclass, as it's critical) because the user 00051 // didn't specify any values and relied on the default values 00052 // defined for the user type she chose. 00053 $this->load_config(); 00054 00055 // Make sure we get sane defaults for critical values. 00056 $this->config->ldapencoding = $this->get_config('ldapencoding', 'utf-8'); 00057 $this->config->user_type = $this->get_config('user_type', 'default'); 00058 00059 $ldap_usertypes = ldap_supported_usertypes(); 00060 $this->config->user_type_name = $ldap_usertypes[$this->config->user_type]; 00061 unset($ldap_usertypes); 00062 00063 $default = ldap_getdefaults(); 00064 // Remove the objectclass default, as the values specified there are for 00065 // users, and we are dealing with groups here. 00066 unset($default['objectclass']); 00067 00068 // Use defaults if values not given. Dont use this->get_config() 00069 // here to be able to check for 0 and false values too. 00070 foreach ($default as $key => $value) { 00071 // Watch out - 0, false are correct values too, so we can't use $this->get_config() 00072 if (!isset($this->config->{$key}) or $this->config->{$key} == '') { 00073 $this->config->{$key} = $value[$this->config->user_type]; 00074 } 00075 } 00076 00077 if (empty($this->config->objectclass)) { 00078 // Can't send empty filter. Fix it for now and future occasions 00079 $this->set_config('objectclass', '(objectClass=*)'); 00080 } else if (stripos($this->config->objectclass, 'objectClass=') === 0) { 00081 // Value is 'objectClass=some-string-here', so just add () 00082 // around the value (filter _must_ have them). 00083 // Fix it for now and future occasions 00084 $this->set_config('objectclass', '('.$this->config->objectclass.')'); 00085 } else if (stripos($this->config->objectclass, '(') !== 0) { 00086 // Value is 'some-string-not-starting-with-left-parentheses', 00087 // which is assumed to be the objectClass matching value. 00088 // So build a valid filter with it. 00089 $this->set_config('objectclass', '(objectClass='.$this->config->objectclass.')'); 00090 } else { 00091 // There is an additional possible value 00092 // '(some-string-here)', that can be used to specify any 00093 // valid filter string, to select subsets of users based 00094 // on any criteria. For example, we could select the users 00095 // whose objectClass is 'user' and have the 00096 // 'enabledMoodleUser' attribute, with something like: 00097 // 00098 // (&(objectClass=user)(enabledMoodleUser=1)) 00099 // 00100 // In this particular case we don't need to do anything, 00101 // so leave $this->config->objectclass as is. 00102 } 00103 } 00104 00111 public function instance_deleteable($instance) { 00112 if (!enrol_is_enabled('ldap')) { 00113 return true; 00114 } 00115 00116 if (!$this->get_config('ldap_host') or !$this->get_config('objectclass') or !$this->get_config('course_idnumber')) { 00117 return true; 00118 } 00119 00120 // TODO: connect to external system and make sure no users are to be enrolled in this course 00121 return false; 00122 } 00123 00131 public function sync_user_enrolments($user) { 00132 global $DB; 00133 00134 $ldapconnection = $this->ldap_connect(); 00135 if (!$ldapconnection) { 00136 return; 00137 } 00138 00139 if (!is_object($user) or !property_exists($user, 'id')) { 00140 throw new coding_exception('Invalid $user parameter in sync_user_enrolments()'); 00141 } 00142 00143 if (!property_exists($user, 'idnumber')) { 00144 debugging('Invalid $user parameter in sync_user_enrolments(), missing idnumber'); 00145 $user = $DB->get_record('user', array('id'=>$user->id)); 00146 } 00147 00148 // We may need a lot of memory here 00149 @set_time_limit(0); 00150 raise_memory_limit(MEMORY_HUGE); 00151 00152 // Get enrolments for each type of role. 00153 $roles = get_all_roles(); 00154 $enrolments = array(); 00155 foreach($roles as $role) { 00156 // Get external enrolments according to LDAP server 00157 $enrolments[$role->id]['ext'] = $this->find_ext_enrolments($ldapconnection, $user->idnumber, $role); 00158 00159 // Get the list of current user enrolments that come from LDAP 00160 $sql= "SELECT e.courseid, ue.status, e.id as enrolid, c.shortname 00161 FROM {user} u 00162 JOIN {role_assignments} ra ON (ra.userid = u.id AND ra.component = 'enrol_ldap' AND ra.roleid = :roleid) 00163 JOIN {user_enrolments} ue ON (ue.userid = u.id AND ue.enrolid = ra.itemid) 00164 JOIN {enrol} e ON (e.id = ue.enrolid) 00165 JOIN {course} c ON (c.id = e.courseid) 00166 WHERE u.deleted = 0 AND u.id = :userid"; 00167 $params = array ('roleid'=>$role->id, 'userid'=>$user->id); 00168 $enrolments[$role->id]['current'] = $DB->get_records_sql($sql, $params); 00169 } 00170 00171 $ignorehidden = $this->get_config('ignorehiddencourses'); 00172 $courseidnumber = $this->get_config('course_idnumber'); 00173 foreach($roles as $role) { 00174 foreach ($enrolments[$role->id]['ext'] as $enrol) { 00175 $course_ext_id = $enrol[$courseidnumber][0]; 00176 if (empty($course_ext_id)) { 00177 error_log($this->errorlogtag.get_string('extcourseidinvalid', 'enrol_ldap')); 00178 continue; // Next; skip this one! 00179 } 00180 00181 // Create the course if required 00182 $course = $DB->get_record('course', array($this->enrol_localcoursefield=>$course_ext_id)); 00183 if (empty($course)) { // Course doesn't exist 00184 if ($this->get_config('autocreate')) { // Autocreate 00185 error_log($this->errorlogtag.get_string('createcourseextid', 'enrol_ldap', 00186 array('courseextid'=>$course_ext_id))); 00187 if ($newcourseid = $this->create_course($enrol)) { 00188 $course = $DB->get_record('course', array('id'=>$newcourseid)); 00189 } 00190 } else { 00191 error_log($this->errorlogtag.get_string('createnotcourseextid', 'enrol_ldap', 00192 array('courseextid'=>$course_ext_id))); 00193 continue; // Next; skip this one! 00194 } 00195 } 00196 00197 // Deal with enrolment in the moodle db 00198 // Add necessary enrol instance if not present yet; 00199 $sql = "SELECT c.id, c.visible, e.id as enrolid 00200 FROM {course} c 00201 JOIN {enrol} e ON (e.courseid = c.id AND e.enrol = 'ldap') 00202 WHERE c.id = :courseid"; 00203 $params = array('courseid'=>$course->id); 00204 if (!($course_instance = $DB->get_record_sql($sql, $params, IGNORE_MULTIPLE))) { 00205 $course_instance = new stdClass(); 00206 $course_instance->id = $course->id; 00207 $course_instance->visible = $course->visible; 00208 $course_instance->enrolid = $this->add_instance($course_instance); 00209 } 00210 00211 if (!$instance = $DB->get_record('enrol', array('id'=>$course_instance->enrolid))) { 00212 continue; // Weird; skip this one. 00213 } 00214 00215 if ($ignorehidden && !$course_instance->visible) { 00216 continue; 00217 } 00218 00219 if (empty($enrolments[$role->id]['current'][$course->id])) { 00220 // Enrol the user in the given course, with that role. 00221 $this->enrol_user($instance, $user->id, $role->id); 00222 // Make sure we set the enrolment status to active. If the user wasn't 00223 // previously enrolled to the course, enrol_user() sets it. But if we 00224 // configured the plugin to suspend the user enrolments _AND_ remove 00225 // the role assignments on external unenrol, then enrol_user() doesn't 00226 // set it back to active on external re-enrolment. So set it 00227 // unconditionnally to cover both cases. 00228 $DB->set_field('user_enrolments', 'status', ENROL_USER_ACTIVE, array('enrolid'=>$instance->id, 'userid'=>$user->id)); 00229 error_log($this->errorlogtag.get_string('enroluser', 'enrol_ldap', 00230 array('user_username'=> $user->username, 00231 'course_shortname'=>$course->shortname, 00232 'course_id'=>$course->id))); 00233 } else { 00234 if ($enrolments[$role->id]['current'][$course->id]->status == ENROL_USER_SUSPENDED) { 00235 // Reenable enrolment that was previously disabled. Enrolment refreshed 00236 $DB->set_field('user_enrolments', 'status', ENROL_USER_ACTIVE, array('enrolid'=>$instance->id, 'userid'=>$user->id)); 00237 error_log($this->errorlogtag.get_string('enroluserenable', 'enrol_ldap', 00238 array('user_username'=> $user->username, 00239 'course_shortname'=>$course->shortname, 00240 'course_id'=>$course->id))); 00241 } 00242 } 00243 00244 // Remove this course from the current courses, to be able to detect 00245 // which current courses should be unenroled from when we finish processing 00246 // external enrolments. 00247 unset($enrolments[$role->id]['current'][$course->id]); 00248 } 00249 00250 // Deal with unenrolments. 00251 $transaction = $DB->start_delegated_transaction(); 00252 foreach ($enrolments[$role->id]['current'] as $course) { 00253 $context = get_context_instance(CONTEXT_COURSE, $course->courseid); 00254 $instance = $DB->get_record('enrol', array('id'=>$course->enrolid)); 00255 switch ($this->get_config('unenrolaction')) { 00256 case ENROL_EXT_REMOVED_UNENROL: 00257 $this->unenrol_user($instance, $user->id); 00258 error_log($this->errorlogtag.get_string('extremovedunenrol', 'enrol_ldap', 00259 array('user_username'=> $user->username, 00260 'course_shortname'=>$course->shortname, 00261 'course_id'=>$course->courseid))); 00262 break; 00263 case ENROL_EXT_REMOVED_KEEP: 00264 // Keep - only adding enrolments 00265 break; 00266 case ENROL_EXT_REMOVED_SUSPEND: 00267 if ($course->status != ENROL_USER_SUSPENDED) { 00268 $DB->set_field('user_enrolments', 'status', ENROL_USER_SUSPENDED, array('enrolid'=>$instance->id, 'userid'=>$user->id)); 00269 error_log($this->errorlogtag.get_string('extremovedsuspend', 'enrol_ldap', 00270 array('user_username'=> $user->username, 00271 'course_shortname'=>$course->shortname, 00272 'course_id'=>$course->courseid))); 00273 } 00274 break; 00275 case ENROL_EXT_REMOVED_SUSPENDNOROLES: 00276 if ($course->status != ENROL_USER_SUSPENDED) { 00277 $DB->set_field('user_enrolments', 'status', ENROL_USER_SUSPENDED, array('enrolid'=>$instance->id, 'userid'=>$user->id)); 00278 } 00279 role_unassign_all(array('contextid'=>$context->id, 'userid'=>$user->id, 'component'=>'enrol_ldap', 'itemid'=>$instance->id)); 00280 error_log($this->errorlogtag.get_string('extremovedsuspendnoroles', 'enrol_ldap', 00281 array('user_username'=> $user->username, 00282 'course_shortname'=>$course->shortname, 00283 'course_id'=>$course->courseid))); 00284 break; 00285 } 00286 } 00287 $transaction->allow_commit(); 00288 } 00289 00290 $this->ldap_close($ldapconnection); 00291 } 00292 00299 public function sync_enrolments() { 00300 global $CFG, $DB; 00301 00302 $ldapconnection = $this->ldap_connect(); 00303 if (!$ldapconnection) { 00304 return; 00305 } 00306 00307 // we may need a lot of memory here 00308 @set_time_limit(0); 00309 raise_memory_limit(MEMORY_HUGE); 00310 00311 // Get enrolments for each type of role. 00312 $roles = get_all_roles(); 00313 $enrolments = array(); 00314 foreach($roles as $role) { 00315 // Get all contexts 00316 $ldap_contexts = explode(';', $this->config->{'contexts_role'.$role->id}); 00317 00318 // Get all the fields we will want for the potential course creation 00319 // as they are light. Don't get membership -- potentially a lot of data. 00320 $ldap_fields_wanted = array('dn', $this->config->course_idnumber); 00321 if (!empty($this->config->course_fullname)) { 00322 array_push($ldap_fields_wanted, $this->config->course_fullname); 00323 } 00324 if (!empty($this->config->course_shortname)) { 00325 array_push($ldap_fields_wanted, $this->config->course_shortname); 00326 } 00327 if (!empty($this->config->course_summary)) { 00328 array_push($ldap_fields_wanted, $this->config->course_summary); 00329 } 00330 array_push($ldap_fields_wanted, $this->config->{'memberattribute_role'.$role->id}); 00331 00332 // Define the search pattern 00333 $ldap_search_pattern = $this->config->objectclass; 00334 00335 foreach ($ldap_contexts as $ldap_context) { 00336 $ldap_context = trim($ldap_context); 00337 if (empty($ldap_context)) { 00338 continue; // Next; 00339 } 00340 00341 if ($this->config->course_search_sub) { 00342 // Use ldap_search to find first user from subtree 00343 $ldap_result = @ldap_search($ldapconnection, 00344 $ldap_context, 00345 $ldap_search_pattern, 00346 $ldap_fields_wanted); 00347 } else { 00348 // Search only in this context 00349 $ldap_result = @ldap_list($ldapconnection, 00350 $ldap_context, 00351 $ldap_search_pattern, 00352 $ldap_fields_wanted); 00353 } 00354 if (!$ldap_result) { 00355 continue; // Next 00356 } 00357 00358 // Check and push results 00359 $records = ldap_get_entries($ldapconnection, $ldap_result); 00360 00361 // LDAP libraries return an odd array, really. fix it: 00362 $flat_records = array(); 00363 for ($c = 0; $c < $records['count']; $c++) { 00364 array_push($flat_records, $records[$c]); 00365 } 00366 // Free some mem 00367 unset($records); 00368 00369 if (count($flat_records)) { 00370 $ignorehidden = $this->get_config('ignorehiddencourses'); 00371 foreach($flat_records as $course) { 00372 $course = array_change_key_case($course, CASE_LOWER); 00373 $idnumber = $course{$this->config->course_idnumber}[0]; 00374 print_string('synccourserole', 'enrol_ldap', 00375 array('idnumber'=>$idnumber, 'role_shortname'=>$role->shortname)); 00376 00377 // Does the course exist in moodle already? 00378 $course_obj = $DB->get_record('course', array($this->enrol_localcoursefield=>$idnumber)); 00379 if (empty($course_obj)) { // Course doesn't exist 00380 if ($this->get_config('autocreate')) { // Autocreate 00381 error_log($this->errorlogtag.get_string('createcourseextid', 'enrol_ldap', 00382 array('courseextid'=>$idnumber))); 00383 if ($newcourseid = $this->create_course($course)) { 00384 $course_obj = $DB->get_record('course', array('id'=>$newcourseid)); 00385 } 00386 } else { 00387 error_log($this->errorlogtag.get_string('createnotcourseextid', 'enrol_ldap', 00388 array('courseextid'=>$idnumber))); 00389 continue; // Next; skip this one! 00390 } 00391 } 00392 00393 // Enrol & unenrol 00394 00395 // Pull the ldap membership into a nice array 00396 // this is an odd array -- mix of hash and array -- 00397 $ldapmembers = array(); 00398 00399 if (array_key_exists('memberattribute_role'.$role->id, $this->config) 00400 && !empty($this->config->{'memberattribute_role'.$role->id}) 00401 && !empty($course[$this->config->{'memberattribute_role'.$role->id}])) { // May have no membership! 00402 00403 $ldapmembers = $course[$this->config->{'memberattribute_role'.$role->id}]; 00404 unset($ldapmembers['count']); // Remove oddity ;) 00405 00406 // If we have enabled nested groups, we need to expand 00407 // the groups to get the real user list. We need to do 00408 // this before dealing with 'memberattribute_isdn'. 00409 if ($this->config->nested_groups) { 00410 $users = array(); 00411 foreach ($ldapmembers as $ldapmember) { 00412 $grpusers = $this->ldap_explode_group($ldapconnection, 00413 $ldapmember, 00414 $this->config->{'memberattribute_role'.$role->id}); 00415 00416 $users = array_merge($users, $grpusers); 00417 } 00418 $ldapmembers = array_unique($users); // There might be duplicates. 00419 } 00420 00421 // Deal with the case where the member attribute holds distinguished names, 00422 // but only if the user attribute is not a distinguished name itself. 00423 if ($this->config->memberattribute_isdn 00424 && ($this->config->idnumber_attribute !== 'dn') 00425 && ($this->config->idnumber_attribute !== 'distinguishedname')) { 00426 // We need to retrieve the idnumber for all the users in $ldapmembers, 00427 // as the idnumber does not match their dn and we get dn's from membership. 00428 $memberidnumbers = array(); 00429 foreach ($ldapmembers as $ldapmember) { 00430 $result = ldap_read($ldapconnection, $ldapmember, '(objectClass=*)', 00431 array($this->config->idnumber_attribute)); 00432 $entry = ldap_first_entry($ldapconnection, $result); 00433 $values = ldap_get_values($ldapconnection, $entry, $this->config->idnumber_attribute); 00434 array_push($memberidnumbers, $values[0]); 00435 } 00436 00437 $ldapmembers = $memberidnumbers; 00438 } 00439 } 00440 00441 // Prune old ldap enrolments 00442 // hopefully they'll fit in the max buffer size for the RDBMS 00443 $sql= "SELECT u.id as userid, u.username, ue.status, 00444 ra.contextid, ra.itemid as instanceid 00445 FROM {user} u 00446 JOIN {role_assignments} ra ON (ra.userid = u.id AND ra.component = 'enrol_ldap' AND ra.roleid = :roleid) 00447 JOIN {user_enrolments} ue ON (ue.userid = u.id AND ue.enrolid = ra.itemid) 00448 JOIN {enrol} e ON (e.id = ue.enrolid) 00449 WHERE u.deleted = 0 AND e.courseid = :courseid "; 00450 $params = array('roleid'=>$role->id, 'courseid'=>$course_obj->id); 00451 $context = get_context_instance(CONTEXT_COURSE, $course_obj->id); 00452 if (!empty($ldapmembers)) { 00453 list($ldapml, $params2) = $DB->get_in_or_equal($ldapmembers, SQL_PARAMS_NAMED, 'm', false); 00454 $sql .= "AND u.idnumber $ldapml"; 00455 $params = array_merge($params, $params2); 00456 unset($params2); 00457 } else { 00458 $shortname = format_string($course_obj->shortname, true, array('context' => $context)); 00459 print_string('emptyenrolment', 'enrol_ldap', 00460 array('role_shortname'=> $role->shortname, 00461 'course_shortname' => $shortname)); 00462 } 00463 $todelete = $DB->get_records_sql($sql, $params); 00464 00465 if (!empty($todelete)) { 00466 $transaction = $DB->start_delegated_transaction(); 00467 foreach ($todelete as $row) { 00468 $instance = $DB->get_record('enrol', array('id'=>$row->instanceid)); 00469 switch ($this->get_config('unenrolaction')) { 00470 case ENROL_EXT_REMOVED_UNENROL: 00471 $this->unenrol_user($instance, $row->userid); 00472 error_log($this->errorlogtag.get_string('extremovedunenrol', 'enrol_ldap', 00473 array('user_username'=> $row->username, 00474 'course_shortname'=>$course_obj->shortname, 00475 'course_id'=>$course_obj->id))); 00476 break; 00477 case ENROL_EXT_REMOVED_KEEP: 00478 // Keep - only adding enrolments 00479 break; 00480 case ENROL_EXT_REMOVED_SUSPEND: 00481 if ($row->status != ENROL_USER_SUSPENDED) { 00482 $DB->set_field('user_enrolments', 'status', ENROL_USER_SUSPENDED, array('enrolid'=>$instance->id, 'userid'=>$row->userid)); 00483 error_log($this->errorlogtag.get_string('extremovedsuspend', 'enrol_ldap', 00484 array('user_username'=> $row->username, 00485 'course_shortname'=>$course_obj->shortname, 00486 'course_id'=>$course_obj->id))); 00487 } 00488 break; 00489 case ENROL_EXT_REMOVED_SUSPENDNOROLES: 00490 if ($row->status != ENROL_USER_SUSPENDED) { 00491 $DB->set_field('user_enrolments', 'status', ENROL_USER_SUSPENDED, array('enrolid'=>$instance->id, 'userid'=>$row->userid)); 00492 } 00493 role_unassign_all(array('contextid'=>$row->contextid, 'userid'=>$row->userid, 'component'=>'enrol_ldap', 'itemid'=>$instance->id)); 00494 error_log($this->errorlogtag.get_string('extremovedsuspendnoroles', 'enrol_ldap', 00495 array('user_username'=> $row->username, 00496 'course_shortname'=>$course_obj->shortname, 00497 'course_id'=>$course_obj->id))); 00498 break; 00499 } 00500 } 00501 $transaction->allow_commit(); 00502 } 00503 00504 // Insert current enrolments 00505 // bad we can't do INSERT IGNORE with postgres... 00506 00507 // Add necessary enrol instance if not present yet; 00508 $sql = "SELECT c.id, c.visible, e.id as enrolid 00509 FROM {course} c 00510 JOIN {enrol} e ON (e.courseid = c.id AND e.enrol = 'ldap') 00511 WHERE c.id = :courseid"; 00512 $params = array('courseid'=>$course_obj->id); 00513 if (!($course_instance = $DB->get_record_sql($sql, $params, IGNORE_MULTIPLE))) { 00514 $course_instance = new stdClass(); 00515 $course_instance->id = $course_obj->id; 00516 $course_instance->visible = $course_obj->visible; 00517 $course_instance->enrolid = $this->add_instance($course_instance); 00518 } 00519 00520 if (!$instance = $DB->get_record('enrol', array('id'=>$course_instance->enrolid))) { 00521 continue; // Weird; skip this one. 00522 } 00523 00524 if ($ignorehidden && !$course_instance->visible) { 00525 continue; 00526 } 00527 00528 $transaction = $DB->start_delegated_transaction(); 00529 foreach ($ldapmembers as $ldapmember) { 00530 $sql = 'SELECT id,username,1 FROM {user} WHERE idnumber = ? AND deleted = 0'; 00531 $member = $DB->get_record_sql($sql, array($ldapmember)); 00532 if(empty($member) || empty($member->id)){ 00533 print_string ('couldnotfinduser', 'enrol_ldap', $ldapmember); 00534 continue; 00535 } 00536 00537 $sql= "SELECT ue.status 00538 FROM {user_enrolments} ue 00539 JOIN {enrol} e ON (e.id = ue.enrolid) 00540 JOIN {role_assignments} ra ON (ra.itemid = e.id AND ra.component = 'enrol_ldap') 00541 WHERE e.courseid = :courseid AND ue.userid = :userid"; 00542 $params = array('courseid'=>$course_obj->id, 'userid'=>$member->id); 00543 $userenrolment = $DB->get_record_sql($sql, $params); 00544 00545 if(empty($userenrolment)) { 00546 $this->enrol_user($instance, $member->id, $role->id); 00547 // Make sure we set the enrolment status to active. If the user wasn't 00548 // previously enrolled to the course, enrol_user() sets it. But if we 00549 // configured the plugin to suspend the user enrolments _AND_ remove 00550 // the role assignments on external unenrol, then enrol_user() doesn't 00551 // set it back to active on external re-enrolment. So set it 00552 // unconditionnally to cover both cases. 00553 $DB->set_field('user_enrolments', 'status', ENROL_USER_ACTIVE, array('enrolid'=>$instance->id, 'userid'=>$member->id)); 00554 error_log($this->errorlogtag.get_string('enroluser', 'enrol_ldap', 00555 array('user_username'=> $member->username, 00556 'course_shortname'=>$course_obj->shortname, 00557 'course_id'=>$course_obj->id))); 00558 00559 } else { 00560 if ($userenrolment->status == ENROL_USER_SUSPENDED) { 00561 // Reenable enrolment that was previously disabled. Enrolment refreshed 00562 $DB->set_field('user_enrolments', 'status', ENROL_USER_ACTIVE, array('enrolid'=>$instance->id, 'userid'=>$member->id)); 00563 error_log($this->errorlogtag.get_string('enroluserenable', 'enrol_ldap', 00564 array('user_username'=> $member->username, 00565 'course_shortname'=>$course_obj->shortname, 00566 'course_id'=>$course_obj->id))); 00567 } 00568 } 00569 } 00570 $transaction->allow_commit(); 00571 } 00572 } 00573 } 00574 } 00575 @$this->ldap_close(); 00576 } 00577 00584 protected function ldap_connect() { 00585 global $CFG; 00586 require_once($CFG->libdir.'/ldaplib.php'); 00587 00588 // Cache ldap connections. They are expensive to set up 00589 // and can drain the TCP/IP ressources on the server if we 00590 // are syncing a lot of users (as we try to open a new connection 00591 // to get the user details). This is the least invasive way 00592 // to reuse existing connections without greater code surgery. 00593 if(!empty($this->ldapconnection)) { 00594 $this->ldapconns++; 00595 return $this->ldapconnection; 00596 } 00597 00598 if ($ldapconnection = ldap_connect_moodle($this->get_config('host_url'), $this->get_config('ldap_version'), 00599 $this->get_config('user_type'), $this->get_config('bind_dn'), 00600 $this->get_config('bind_pw'), $this->get_config('opt_deref'), 00601 $debuginfo)) { 00602 $this->ldapconns = 1; 00603 $this->ldapconnection = $ldapconnection; 00604 return $ldapconnection; 00605 } 00606 00607 // Log the problem, but don't show it to the user. She doesn't 00608 // even have a chance to see it, as we redirect instantly to 00609 // the user/front page. 00610 error_log($this->errorlogtag.$debuginfo); 00611 00612 return false; 00613 } 00614 00619 protected function ldap_close() { 00620 $this->ldapconns--; 00621 if($this->ldapconns == 0) { 00622 @ldap_close($this->ldapconnection); 00623 unset($this->ldapconnection); 00624 } 00625 } 00626 00636 protected function find_ext_enrolments ($ldapconnection, $memberuid, $role) { 00637 global $CFG; 00638 require_once($CFG->libdir.'/ldaplib.php'); 00639 00640 if (empty($memberuid)) { 00641 // No "idnumber" stored for this user, so no LDAP enrolments 00642 return array(); 00643 } 00644 00645 $ldap_contexts = trim($this->get_config('contexts_role'.$role->id)); 00646 if (empty($ldap_contexts)) { 00647 // No role contexts, so no LDAP enrolments 00648 return array(); 00649 } 00650 00651 $textlib = textlib_get_instance(); 00652 $extmemberuid = $textlib->convert($memberuid, 'utf-8', $this->get_config('ldapencoding')); 00653 00654 if($this->get_config('memberattribute_isdn')) { 00655 if (!($extmemberuid = $this->ldap_find_userdn ($ldapconnection, $extmemberuid))) { 00656 return array(); 00657 } 00658 } 00659 00660 $ldap_search_pattern = ''; 00661 if($this->get_config('nested_groups')) { 00662 $usergroups = $this->ldap_find_user_groups($ldapconnection, $extmemberuid); 00663 if(count($usergroups) > 0) { 00664 foreach ($usergroups as $group) { 00665 $ldap_search_pattern .= '('.$this->get_config('memberattribute_role'.$role->id).'='.$group.')'; 00666 } 00667 } 00668 } 00669 00670 // Default return value 00671 $courses = array(); 00672 00673 // Get all the fields we will want for the potential course creation 00674 // as they are light. don't get membership -- potentially a lot of data. 00675 $ldap_fields_wanted = array('dn', $this->get_config('course_idnumber')); 00676 $fullname = $this->get_config('course_fullname'); 00677 $shortname = $this->get_config('course_shortname'); 00678 $summary = $this->get_config('course_summary'); 00679 if (isset($fullname)) { 00680 array_push($ldap_fields_wanted, $fullname); 00681 } 00682 if (isset($shortname)) { 00683 array_push($ldap_fields_wanted, $shortname); 00684 } 00685 if (isset($summary)) { 00686 array_push($ldap_fields_wanted, $summary); 00687 } 00688 00689 // Define the search pattern 00690 if (empty($ldap_search_pattern)) { 00691 $ldap_search_pattern = '('.$this->get_config('memberattribute_role'.$role->id).'='.ldap_filter_addslashes($extmemberuid).')'; 00692 } else { 00693 $ldap_search_pattern = '(|' . $ldap_search_pattern . 00694 '('.$this->get_config('memberattribute_role'.$role->id).'='.ldap_filter_addslashes($extmemberuid).')' . 00695 ')'; 00696 } 00697 $ldap_search_pattern='(&'.$this->get_config('objectclass').$ldap_search_pattern.')'; 00698 00699 // Get all contexts and look for first matching user 00700 $ldap_contexts = explode(';', $ldap_contexts); 00701 foreach ($ldap_contexts as $context) { 00702 $context = trim($context); 00703 if (empty($context)) { 00704 continue; 00705 } 00706 00707 if ($this->get_config('course_search_sub')) { 00708 // Use ldap_search to find first user from subtree 00709 $ldap_result = @ldap_search($ldapconnection, 00710 $context, 00711 $ldap_search_pattern, 00712 $ldap_fields_wanted); 00713 } else { 00714 // Search only in this context 00715 $ldap_result = @ldap_list($ldapconnection, 00716 $context, 00717 $ldap_search_pattern, 00718 $ldap_fields_wanted); 00719 } 00720 00721 if (!$ldap_result) { 00722 continue; 00723 } 00724 00725 // Check and push results. ldap_get_entries() already 00726 // lowercases the attribute index, so there's no need to 00727 // use array_change_key_case() later. 00728 $records = ldap_get_entries($ldapconnection, $ldap_result); 00729 00730 // LDAP libraries return an odd array, really. Fix it. 00731 $flat_records = array(); 00732 for ($c = 0; $c < $records['count']; $c++) { 00733 array_push($flat_records, $records[$c]); 00734 } 00735 unset($records); 00736 00737 if (count($flat_records)) { 00738 $courses = array_merge($courses, $flat_records); 00739 } 00740 } 00741 00742 return $courses; 00743 } 00744 00754 protected function ldap_find_userdn($ldapconnection, $userid) { 00755 global $CFG; 00756 require_once($CFG->libdir.'/ldaplib.php'); 00757 00758 $ldap_contexts = explode(';', $this->get_config('user_contexts')); 00759 $ldap_defaults = ldap_getdefaults(); 00760 00761 return ldap_find_userdn($ldapconnection, $userid, $ldap_contexts, 00762 '(objectClass='.$ldap_defaults['objectclass'][$this->get_config('user_type')].')', 00763 $this->get_config('idnumber_attribute'), $this->get_config('user_search_sub')); 00764 } 00765 00774 protected function ldap_find_user_groups($ldapconnection, $memberdn) { 00775 $groups = array(); 00776 00777 $this->ldap_find_user_groups_recursively($ldapconnection, $memberdn, $groups); 00778 return $groups; 00779 } 00780 00791 protected function ldap_find_user_groups_recursively($ldapconnection, $memberdn, &$membergroups) { 00792 $result = @ldap_read ($ldapconnection, $memberdn, '(objectClass=*)', array($this->get_config('group_memberofattribute'))); 00793 if (!$result) { 00794 return; 00795 } 00796 00797 if ($entry = ldap_first_entry($ldapconnection, $result)) { 00798 do { 00799 $attributes = ldap_get_attributes($ldapconnection, $entry); 00800 for ($j = 0; $j < $attributes['count']; $j++) { 00801 $groups = ldap_get_values_len($ldapconnection, $entry, $attributes[$j]); 00802 foreach ($groups as $key => $group) { 00803 if ($key === 'count') { // Skip the entries count 00804 continue; 00805 } 00806 if(!in_array($group, $membergroups)) { 00807 // Only push and recurse if we haven't 'seen' this group before 00808 // to prevent loops (MS Active Directory allows them!!). 00809 array_push($membergroups, $group); 00810 $this->ldap_find_user_groups_recursively($ldapconnection, $group, $membergroups); 00811 } 00812 } 00813 } 00814 } 00815 while ($entry = ldap_next_entry($ldapconnection, $entry)); 00816 } 00817 } 00818 00831 protected function ldap_explode_group($ldapconnection, $group, $memberattribute) { 00832 switch ($this->get_config('user_type')) { 00833 case 'ad': 00834 // $group is already the distinguished name to search. 00835 $dn = $group; 00836 00837 $result = ldap_read($ldapconnection, $dn, '(objectClass=*)', array('objectClass')); 00838 $entry = ldap_first_entry($ldapconnection, $result); 00839 $objectclass = ldap_get_values($ldapconnection, $entry, 'objectClass'); 00840 00841 if (!in_array('group', $objectclass)) { 00842 // Not a group, so return immediately. 00843 return array($group); 00844 } 00845 00846 $result = ldap_read($ldapconnection, $dn, '(objectClass=*)', array($memberattribute)); 00847 $entry = ldap_first_entry($ldapconnection, $result); 00848 $members = @ldap_get_values($ldapconnection, $entry, $memberattribute); // Can be empty and throws a warning 00849 if ($members['count'] == 0) { 00850 // There are no members in this group, return nothing. 00851 return array(); 00852 } 00853 unset($members['count']); 00854 00855 $users = array(); 00856 foreach ($members as $member) { 00857 $group_members = $this->ldap_explode_group($ldapconnection, $member, $memberattribute); 00858 $users = array_merge($users, $group_members); 00859 } 00860 00861 return ($users); 00862 break; 00863 default: 00864 error_log($this->errorlogtag.get_string('explodegroupusertypenotsupported', 'enrol_ldap', 00865 $this->get_config('user_type_name'))); 00866 00867 return array($group); 00868 } 00869 } 00870 00882 function create_course($course_ext, $skip_fix_course_sortorder=false) { 00883 global $CFG, $DB; 00884 00885 require_once("$CFG->dirroot/course/lib.php"); 00886 00887 // Override defaults with template course 00888 $template = false; 00889 if ($this->get_config('template')) { 00890 if ($template = $DB->get_record('course', array('shortname'=>$this->get_config('template')))) { 00891 unset($template->id); // So we are clear to reinsert the record 00892 unset($template->fullname); 00893 unset($template->shortname); 00894 unset($template->idnumber); 00895 } 00896 } 00897 if (!$template) { 00898 $courseconfig = get_config('moodlecourse'); 00899 $template = new stdClass(); 00900 $template->summary = ''; 00901 $template->summaryformat = FORMAT_HTML; 00902 $template->format = $courseconfig->format; 00903 $template->numsections = $courseconfig->numsections; 00904 $template->hiddensections = $courseconfig->hiddensections; 00905 $template->newsitems = $courseconfig->newsitems; 00906 $template->showgrades = $courseconfig->showgrades; 00907 $template->showreports = $courseconfig->showreports; 00908 $template->maxbytes = $courseconfig->maxbytes; 00909 $template->groupmode = $courseconfig->groupmode; 00910 $template->groupmodeforce = $courseconfig->groupmodeforce; 00911 $template->visible = $courseconfig->visible; 00912 $template->lang = $courseconfig->lang; 00913 $template->groupmodeforce = $courseconfig->groupmodeforce; 00914 } 00915 $course = $template; 00916 00917 $course->category = $this->get_config('category'); 00918 if (!$DB->record_exists('course_categories', array('id'=>$this->get_config('category')))) { 00919 $categories = $DB->get_records('course_categories', array(), 'sortorder', 'id', 0, 1); 00920 $first = reset($categories); 00921 $course->category = $first->id; 00922 } 00923 00924 // Override with required ext data 00925 $course->idnumber = $course_ext[$this->get_config('course_idnumber')][0]; 00926 $course->fullname = $course_ext[$this->get_config('course_fullname')][0]; 00927 $course->shortname = $course_ext[$this->get_config('course_shortname')][0]; 00928 if (empty($course->idnumber) || empty($course->fullname) || empty($course->shortname)) { 00929 // We are in trouble! 00930 error_log($this->errorlogtag.get_string('cannotcreatecourse', 'enrol_ldap')); 00931 error_log($this->errorlogtag.var_export($course, true)); 00932 return false; 00933 } 00934 00935 $summary = $this->get_config('course_summary'); 00936 if (!isset($summary) || empty($course_ext[$summary][0])) { 00937 $course->summary = ''; 00938 } else { 00939 $course->summary = $course_ext[$this->get_config('course_summary')][0]; 00940 } 00941 00942 $newcourse = create_course($course); 00943 return $newcourse->id; 00944 } 00945 } 00946