PageRenderTime 53ms CodeModel.GetById 17ms RepoModel.GetById 0ms app.codeStats 0ms

/enrol/ldap/lib.php

https://bitbucket.org/kudutest1/moodlegit
PHP | 992 lines | 649 code | 114 blank | 229 comment | 128 complexity | c8f44b41579f1ce341363caefefd5a88 MD5 | raw file
  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. * LDAP enrolment plugin implementation.
  18. *
  19. * This plugin synchronises enrolment and roles with a LDAP server.
  20. *
  21. * @package enrol
  22. * @subpackage 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. defined('MOODLE_INTERNAL') || die();
  29. class enrol_ldap_plugin extends enrol_plugin {
  30. protected $enrol_localcoursefield = 'idnumber';
  31. protected $enroltype = 'enrol_ldap';
  32. protected $errorlogtag = '[ENROL LDAP] ';
  33. /**
  34. * Constructor for the plugin. In addition to calling the parent
  35. * constructor, we define and 'fix' some settings depending on the
  36. * real settings the admin defined.
  37. */
  38. public function __construct() {
  39. global $CFG;
  40. require_once($CFG->libdir.'/ldaplib.php');
  41. // Do our own stuff to fix the config (it's easier to do it
  42. // here than using the admin settings infrastructure). We
  43. // don't call $this->set_config() for any of the 'fixups'
  44. // (except the objectclass, as it's critical) because the user
  45. // didn't specify any values and relied on the default values
  46. // defined for the user type she chose.
  47. $this->load_config();
  48. // Make sure we get sane defaults for critical values.
  49. $this->config->ldapencoding = $this->get_config('ldapencoding', 'utf-8');
  50. $this->config->user_type = $this->get_config('user_type', 'default');
  51. $ldap_usertypes = ldap_supported_usertypes();
  52. $this->config->user_type_name = $ldap_usertypes[$this->config->user_type];
  53. unset($ldap_usertypes);
  54. $default = ldap_getdefaults();
  55. // Remove the objectclass default, as the values specified there are for
  56. // users, and we are dealing with groups here.
  57. unset($default['objectclass']);
  58. // Use defaults if values not given. Dont use this->get_config()
  59. // here to be able to check for 0 and false values too.
  60. foreach ($default as $key => $value) {
  61. // Watch out - 0, false are correct values too, so we can't use $this->get_config()
  62. if (!isset($this->config->{$key}) or $this->config->{$key} == '') {
  63. $this->config->{$key} = $value[$this->config->user_type];
  64. }
  65. }
  66. if (empty($this->config->objectclass)) {
  67. // Can't send empty filter. Fix it for now and future occasions
  68. $this->set_config('objectclass', '(objectClass=*)');
  69. } else if (stripos($this->config->objectclass, 'objectClass=') === 0) {
  70. // Value is 'objectClass=some-string-here', so just add ()
  71. // around the value (filter _must_ have them).
  72. // Fix it for now and future occasions
  73. $this->set_config('objectclass', '('.$this->config->objectclass.')');
  74. } else if (stripos($this->config->objectclass, '(') !== 0) {
  75. // Value is 'some-string-not-starting-with-left-parentheses',
  76. // which is assumed to be the objectClass matching value.
  77. // So build a valid filter with it.
  78. $this->set_config('objectclass', '(objectClass='.$this->config->objectclass.')');
  79. } else {
  80. // There is an additional possible value
  81. // '(some-string-here)', that can be used to specify any
  82. // valid filter string, to select subsets of users based
  83. // on any criteria. For example, we could select the users
  84. // whose objectClass is 'user' and have the
  85. // 'enabledMoodleUser' attribute, with something like:
  86. //
  87. // (&(objectClass=user)(enabledMoodleUser=1))
  88. //
  89. // In this particular case we don't need to do anything,
  90. // so leave $this->config->objectclass as is.
  91. }
  92. }
  93. /**
  94. * Is it possible to delete enrol instance via standard UI?
  95. *
  96. * @param object $instance
  97. * @return bool
  98. */
  99. public function instance_deleteable($instance) {
  100. if (!enrol_is_enabled('ldap')) {
  101. return true;
  102. }
  103. if (!$this->get_config('ldap_host') or !$this->get_config('objectclass') or !$this->get_config('course_idnumber')) {
  104. return true;
  105. }
  106. // TODO: connect to external system and make sure no users are to be enrolled in this course
  107. return false;
  108. }
  109. /**
  110. * Forces synchronisation of user enrolments with LDAP server.
  111. * It creates courses if the plugin is configured to do so.
  112. *
  113. * @param object $user user record
  114. * @return void
  115. */
  116. public function sync_user_enrolments($user) {
  117. global $DB;
  118. // Do not try to print anything to the output because this method is called during interactive login.
  119. $trace = new error_log_progress_trace($this->errorlogtag);
  120. if (!$this->ldap_connect($trace)) {
  121. $trace->finished();
  122. return;
  123. }
  124. if (!is_object($user) or !property_exists($user, 'id')) {
  125. throw new coding_exception('Invalid $user parameter in sync_user_enrolments()');
  126. }
  127. if (!property_exists($user, 'idnumber')) {
  128. debugging('Invalid $user parameter in sync_user_enrolments(), missing idnumber');
  129. $user = $DB->get_record('user', array('id'=>$user->id));
  130. }
  131. // We may need a lot of memory here
  132. @set_time_limit(0);
  133. raise_memory_limit(MEMORY_HUGE);
  134. // Get enrolments for each type of role.
  135. $roles = get_all_roles();
  136. $enrolments = array();
  137. foreach($roles as $role) {
  138. // Get external enrolments according to LDAP server
  139. $enrolments[$role->id]['ext'] = $this->find_ext_enrolments($user->idnumber, $role);
  140. // Get the list of current user enrolments that come from LDAP
  141. $sql= "SELECT e.courseid, ue.status, e.id as enrolid, c.shortname
  142. FROM {user} u
  143. JOIN {role_assignments} ra ON (ra.userid = u.id AND ra.component = 'enrol_ldap' AND ra.roleid = :roleid)
  144. JOIN {user_enrolments} ue ON (ue.userid = u.id AND ue.enrolid = ra.itemid)
  145. JOIN {enrol} e ON (e.id = ue.enrolid)
  146. JOIN {course} c ON (c.id = e.courseid)
  147. WHERE u.deleted = 0 AND u.id = :userid";
  148. $params = array ('roleid'=>$role->id, 'userid'=>$user->id);
  149. $enrolments[$role->id]['current'] = $DB->get_records_sql($sql, $params);
  150. }
  151. $ignorehidden = $this->get_config('ignorehiddencourses');
  152. $courseidnumber = $this->get_config('course_idnumber');
  153. foreach($roles as $role) {
  154. foreach ($enrolments[$role->id]['ext'] as $enrol) {
  155. $course_ext_id = $enrol[$courseidnumber][0];
  156. if (empty($course_ext_id)) {
  157. $trace->output(get_string('extcourseidinvalid', 'enrol_ldap'));
  158. continue; // Next; skip this one!
  159. }
  160. // Create the course if required
  161. $course = $DB->get_record('course', array($this->enrol_localcoursefield=>$course_ext_id));
  162. if (empty($course)) { // Course doesn't exist
  163. if ($this->get_config('autocreate')) { // Autocreate
  164. $trace->output(get_string('createcourseextid', 'enrol_ldap', array('courseextid'=>$course_ext_id)));
  165. if (!$newcourseid = $this->create_course($enrol, $trace)) {
  166. continue;
  167. }
  168. $course = $DB->get_record('course', array('id'=>$newcourseid));
  169. } else {
  170. $trace->output(get_string('createnotcourseextid', 'enrol_ldap', array('courseextid'=>$course_ext_id)));
  171. continue; // Next; skip this one!
  172. }
  173. }
  174. // Deal with enrolment in the moodle db
  175. // Add necessary enrol instance if not present yet;
  176. $sql = "SELECT c.id, c.visible, e.id as enrolid
  177. FROM {course} c
  178. JOIN {enrol} e ON (e.courseid = c.id AND e.enrol = 'ldap')
  179. WHERE c.id = :courseid";
  180. $params = array('courseid'=>$course->id);
  181. if (!($course_instance = $DB->get_record_sql($sql, $params, IGNORE_MULTIPLE))) {
  182. $course_instance = new stdClass();
  183. $course_instance->id = $course->id;
  184. $course_instance->visible = $course->visible;
  185. $course_instance->enrolid = $this->add_instance($course_instance);
  186. }
  187. if (!$instance = $DB->get_record('enrol', array('id'=>$course_instance->enrolid))) {
  188. continue; // Weird; skip this one.
  189. }
  190. if ($ignorehidden && !$course_instance->visible) {
  191. continue;
  192. }
  193. if (empty($enrolments[$role->id]['current'][$course->id])) {
  194. // Enrol the user in the given course, with that role.
  195. $this->enrol_user($instance, $user->id, $role->id);
  196. // Make sure we set the enrolment status to active. If the user wasn't
  197. // previously enrolled to the course, enrol_user() sets it. But if we
  198. // configured the plugin to suspend the user enrolments _AND_ remove
  199. // the role assignments on external unenrol, then enrol_user() doesn't
  200. // set it back to active on external re-enrolment. So set it
  201. // unconditionnally to cover both cases.
  202. $DB->set_field('user_enrolments', 'status', ENROL_USER_ACTIVE, array('enrolid'=>$instance->id, 'userid'=>$user->id));
  203. $trace->output(get_string('enroluser', 'enrol_ldap',
  204. array('user_username'=> $user->username,
  205. 'course_shortname'=>$course->shortname,
  206. 'course_id'=>$course->id)));
  207. } else {
  208. if ($enrolments[$role->id]['current'][$course->id]->status == ENROL_USER_SUSPENDED) {
  209. // Reenable enrolment that was previously disabled. Enrolment refreshed
  210. $DB->set_field('user_enrolments', 'status', ENROL_USER_ACTIVE, array('enrolid'=>$instance->id, 'userid'=>$user->id));
  211. $trace->output(get_string('enroluserenable', 'enrol_ldap',
  212. array('user_username'=> $user->username,
  213. 'course_shortname'=>$course->shortname,
  214. 'course_id'=>$course->id)));
  215. }
  216. }
  217. // Remove this course from the current courses, to be able to detect
  218. // which current courses should be unenroled from when we finish processing
  219. // external enrolments.
  220. unset($enrolments[$role->id]['current'][$course->id]);
  221. }
  222. // Deal with unenrolments.
  223. $transaction = $DB->start_delegated_transaction();
  224. foreach ($enrolments[$role->id]['current'] as $course) {
  225. $context = context_course::instance($course->courseid);
  226. $instance = $DB->get_record('enrol', array('id'=>$course->enrolid));
  227. switch ($this->get_config('unenrolaction')) {
  228. case ENROL_EXT_REMOVED_UNENROL:
  229. $this->unenrol_user($instance, $user->id);
  230. $trace->output(get_string('extremovedunenrol', 'enrol_ldap',
  231. array('user_username'=> $user->username,
  232. 'course_shortname'=>$course->shortname,
  233. 'course_id'=>$course->courseid)));
  234. break;
  235. case ENROL_EXT_REMOVED_KEEP:
  236. // Keep - only adding enrolments
  237. break;
  238. case ENROL_EXT_REMOVED_SUSPEND:
  239. if ($course->status != ENROL_USER_SUSPENDED) {
  240. $DB->set_field('user_enrolments', 'status', ENROL_USER_SUSPENDED, array('enrolid'=>$instance->id, 'userid'=>$user->id));
  241. $trace->output(get_string('extremovedsuspend', 'enrol_ldap',
  242. array('user_username'=> $user->username,
  243. 'course_shortname'=>$course->shortname,
  244. 'course_id'=>$course->courseid)));
  245. }
  246. break;
  247. case ENROL_EXT_REMOVED_SUSPENDNOROLES:
  248. if ($course->status != ENROL_USER_SUSPENDED) {
  249. $DB->set_field('user_enrolments', 'status', ENROL_USER_SUSPENDED, array('enrolid'=>$instance->id, 'userid'=>$user->id));
  250. }
  251. role_unassign_all(array('contextid'=>$context->id, 'userid'=>$user->id, 'component'=>'enrol_ldap', 'itemid'=>$instance->id));
  252. $trace->output(get_string('extremovedsuspendnoroles', 'enrol_ldap',
  253. array('user_username'=> $user->username,
  254. 'course_shortname'=>$course->shortname,
  255. 'course_id'=>$course->courseid)));
  256. break;
  257. }
  258. }
  259. $transaction->allow_commit();
  260. }
  261. $this->ldap_close();
  262. $trace->finished();
  263. }
  264. /**
  265. * Forces synchronisation of all enrolments with LDAP server.
  266. * It creates courses if the plugin is configured to do so.
  267. *
  268. * @param progress_trace $trace
  269. * @param int|null $onecourse limit sync to one course->id, null if all courses
  270. * @return void
  271. */
  272. public function sync_enrolments(progress_trace $trace, $onecourse = null) {
  273. global $CFG, $DB;
  274. if (!$this->ldap_connect($trace)) {
  275. $trace->finished();
  276. return;
  277. }
  278. $ldap_pagedresults = ldap_paged_results_supported($this->get_config('ldap_version'));
  279. // we may need a lot of memory here
  280. @set_time_limit(0);
  281. raise_memory_limit(MEMORY_HUGE);
  282. $oneidnumber = null;
  283. if ($onecourse) {
  284. if (!$course = $DB->get_record('course', array('id'=>$onecourse), 'id,'.$this->enrol_localcoursefield)) {
  285. // Course does not exist, nothing to do.
  286. $trace->output("Requested course $onecourse does not exist, no sync performed.");
  287. $trace->finished();
  288. return;
  289. }
  290. if (empty($course->{$this->enrol_localcoursefield})) {
  291. $trace->output("Requested course $onecourse does not have {$this->enrol_localcoursefield}, no sync performed.");
  292. $trace->finished();
  293. return;
  294. }
  295. $oneidnumber = ldap_filter_addslashes(textlib::convert($course->idnumber, 'utf-8', $this->get_config('ldapencoding')));
  296. }
  297. // Get enrolments for each type of role.
  298. $roles = get_all_roles();
  299. $enrolments = array();
  300. foreach($roles as $role) {
  301. // Get all contexts
  302. $ldap_contexts = explode(';', $this->config->{'contexts_role'.$role->id});
  303. // Get all the fields we will want for the potential course creation
  304. // as they are light. Don't get membership -- potentially a lot of data.
  305. $ldap_fields_wanted = array('dn', $this->config->course_idnumber);
  306. if (!empty($this->config->course_fullname)) {
  307. array_push($ldap_fields_wanted, $this->config->course_fullname);
  308. }
  309. if (!empty($this->config->course_shortname)) {
  310. array_push($ldap_fields_wanted, $this->config->course_shortname);
  311. }
  312. if (!empty($this->config->course_summary)) {
  313. array_push($ldap_fields_wanted, $this->config->course_summary);
  314. }
  315. array_push($ldap_fields_wanted, $this->config->{'memberattribute_role'.$role->id});
  316. // Define the search pattern
  317. $ldap_search_pattern = $this->config->objectclass;
  318. if ($oneidnumber !== null) {
  319. $ldap_search_pattern = "(&$ldap_search_pattern({$this->config->course_idnumber}=$oneidnumber))";
  320. }
  321. $ldap_cookie = '';
  322. foreach ($ldap_contexts as $ldap_context) {
  323. $ldap_context = trim($ldap_context);
  324. if (empty($ldap_context)) {
  325. continue; // Next;
  326. }
  327. $flat_records = array();
  328. do {
  329. if ($ldap_pagedresults) {
  330. ldap_control_paged_result($this->ldapconnection, $this->config->pagesize, true, $ldap_cookie);
  331. }
  332. if ($this->config->course_search_sub) {
  333. // Use ldap_search to find first user from subtree
  334. $ldap_result = @ldap_search($this->ldapconnection,
  335. $ldap_context,
  336. $ldap_search_pattern,
  337. $ldap_fields_wanted);
  338. } else {
  339. // Search only in this context
  340. $ldap_result = @ldap_list($this->ldapconnection,
  341. $ldap_context,
  342. $ldap_search_pattern,
  343. $ldap_fields_wanted);
  344. }
  345. if (!$ldap_result) {
  346. continue; // Next
  347. }
  348. if ($ldap_pagedresults) {
  349. ldap_control_paged_result_response($this->ldapconnection, $ldap_result, $ldap_cookie);
  350. }
  351. // Check and push results
  352. $records = ldap_get_entries($this->ldapconnection, $ldap_result);
  353. // LDAP libraries return an odd array, really. fix it:
  354. for ($c = 0; $c < $records['count']; $c++) {
  355. array_push($flat_records, $records[$c]);
  356. }
  357. // Free some mem
  358. unset($records);
  359. } while ($ldap_pagedresults && !empty($ldap_cookie));
  360. // If LDAP paged results were used, the current connection must be completely
  361. // closed and a new one created, to work without paged results from here on.
  362. if ($ldap_pagedresults) {
  363. $this->ldap_close();
  364. $this->ldap_connect($trace);
  365. }
  366. if (count($flat_records)) {
  367. $ignorehidden = $this->get_config('ignorehiddencourses');
  368. foreach($flat_records as $course) {
  369. $course = array_change_key_case($course, CASE_LOWER);
  370. $idnumber = $course{$this->config->course_idnumber}[0];
  371. $trace->output(get_string('synccourserole', 'enrol_ldap', array('idnumber'=>$idnumber, 'role_shortname'=>$role->shortname)));
  372. // Does the course exist in moodle already?
  373. $course_obj = $DB->get_record('course', array($this->enrol_localcoursefield=>$idnumber));
  374. if (empty($course_obj)) { // Course doesn't exist
  375. if ($this->get_config('autocreate')) { // Autocreate
  376. $trace->output(get_string('createcourseextid', 'enrol_ldap', array('courseextid'=>$idnumber)));
  377. if (!$newcourseid = $this->create_course($course, $trace)) {
  378. continue;
  379. }
  380. $course_obj = $DB->get_record('course', array('id'=>$newcourseid));
  381. } else {
  382. $trace->output(get_string('createnotcourseextid', 'enrol_ldap', array('courseextid'=>$idnumber)));
  383. continue; // Next; skip this one!
  384. }
  385. }
  386. // Enrol & unenrol
  387. // Pull the ldap membership into a nice array
  388. // this is an odd array -- mix of hash and array --
  389. $ldapmembers = array();
  390. if (array_key_exists('memberattribute_role'.$role->id, $this->config)
  391. && !empty($this->config->{'memberattribute_role'.$role->id})
  392. && !empty($course[$this->config->{'memberattribute_role'.$role->id}])) { // May have no membership!
  393. $ldapmembers = $course[$this->config->{'memberattribute_role'.$role->id}];
  394. unset($ldapmembers['count']); // Remove oddity ;)
  395. // If we have enabled nested groups, we need to expand
  396. // the groups to get the real user list. We need to do
  397. // this before dealing with 'memberattribute_isdn'.
  398. if ($this->config->nested_groups) {
  399. $users = array();
  400. foreach ($ldapmembers as $ldapmember) {
  401. $grpusers = $this->ldap_explode_group($ldapmember,
  402. $this->config->{'memberattribute_role'.$role->id});
  403. $users = array_merge($users, $grpusers);
  404. }
  405. $ldapmembers = array_unique($users); // There might be duplicates.
  406. }
  407. // Deal with the case where the member attribute holds distinguished names,
  408. // but only if the user attribute is not a distinguished name itself.
  409. if ($this->config->memberattribute_isdn
  410. && ($this->config->idnumber_attribute !== 'dn')
  411. && ($this->config->idnumber_attribute !== 'distinguishedname')) {
  412. // We need to retrieve the idnumber for all the users in $ldapmembers,
  413. // as the idnumber does not match their dn and we get dn's from membership.
  414. $memberidnumbers = array();
  415. foreach ($ldapmembers as $ldapmember) {
  416. $result = ldap_read($this->ldapconnection, $ldapmember, '(objectClass=*)',
  417. array($this->config->idnumber_attribute));
  418. $entry = ldap_first_entry($this->ldapconnection, $result);
  419. $values = ldap_get_values($this->ldapconnection, $entry, $this->config->idnumber_attribute);
  420. array_push($memberidnumbers, $values[0]);
  421. }
  422. $ldapmembers = $memberidnumbers;
  423. }
  424. }
  425. // Prune old ldap enrolments
  426. // hopefully they'll fit in the max buffer size for the RDBMS
  427. $sql= "SELECT u.id as userid, u.username, ue.status,
  428. ra.contextid, ra.itemid as instanceid
  429. FROM {user} u
  430. JOIN {role_assignments} ra ON (ra.userid = u.id AND ra.component = 'enrol_ldap' AND ra.roleid = :roleid)
  431. JOIN {user_enrolments} ue ON (ue.userid = u.id AND ue.enrolid = ra.itemid)
  432. JOIN {enrol} e ON (e.id = ue.enrolid)
  433. WHERE u.deleted = 0 AND e.courseid = :courseid ";
  434. $params = array('roleid'=>$role->id, 'courseid'=>$course_obj->id);
  435. $context = context_course::instance($course_obj->id);
  436. if (!empty($ldapmembers)) {
  437. list($ldapml, $params2) = $DB->get_in_or_equal($ldapmembers, SQL_PARAMS_NAMED, 'm', false);
  438. $sql .= "AND u.idnumber $ldapml";
  439. $params = array_merge($params, $params2);
  440. unset($params2);
  441. } else {
  442. $shortname = format_string($course_obj->shortname, true, array('context' => $context));
  443. $trace->output(get_string('emptyenrolment', 'enrol_ldap',
  444. array('role_shortname'=> $role->shortname,
  445. 'course_shortname' => $shortname)));
  446. }
  447. $todelete = $DB->get_records_sql($sql, $params);
  448. if (!empty($todelete)) {
  449. $transaction = $DB->start_delegated_transaction();
  450. foreach ($todelete as $row) {
  451. $instance = $DB->get_record('enrol', array('id'=>$row->instanceid));
  452. switch ($this->get_config('unenrolaction')) {
  453. case ENROL_EXT_REMOVED_UNENROL:
  454. $this->unenrol_user($instance, $row->userid);
  455. $trace->output(get_string('extremovedunenrol', 'enrol_ldap',
  456. array('user_username'=> $row->username,
  457. 'course_shortname'=>$course_obj->shortname,
  458. 'course_id'=>$course_obj->id)));
  459. break;
  460. case ENROL_EXT_REMOVED_KEEP:
  461. // Keep - only adding enrolments
  462. break;
  463. case ENROL_EXT_REMOVED_SUSPEND:
  464. if ($row->status != ENROL_USER_SUSPENDED) {
  465. $DB->set_field('user_enrolments', 'status', ENROL_USER_SUSPENDED, array('enrolid'=>$instance->id, 'userid'=>$row->userid));
  466. $trace->output(get_string('extremovedsuspend', 'enrol_ldap',
  467. array('user_username'=> $row->username,
  468. 'course_shortname'=>$course_obj->shortname,
  469. 'course_id'=>$course_obj->id)));
  470. }
  471. break;
  472. case ENROL_EXT_REMOVED_SUSPENDNOROLES:
  473. if ($row->status != ENROL_USER_SUSPENDED) {
  474. $DB->set_field('user_enrolments', 'status', ENROL_USER_SUSPENDED, array('enrolid'=>$instance->id, 'userid'=>$row->userid));
  475. }
  476. role_unassign_all(array('contextid'=>$row->contextid, 'userid'=>$row->userid, 'component'=>'enrol_ldap', 'itemid'=>$instance->id));
  477. $trace->output(get_string('extremovedsuspendnoroles', 'enrol_ldap',
  478. array('user_username'=> $row->username,
  479. 'course_shortname'=>$course_obj->shortname,
  480. 'course_id'=>$course_obj->id)));
  481. break;
  482. }
  483. }
  484. $transaction->allow_commit();
  485. }
  486. // Insert current enrolments
  487. // bad we can't do INSERT IGNORE with postgres...
  488. // Add necessary enrol instance if not present yet;
  489. $sql = "SELECT c.id, c.visible, e.id as enrolid
  490. FROM {course} c
  491. JOIN {enrol} e ON (e.courseid = c.id AND e.enrol = 'ldap')
  492. WHERE c.id = :courseid";
  493. $params = array('courseid'=>$course_obj->id);
  494. if (!($course_instance = $DB->get_record_sql($sql, $params, IGNORE_MULTIPLE))) {
  495. $course_instance = new stdClass();
  496. $course_instance->id = $course_obj->id;
  497. $course_instance->visible = $course_obj->visible;
  498. $course_instance->enrolid = $this->add_instance($course_instance);
  499. }
  500. if (!$instance = $DB->get_record('enrol', array('id'=>$course_instance->enrolid))) {
  501. continue; // Weird; skip this one.
  502. }
  503. if ($ignorehidden && !$course_instance->visible) {
  504. continue;
  505. }
  506. $transaction = $DB->start_delegated_transaction();
  507. foreach ($ldapmembers as $ldapmember) {
  508. $sql = 'SELECT id,username,1 FROM {user} WHERE idnumber = ? AND deleted = 0';
  509. $member = $DB->get_record_sql($sql, array($ldapmember));
  510. if(empty($member) || empty($member->id)){
  511. $trace->output(get_string('couldnotfinduser', 'enrol_ldap', $ldapmember));
  512. continue;
  513. }
  514. $sql= "SELECT ue.status
  515. FROM {user_enrolments} ue
  516. JOIN {enrol} e ON (e.id = ue.enrolid AND e.enrol = 'ldap')
  517. WHERE e.courseid = :courseid AND ue.userid = :userid";
  518. $params = array('courseid'=>$course_obj->id, 'userid'=>$member->id);
  519. $userenrolment = $DB->get_record_sql($sql, $params, IGNORE_MULTIPLE);
  520. if (empty($userenrolment)) {
  521. $this->enrol_user($instance, $member->id, $role->id);
  522. // Make sure we set the enrolment status to active. If the user wasn't
  523. // previously enrolled to the course, enrol_user() sets it. But if we
  524. // configured the plugin to suspend the user enrolments _AND_ remove
  525. // the role assignments on external unenrol, then enrol_user() doesn't
  526. // set it back to active on external re-enrolment. So set it
  527. // unconditionally to cover both cases.
  528. $DB->set_field('user_enrolments', 'status', ENROL_USER_ACTIVE, array('enrolid'=>$instance->id, 'userid'=>$member->id));
  529. $trace->output(get_string('enroluser', 'enrol_ldap',
  530. array('user_username'=> $member->username,
  531. 'course_shortname'=>$course_obj->shortname,
  532. 'course_id'=>$course_obj->id)));
  533. } else {
  534. if (!$DB->record_exists('role_assignments', array('roleid'=>$role->id, 'userid'=>$member->id, 'contextid'=>$context->id, 'component'=>'enrol_ldap', 'itemid'=>$instance->id))) {
  535. // This happens when reviving users or when user has multiple roles in one course.
  536. $context = context_course::instance($course_obj->id);
  537. role_assign($role->id, $member->id, $context->id, 'enrol_ldap', $instance->id);
  538. $trace->output("Assign role to user '$member->username' in course '$course_obj->shortname ($course_obj->id)'");
  539. }
  540. if ($userenrolment->status == ENROL_USER_SUSPENDED) {
  541. // Reenable enrolment that was previously disabled. Enrolment refreshed
  542. $DB->set_field('user_enrolments', 'status', ENROL_USER_ACTIVE, array('enrolid'=>$instance->id, 'userid'=>$member->id));
  543. $trace->output(get_string('enroluserenable', 'enrol_ldap',
  544. array('user_username'=> $member->username,
  545. 'course_shortname'=>$course_obj->shortname,
  546. 'course_id'=>$course_obj->id)));
  547. }
  548. }
  549. }
  550. $transaction->allow_commit();
  551. }
  552. }
  553. }
  554. }
  555. @$this->ldap_close();
  556. $trace->finished();
  557. }
  558. /**
  559. * Connect to the LDAP server, using the plugin configured
  560. * settings. It's actually a wrapper around ldap_connect_moodle()
  561. *
  562. * @param progress_trace $trace
  563. * @return bool success
  564. */
  565. protected function ldap_connect(progress_trace $trace = null) {
  566. global $CFG;
  567. require_once($CFG->libdir.'/ldaplib.php');
  568. if (isset($this->ldapconnection)) {
  569. return true;
  570. }
  571. if ($ldapconnection = ldap_connect_moodle($this->get_config('host_url'), $this->get_config('ldap_version'),
  572. $this->get_config('user_type'), $this->get_config('bind_dn'),
  573. $this->get_config('bind_pw'), $this->get_config('opt_deref'),
  574. $debuginfo, $this->get_config('start_tls'))) {
  575. $this->ldapconnection = $ldapconnection;
  576. return true;
  577. }
  578. if ($trace) {
  579. $trace->output($debuginfo);
  580. } else {
  581. error_log($this->errorlogtag.$debuginfo);
  582. }
  583. return false;
  584. }
  585. /**
  586. * Disconnects from a LDAP server
  587. *
  588. */
  589. protected function ldap_close() {
  590. if (isset($this->ldapconnection)) {
  591. @ldap_close($this->ldapconnection);
  592. $this->ldapconnection = null;
  593. }
  594. return;
  595. }
  596. /**
  597. * Return multidimensional array with details of user courses (at
  598. * least dn and idnumber).
  599. *
  600. * @param string $memberuid user idnumber (without magic quotes).
  601. * @param object role is a record from the mdl_role table.
  602. * @return array
  603. */
  604. protected function find_ext_enrolments($memberuid, $role) {
  605. global $CFG;
  606. require_once($CFG->libdir.'/ldaplib.php');
  607. if (empty($memberuid)) {
  608. // No "idnumber" stored for this user, so no LDAP enrolments
  609. return array();
  610. }
  611. $ldap_contexts = trim($this->get_config('contexts_role'.$role->id));
  612. if (empty($ldap_contexts)) {
  613. // No role contexts, so no LDAP enrolments
  614. return array();
  615. }
  616. $extmemberuid = textlib::convert($memberuid, 'utf-8', $this->get_config('ldapencoding'));
  617. if($this->get_config('memberattribute_isdn')) {
  618. if (!($extmemberuid = $this->ldap_find_userdn($extmemberuid))) {
  619. return array();
  620. }
  621. }
  622. $ldap_search_pattern = '';
  623. if($this->get_config('nested_groups')) {
  624. $usergroups = $this->ldap_find_user_groups($extmemberuid);
  625. if(count($usergroups) > 0) {
  626. foreach ($usergroups as $group) {
  627. $ldap_search_pattern .= '('.$this->get_config('memberattribute_role'.$role->id).'='.$group.')';
  628. }
  629. }
  630. }
  631. // Default return value
  632. $courses = array();
  633. // Get all the fields we will want for the potential course creation
  634. // as they are light. don't get membership -- potentially a lot of data.
  635. $ldap_fields_wanted = array('dn', $this->get_config('course_idnumber'));
  636. $fullname = $this->get_config('course_fullname');
  637. $shortname = $this->get_config('course_shortname');
  638. $summary = $this->get_config('course_summary');
  639. if (isset($fullname)) {
  640. array_push($ldap_fields_wanted, $fullname);
  641. }
  642. if (isset($shortname)) {
  643. array_push($ldap_fields_wanted, $shortname);
  644. }
  645. if (isset($summary)) {
  646. array_push($ldap_fields_wanted, $summary);
  647. }
  648. // Define the search pattern
  649. if (empty($ldap_search_pattern)) {
  650. $ldap_search_pattern = '('.$this->get_config('memberattribute_role'.$role->id).'='.ldap_filter_addslashes($extmemberuid).')';
  651. } else {
  652. $ldap_search_pattern = '(|' . $ldap_search_pattern .
  653. '('.$this->get_config('memberattribute_role'.$role->id).'='.ldap_filter_addslashes($extmemberuid).')' .
  654. ')';
  655. }
  656. $ldap_search_pattern='(&'.$this->get_config('objectclass').$ldap_search_pattern.')';
  657. // Get all contexts and look for first matching user
  658. $ldap_contexts = explode(';', $ldap_contexts);
  659. $ldap_pagedresults = ldap_paged_results_supported($this->get_config('ldap_version'));
  660. foreach ($ldap_contexts as $context) {
  661. $context = trim($context);
  662. if (empty($context)) {
  663. continue;
  664. }
  665. $ldap_cookie = '';
  666. $flat_records = array();
  667. do {
  668. if ($ldap_pagedresults) {
  669. ldap_control_paged_result($this->ldapconnection, $this->config->pagesize, true, $ldap_cookie);
  670. }
  671. if ($this->get_config('course_search_sub')) {
  672. // Use ldap_search to find first user from subtree
  673. $ldap_result = @ldap_search($this->ldapconnection,
  674. $context,
  675. $ldap_search_pattern,
  676. $ldap_fields_wanted);
  677. } else {
  678. // Search only in this context
  679. $ldap_result = @ldap_list($this->ldapconnection,
  680. $context,
  681. $ldap_search_pattern,
  682. $ldap_fields_wanted);
  683. }
  684. if (!$ldap_result) {
  685. continue;
  686. }
  687. if ($ldap_pagedresults) {
  688. ldap_control_paged_result_response($this->ldapconnection, $ldap_result, $ldap_cookie);
  689. }
  690. // Check and push results. ldap_get_entries() already
  691. // lowercases the attribute index, so there's no need to
  692. // use array_change_key_case() later.
  693. $records = ldap_get_entries($this->ldapconnection, $ldap_result);
  694. // LDAP libraries return an odd array, really. Fix it.
  695. for ($c = 0; $c < $records['count']; $c++) {
  696. array_push($flat_records, $records[$c]);
  697. }
  698. // Free some mem
  699. unset($records);
  700. } while ($ldap_pagedresults && !empty($ldap_cookie));
  701. // If LDAP paged results were used, the current connection must be completely
  702. // closed and a new one created, to work without paged results from here on.
  703. if ($ldap_pagedresults) {
  704. $this->ldap_close();
  705. $this->ldap_connect();
  706. }
  707. if (count($flat_records)) {
  708. $courses = array_merge($courses, $flat_records);
  709. }
  710. }
  711. return $courses;
  712. }
  713. /**
  714. * Search specified contexts for the specified userid and return the
  715. * user dn like: cn=username,ou=suborg,o=org. It's actually a wrapper
  716. * around ldap_find_userdn().
  717. *
  718. * @param string $userid the userid to search for (in external LDAP encoding, no magic quotes).
  719. * @return mixed the user dn or false
  720. */
  721. protected function ldap_find_userdn($userid) {
  722. global $CFG;
  723. require_once($CFG->libdir.'/ldaplib.php');
  724. $ldap_contexts = explode(';', $this->get_config('user_contexts'));
  725. $ldap_defaults = ldap_getdefaults();
  726. return ldap_find_userdn($this->ldapconnection, $userid, $ldap_contexts,
  727. '(objectClass='.$ldap_defaults['objectclass'][$this->get_config('user_type')].')',
  728. $this->get_config('idnumber_attribute'), $this->get_config('user_search_sub'));
  729. }
  730. /**
  731. * Find the groups a given distinguished name belongs to, both directly
  732. * and indirectly via nested groups membership.
  733. *
  734. * @param string $memberdn distinguished name to search
  735. * @return array with member groups' distinguished names (can be emtpy)
  736. */
  737. protected function ldap_find_user_groups($memberdn) {
  738. $groups = array();
  739. $this->ldap_find_user_groups_recursively($memberdn, $groups);
  740. return $groups;
  741. }
  742. /**
  743. * Recursively process the groups the given member distinguished name
  744. * belongs to, adding them to the already processed groups array.
  745. *
  746. * @param string $memberdn distinguished name to search
  747. * @param array reference &$membergroups array with already found
  748. * groups, where we'll put the newly found
  749. * groups.
  750. */
  751. protected function ldap_find_user_groups_recursively($memberdn, &$membergroups) {
  752. $result = @ldap_read($this->ldapconnection, $memberdn, '(objectClass=*)', array($this->get_config('group_memberofattribute')));
  753. if (!$result) {
  754. return;
  755. }
  756. if ($entry = ldap_first_entry($this->ldapconnection, $result)) {
  757. do {
  758. $attributes = ldap_get_attributes($this->ldapconnection, $entry);
  759. for ($j = 0; $j < $attributes['count']; $j++) {
  760. $groups = ldap_get_values_len($this->ldapconnection, $entry, $attributes[$j]);
  761. foreach ($groups as $key => $group) {
  762. if ($key === 'count') { // Skip the entries count
  763. continue;
  764. }
  765. if(!in_array($group, $membergroups)) {
  766. // Only push and recurse if we haven't 'seen' this group before
  767. // to prevent loops (MS Active Directory allows them!!).
  768. array_push($membergroups, $group);
  769. $this->ldap_find_user_groups_recursively($group, $membergroups);
  770. }
  771. }
  772. }
  773. }
  774. while ($entry = ldap_next_entry($this->ldapconnection, $entry));
  775. }
  776. }
  777. /**
  778. * Given a group name (either a RDN or a DN), get the list of users
  779. * belonging to that group. If the group has nested groups, expand all
  780. * the intermediate groups and return the full list of users that
  781. * directly or indirectly belong to the group.
  782. *
  783. * @param string $group the group name to search
  784. * @param string $memberattibute the attribute that holds the members of the group
  785. * @return array the list of users belonging to the group. If $group
  786. * is not actually a group, returns array($group).
  787. */
  788. protected function ldap_explode_group($group, $memberattribute) {
  789. switch ($this->get_config('user_type')) {
  790. case 'ad':
  791. // $group is already the distinguished name to search.
  792. $dn = $group;
  793. $result = ldap_read($this->ldapconnection, $dn, '(objectClass=*)', array('objectClass'));
  794. $entry = ldap_first_entry($this->ldapconnection, $result);
  795. $objectclass = ldap_get_values($this->ldapconnection, $entry, 'objectClass');
  796. if (!in_array('group', $objectclass)) {
  797. // Not a group, so return immediately.
  798. return array($group);
  799. }
  800. $result = ldap_read($this->ldapconnection, $dn, '(objectClass=*)', array($memberattribute));
  801. $entry = ldap_first_entry($this->ldapconnection, $result);
  802. $members = @ldap_get_values($this->ldapconnection, $entry, $memberattribute); // Can be empty and throws a warning
  803. if ($members['count'] == 0) {
  804. // There are no members in this group, return nothing.
  805. return array();
  806. }
  807. unset($members['count']);
  808. $users = array();
  809. foreach ($members as $member) {
  810. $group_members = $this->ldap_explode_group($member, $memberattribute);
  811. $users = array_merge($users, $group_members);
  812. }
  813. return ($users);
  814. break;
  815. default:
  816. error_log($this->errorlogtag.get_string('explodegroupusertypenotsupported', 'enrol_ldap',
  817. $this->get_config('user_type_name')));
  818. return array($group);
  819. }
  820. }
  821. /**
  822. * Will create the moodle course from the template
  823. * course_ext is an array as obtained from ldap -- flattened somewhat
  824. *
  825. * @param array $course_ext
  826. * @param progress_trace $trace
  827. * @return mixed false on error, id for the newly created course otherwise.
  828. */
  829. function create_course($course_ext, progress_trace $trace) {
  830. global $CFG, $DB;
  831. require_once("$CFG->dirroot/course/lib.php");
  832. // Override defaults with template course
  833. $template = false;
  834. if ($this->get_config('template')) {
  835. if ($template = $DB->get_record('course', array('shortname'=>$this->get_config('template')))) {
  836. $template = fullclone(course_get_format($template)->get_course());
  837. unset($template->id); // So we are clear to reinsert the record
  838. unset($template->fullname);
  839. unset($template->shortname);
  840. unset($template->idnumber);
  841. }
  842. }
  843. if (!$template) {
  844. $courseconfig = get_config('moodlecourse');
  845. $template = new stdClass();
  846. $template->summary = '';
  847. $template->summaryformat = FORMAT_HTML;
  848. $template->format = $courseconfig->format;
  849. $template->newsitems = $courseconfig->newsitems;
  850. $template->showgrades = $courseconfig->showgrades;
  851. $template->showreports = $courseconfig->showreports;
  852. $template->maxbytes = $courseconfig->maxbytes;
  853. $template->groupmode = $courseconfig->groupmode;
  854. $template->groupmodeforce = $courseconfig->groupmodeforce;
  855. $template->visible = $courseconfig->visible;
  856. $template->lang = $courseconfig->lang;
  857. $template->groupmodeforce = $courseconfig->groupmodeforce;
  858. }
  859. $course = $template;
  860. $course->category = $this->get_config('category');
  861. if (!$DB->record_exists('course_categories', array('id'=>$this->get_config('category')))) {
  862. $categories = $DB->get_records('course_categories', array(), 'sortorder', 'id', 0, 1);
  863. $first = reset($categories);
  864. $course->category = $first->id;
  865. }
  866. // Override with required ext data
  867. $course->idnumber = $course_ext[$this->get_config('course_idnumber')][0];
  868. $course->fullname = $course_ext[$this->get_config('course_fullname')][0];
  869. $course->shortname = $course_ext[$this->get_config('course_shortname')][0];
  870. if (empty($course->idnumber) || empty($course->fullname) || empty($course->shortname)) {
  871. // We are in trouble!
  872. $trace->output(get_string('cannotcreatecourse', 'enrol_ldap').' '.var_export($course, true));
  873. return false;
  874. }
  875. $summary = $this->get_config('course_summary');
  876. if (!isset($summary) || empty($course_ext[$summary][0])) {
  877. $course->summary = '';
  878. } else {