PageRenderTime 52ms CodeModel.GetById 16ms RepoModel.GetById 0ms app.codeStats 1ms

/moodle/enrol/ldap/lib.php

#
PHP | 946 lines | 608 code | 104 blank | 234 comment | 112 complexity | fbed6b6a46125a9eae22ef37cecd27be MD5 | raw file
Possible License(s): GPL-3.0, LGPL-2.1, BSD-3-Clause, AGPL-3.0, MPL-2.0-no-copyleft-exception, LGPL-3.0, Apache-2.0
  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. $ldapconnection = $this->ldap_connect();
  119. if (!$ldapconnection) {
  120. return;
  121. }
  122. if (!is_object($user) or !property_exists($user, 'id')) {
  123. throw new coding_exception('Invalid $user parameter in sync_user_enrolments()');
  124. }
  125. if (!property_exists($user, 'idnumber')) {
  126. debugging('Invalid $user parameter in sync_user_enrolments(), missing idnumber');
  127. $user = $DB->get_record('user', array('id'=>$user->id));
  128. }
  129. // We may need a lot of memory here
  130. @set_time_limit(0);
  131. raise_memory_limit(MEMORY_HUGE);
  132. // Get enrolments for each type of role.
  133. $roles = get_all_roles();
  134. $enrolments = array();
  135. foreach($roles as $role) {
  136. // Get external enrolments according to LDAP server
  137. $enrolments[$role->id]['ext'] = $this->find_ext_enrolments($ldapconnection, $user->idnumber, $role);
  138. // Get the list of current user enrolments that come from LDAP
  139. $sql= "SELECT e.courseid, ue.status, e.id as enrolid, c.shortname
  140. FROM {user} u
  141. JOIN {role_assignments} ra ON (ra.userid = u.id AND ra.component = 'enrol_ldap' AND ra.roleid = :roleid)
  142. JOIN {user_enrolments} ue ON (ue.userid = u.id AND ue.enrolid = ra.itemid)
  143. JOIN {enrol} e ON (e.id = ue.enrolid)
  144. JOIN {course} c ON (c.id = e.courseid)
  145. WHERE u.deleted = 0 AND u.id = :userid";
  146. $params = array ('roleid'=>$role->id, 'userid'=>$user->id);
  147. $enrolments[$role->id]['current'] = $DB->get_records_sql($sql, $params);
  148. }
  149. $ignorehidden = $this->get_config('ignorehiddencourses');
  150. $courseidnumber = $this->get_config('course_idnumber');
  151. foreach($roles as $role) {
  152. foreach ($enrolments[$role->id]['ext'] as $enrol) {
  153. $course_ext_id = $enrol[$courseidnumber][0];
  154. if (empty($course_ext_id)) {
  155. error_log($this->errorlogtag.get_string('extcourseidinvalid', 'enrol_ldap'));
  156. continue; // Next; skip this one!
  157. }
  158. // Create the course if required
  159. $course = $DB->get_record('course', array($this->enrol_localcoursefield=>$course_ext_id));
  160. if (empty($course)) { // Course doesn't exist
  161. if ($this->get_config('autocreate')) { // Autocreate
  162. error_log($this->errorlogtag.get_string('createcourseextid', 'enrol_ldap',
  163. array('courseextid'=>$course_ext_id)));
  164. if ($newcourseid = $this->create_course($enrol)) {
  165. $course = $DB->get_record('course', array('id'=>$newcourseid));
  166. }
  167. } else {
  168. error_log($this->errorlogtag.get_string('createnotcourseextid', 'enrol_ldap',
  169. array('courseextid'=>$course_ext_id)));
  170. continue; // Next; skip this one!
  171. }
  172. }
  173. // Deal with enrolment in the moodle db
  174. // Add necessary enrol instance if not present yet;
  175. $sql = "SELECT c.id, c.visible, e.id as enrolid
  176. FROM {course} c
  177. JOIN {enrol} e ON (e.courseid = c.id AND e.enrol = 'ldap')
  178. WHERE c.id = :courseid";
  179. $params = array('courseid'=>$course->id);
  180. if (!($course_instance = $DB->get_record_sql($sql, $params, IGNORE_MULTIPLE))) {
  181. $course_instance = new stdClass();
  182. $course_instance->id = $course->id;
  183. $course_instance->visible = $course->visible;
  184. $course_instance->enrolid = $this->add_instance($course_instance);
  185. }
  186. if (!$instance = $DB->get_record('enrol', array('id'=>$course_instance->enrolid))) {
  187. continue; // Weird; skip this one.
  188. }
  189. if ($ignorehidden && !$course_instance->visible) {
  190. continue;
  191. }
  192. if (empty($enrolments[$role->id]['current'][$course->id])) {
  193. // Enrol the user in the given course, with that role.
  194. $this->enrol_user($instance, $user->id, $role->id);
  195. // Make sure we set the enrolment status to active. If the user wasn't
  196. // previously enrolled to the course, enrol_user() sets it. But if we
  197. // configured the plugin to suspend the user enrolments _AND_ remove
  198. // the role assignments on external unenrol, then enrol_user() doesn't
  199. // set it back to active on external re-enrolment. So set it
  200. // unconditionnally to cover both cases.
  201. $DB->set_field('user_enrolments', 'status', ENROL_USER_ACTIVE, array('enrolid'=>$instance->id, 'userid'=>$user->id));
  202. error_log($this->errorlogtag.get_string('enroluser', 'enrol_ldap',
  203. array('user_username'=> $user->username,
  204. 'course_shortname'=>$course->shortname,
  205. 'course_id'=>$course->id)));
  206. } else {
  207. if ($enrolments[$role->id]['current'][$course->id]->status == ENROL_USER_SUSPENDED) {
  208. // Reenable enrolment that was previously disabled. Enrolment refreshed
  209. $DB->set_field('user_enrolments', 'status', ENROL_USER_ACTIVE, array('enrolid'=>$instance->id, 'userid'=>$user->id));
  210. error_log($this->errorlogtag.get_string('enroluserenable', 'enrol_ldap',
  211. array('user_username'=> $user->username,
  212. 'course_shortname'=>$course->shortname,
  213. 'course_id'=>$course->id)));
  214. }
  215. }
  216. // Remove this course from the current courses, to be able to detect
  217. // which current courses should be unenroled from when we finish processing
  218. // external enrolments.
  219. unset($enrolments[$role->id]['current'][$course->id]);
  220. }
  221. // Deal with unenrolments.
  222. $transaction = $DB->start_delegated_transaction();
  223. foreach ($enrolments[$role->id]['current'] as $course) {
  224. $context = get_context_instance(CONTEXT_COURSE, $course->courseid);
  225. $instance = $DB->get_record('enrol', array('id'=>$course->enrolid));
  226. switch ($this->get_config('unenrolaction')) {
  227. case ENROL_EXT_REMOVED_UNENROL:
  228. $this->unenrol_user($instance, $user->id);
  229. error_log($this->errorlogtag.get_string('extremovedunenrol', 'enrol_ldap',
  230. array('user_username'=> $user->username,
  231. 'course_shortname'=>$course->shortname,
  232. 'course_id'=>$course->courseid)));
  233. break;
  234. case ENROL_EXT_REMOVED_KEEP:
  235. // Keep - only adding enrolments
  236. break;
  237. case ENROL_EXT_REMOVED_SUSPEND:
  238. if ($course->status != ENROL_USER_SUSPENDED) {
  239. $DB->set_field('user_enrolments', 'status', ENROL_USER_SUSPENDED, array('enrolid'=>$instance->id, 'userid'=>$user->id));
  240. error_log($this->errorlogtag.get_string('extremovedsuspend', 'enrol_ldap',
  241. array('user_username'=> $user->username,
  242. 'course_shortname'=>$course->shortname,
  243. 'course_id'=>$course->courseid)));
  244. }
  245. break;
  246. case ENROL_EXT_REMOVED_SUSPENDNOROLES:
  247. if ($course->status != ENROL_USER_SUSPENDED) {
  248. $DB->set_field('user_enrolments', 'status', ENROL_USER_SUSPENDED, array('enrolid'=>$instance->id, 'userid'=>$user->id));
  249. }
  250. role_unassign_all(array('contextid'=>$context->id, 'userid'=>$user->id, 'component'=>'enrol_ldap', 'itemid'=>$instance->id));
  251. error_log($this->errorlogtag.get_string('extremovedsuspendnoroles', 'enrol_ldap',
  252. array('user_username'=> $user->username,
  253. 'course_shortname'=>$course->shortname,
  254. 'course_id'=>$course->courseid)));
  255. break;
  256. }
  257. }
  258. $transaction->allow_commit();
  259. }
  260. $this->ldap_close($ldapconnection);
  261. }
  262. /**
  263. * Forces synchronisation of all enrolments with LDAP server.
  264. * It creates courses if the plugin is configured to do so.
  265. *
  266. * @return void
  267. */
  268. public function sync_enrolments() {
  269. global $CFG, $DB;
  270. $ldapconnection = $this->ldap_connect();
  271. if (!$ldapconnection) {
  272. return;
  273. }
  274. // we may need a lot of memory here
  275. @set_time_limit(0);
  276. raise_memory_limit(MEMORY_HUGE);
  277. // Get enrolments for each type of role.
  278. $roles = get_all_roles();
  279. $enrolments = array();
  280. foreach($roles as $role) {
  281. // Get all contexts
  282. $ldap_contexts = explode(';', $this->config->{'contexts_role'.$role->id});
  283. // Get all the fields we will want for the potential course creation
  284. // as they are light. Don't get membership -- potentially a lot of data.
  285. $ldap_fields_wanted = array('dn', $this->config->course_idnumber);
  286. if (!empty($this->config->course_fullname)) {
  287. array_push($ldap_fields_wanted, $this->config->course_fullname);
  288. }
  289. if (!empty($this->config->course_shortname)) {
  290. array_push($ldap_fields_wanted, $this->config->course_shortname);
  291. }
  292. if (!empty($this->config->course_summary)) {
  293. array_push($ldap_fields_wanted, $this->config->course_summary);
  294. }
  295. array_push($ldap_fields_wanted, $this->config->{'memberattribute_role'.$role->id});
  296. // Define the search pattern
  297. $ldap_search_pattern = $this->config->objectclass;
  298. foreach ($ldap_contexts as $ldap_context) {
  299. $ldap_context = trim($ldap_context);
  300. if (empty($ldap_context)) {
  301. continue; // Next;
  302. }
  303. if ($this->config->course_search_sub) {
  304. // Use ldap_search to find first user from subtree
  305. $ldap_result = @ldap_search($ldapconnection,
  306. $ldap_context,
  307. $ldap_search_pattern,
  308. $ldap_fields_wanted);
  309. } else {
  310. // Search only in this context
  311. $ldap_result = @ldap_list($ldapconnection,
  312. $ldap_context,
  313. $ldap_search_pattern,
  314. $ldap_fields_wanted);
  315. }
  316. if (!$ldap_result) {
  317. continue; // Next
  318. }
  319. // Check and push results
  320. $records = ldap_get_entries($ldapconnection, $ldap_result);
  321. // LDAP libraries return an odd array, really. fix it:
  322. $flat_records = array();
  323. for ($c = 0; $c < $records['count']; $c++) {
  324. array_push($flat_records, $records[$c]);
  325. }
  326. // Free some mem
  327. unset($records);
  328. if (count($flat_records)) {
  329. $ignorehidden = $this->get_config('ignorehiddencourses');
  330. foreach($flat_records as $course) {
  331. $course = array_change_key_case($course, CASE_LOWER);
  332. $idnumber = $course{$this->config->course_idnumber}[0];
  333. print_string('synccourserole', 'enrol_ldap',
  334. array('idnumber'=>$idnumber, 'role_shortname'=>$role->shortname));
  335. // Does the course exist in moodle already?
  336. $course_obj = $DB->get_record('course', array($this->enrol_localcoursefield=>$idnumber));
  337. if (empty($course_obj)) { // Course doesn't exist
  338. if ($this->get_config('autocreate')) { // Autocreate
  339. error_log($this->errorlogtag.get_string('createcourseextid', 'enrol_ldap',
  340. array('courseextid'=>$idnumber)));
  341. if ($newcourseid = $this->create_course($course)) {
  342. $course_obj = $DB->get_record('course', array('id'=>$newcourseid));
  343. }
  344. } else {
  345. error_log($this->errorlogtag.get_string('createnotcourseextid', 'enrol_ldap',
  346. array('courseextid'=>$idnumber)));
  347. continue; // Next; skip this one!
  348. }
  349. }
  350. // Enrol & unenrol
  351. // Pull the ldap membership into a nice array
  352. // this is an odd array -- mix of hash and array --
  353. $ldapmembers = array();
  354. if (array_key_exists('memberattribute_role'.$role->id, $this->config)
  355. && !empty($this->config->{'memberattribute_role'.$role->id})
  356. && !empty($course[$this->config->{'memberattribute_role'.$role->id}])) { // May have no membership!
  357. $ldapmembers = $course[$this->config->{'memberattribute_role'.$role->id}];
  358. unset($ldapmembers['count']); // Remove oddity ;)
  359. // If we have enabled nested groups, we need to expand
  360. // the groups to get the real user list. We need to do
  361. // this before dealing with 'memberattribute_isdn'.
  362. if ($this->config->nested_groups) {
  363. $users = array();
  364. foreach ($ldapmembers as $ldapmember) {
  365. $grpusers = $this->ldap_explode_group($ldapconnection,
  366. $ldapmember,
  367. $this->config->{'memberattribute_role'.$role->id});
  368. $users = array_merge($users, $grpusers);
  369. }
  370. $ldapmembers = array_unique($users); // There might be duplicates.
  371. }
  372. // Deal with the case where the member attribute holds distinguished names,
  373. // but only if the user attribute is not a distinguished name itself.
  374. if ($this->config->memberattribute_isdn
  375. && ($this->config->idnumber_attribute !== 'dn')
  376. && ($this->config->idnumber_attribute !== 'distinguishedname')) {
  377. // We need to retrieve the idnumber for all the users in $ldapmembers,
  378. // as the idnumber does not match their dn and we get dn's from membership.
  379. $memberidnumbers = array();
  380. foreach ($ldapmembers as $ldapmember) {
  381. $result = ldap_read($ldapconnection, $ldapmember, '(objectClass=*)',
  382. array($this->config->idnumber_attribute));
  383. $entry = ldap_first_entry($ldapconnection, $result);
  384. $values = ldap_get_values($ldapconnection, $entry, $this->config->idnumber_attribute);
  385. array_push($memberidnumbers, $values[0]);
  386. }
  387. $ldapmembers = $memberidnumbers;
  388. }
  389. }
  390. // Prune old ldap enrolments
  391. // hopefully they'll fit in the max buffer size for the RDBMS
  392. $sql= "SELECT u.id as userid, u.username, ue.status,
  393. ra.contextid, ra.itemid as instanceid
  394. FROM {user} u
  395. JOIN {role_assignments} ra ON (ra.userid = u.id AND ra.component = 'enrol_ldap' AND ra.roleid = :roleid)
  396. JOIN {user_enrolments} ue ON (ue.userid = u.id AND ue.enrolid = ra.itemid)
  397. JOIN {enrol} e ON (e.id = ue.enrolid)
  398. WHERE u.deleted = 0 AND e.courseid = :courseid ";
  399. $params = array('roleid'=>$role->id, 'courseid'=>$course_obj->id);
  400. $context = get_context_instance(CONTEXT_COURSE, $course_obj->id);
  401. if (!empty($ldapmembers)) {
  402. list($ldapml, $params2) = $DB->get_in_or_equal($ldapmembers, SQL_PARAMS_NAMED, 'm', false);
  403. $sql .= "AND u.idnumber $ldapml";
  404. $params = array_merge($params, $params2);
  405. unset($params2);
  406. } else {
  407. $shortname = format_string($course_obj->shortname, true, array('context' => $context));
  408. print_string('emptyenrolment', 'enrol_ldap',
  409. array('role_shortname'=> $role->shortname,
  410. 'course_shortname' => $shortname));
  411. }
  412. $todelete = $DB->get_records_sql($sql, $params);
  413. if (!empty($todelete)) {
  414. $transaction = $DB->start_delegated_transaction();
  415. foreach ($todelete as $row) {
  416. $instance = $DB->get_record('enrol', array('id'=>$row->instanceid));
  417. switch ($this->get_config('unenrolaction')) {
  418. case ENROL_EXT_REMOVED_UNENROL:
  419. $this->unenrol_user($instance, $row->userid);
  420. error_log($this->errorlogtag.get_string('extremovedunenrol', 'enrol_ldap',
  421. array('user_username'=> $row->username,
  422. 'course_shortname'=>$course_obj->shortname,
  423. 'course_id'=>$course_obj->id)));
  424. break;
  425. case ENROL_EXT_REMOVED_KEEP:
  426. // Keep - only adding enrolments
  427. break;
  428. case ENROL_EXT_REMOVED_SUSPEND:
  429. if ($row->status != ENROL_USER_SUSPENDED) {
  430. $DB->set_field('user_enrolments', 'status', ENROL_USER_SUSPENDED, array('enrolid'=>$instance->id, 'userid'=>$row->userid));
  431. error_log($this->errorlogtag.get_string('extremovedsuspend', 'enrol_ldap',
  432. array('user_username'=> $row->username,
  433. 'course_shortname'=>$course_obj->shortname,
  434. 'course_id'=>$course_obj->id)));
  435. }
  436. break;
  437. case ENROL_EXT_REMOVED_SUSPENDNOROLES:
  438. if ($row->status != ENROL_USER_SUSPENDED) {
  439. $DB->set_field('user_enrolments', 'status', ENROL_USER_SUSPENDED, array('enrolid'=>$instance->id, 'userid'=>$row->userid));
  440. }
  441. role_unassign_all(array('contextid'=>$row->contextid, 'userid'=>$row->userid, 'component'=>'enrol_ldap', 'itemid'=>$instance->id));
  442. error_log($this->errorlogtag.get_string('extremovedsuspendnoroles', 'enrol_ldap',
  443. array('user_username'=> $row->username,
  444. 'course_shortname'=>$course_obj->shortname,
  445. 'course_id'=>$course_obj->id)));
  446. break;
  447. }
  448. }
  449. $transaction->allow_commit();
  450. }
  451. // Insert current enrolments
  452. // bad we can't do INSERT IGNORE with postgres...
  453. // Add necessary enrol instance if not present yet;
  454. $sql = "SELECT c.id, c.visible, e.id as enrolid
  455. FROM {course} c
  456. JOIN {enrol} e ON (e.courseid = c.id AND e.enrol = 'ldap')
  457. WHERE c.id = :courseid";
  458. $params = array('courseid'=>$course_obj->id);
  459. if (!($course_instance = $DB->get_record_sql($sql, $params, IGNORE_MULTIPLE))) {
  460. $course_instance = new stdClass();
  461. $course_instance->id = $course_obj->id;
  462. $course_instance->visible = $course_obj->visible;
  463. $course_instance->enrolid = $this->add_instance($course_instance);
  464. }
  465. if (!$instance = $DB->get_record('enrol', array('id'=>$course_instance->enrolid))) {
  466. continue; // Weird; skip this one.
  467. }
  468. if ($ignorehidden && !$course_instance->visible) {
  469. continue;
  470. }
  471. $transaction = $DB->start_delegated_transaction();
  472. foreach ($ldapmembers as $ldapmember) {
  473. $sql = 'SELECT id,username,1 FROM {user} WHERE idnumber = ? AND deleted = 0';
  474. $member = $DB->get_record_sql($sql, array($ldapmember));
  475. if(empty($member) || empty($member->id)){
  476. print_string ('couldnotfinduser', 'enrol_ldap', $ldapmember);
  477. continue;
  478. }
  479. $sql= "SELECT ue.status
  480. FROM {user_enrolments} ue
  481. JOIN {enrol} e ON (e.id = ue.enrolid)
  482. JOIN {role_assignments} ra ON (ra.itemid = e.id AND ra.component = 'enrol_ldap')
  483. WHERE e.courseid = :courseid AND ue.userid = :userid";
  484. $params = array('courseid'=>$course_obj->id, 'userid'=>$member->id);
  485. $userenrolment = $DB->get_record_sql($sql, $params);
  486. if(empty($userenrolment)) {
  487. $this->enrol_user($instance, $member->id, $role->id);
  488. // Make sure we set the enrolment status to active. If the user wasn't
  489. // previously enrolled to the course, enrol_user() sets it. But if we
  490. // configured the plugin to suspend the user enrolments _AND_ remove
  491. // the role assignments on external unenrol, then enrol_user() doesn't
  492. // set it back to active on external re-enrolment. So set it
  493. // unconditionnally to cover both cases.
  494. $DB->set_field('user_enrolments', 'status', ENROL_USER_ACTIVE, array('enrolid'=>$instance->id, 'userid'=>$member->id));
  495. error_log($this->errorlogtag.get_string('enroluser', 'enrol_ldap',
  496. array('user_username'=> $member->username,
  497. 'course_shortname'=>$course_obj->shortname,
  498. 'course_id'=>$course_obj->id)));
  499. } else {
  500. if ($userenrolment->status == ENROL_USER_SUSPENDED) {
  501. // Reenable enrolment that was previously disabled. Enrolment refreshed
  502. $DB->set_field('user_enrolments', 'status', ENROL_USER_ACTIVE, array('enrolid'=>$instance->id, 'userid'=>$member->id));
  503. error_log($this->errorlogtag.get_string('enroluserenable', 'enrol_ldap',
  504. array('user_username'=> $member->username,
  505. 'course_shortname'=>$course_obj->shortname,
  506. 'course_id'=>$course_obj->id)));
  507. }
  508. }
  509. }
  510. $transaction->allow_commit();
  511. }
  512. }
  513. }
  514. }
  515. @$this->ldap_close();
  516. }
  517. /**
  518. * Connect to the LDAP server, using the plugin configured
  519. * settings. It's actually a wrapper around ldap_connect_moodle()
  520. *
  521. * @return mixed A valid LDAP connection or false.
  522. */
  523. protected function ldap_connect() {
  524. global $CFG;
  525. require_once($CFG->libdir.'/ldaplib.php');
  526. // Cache ldap connections. They are expensive to set up
  527. // and can drain the TCP/IP ressources on the server if we
  528. // are syncing a lot of users (as we try to open a new connection
  529. // to get the user details). This is the least invasive way
  530. // to reuse existing connections without greater code surgery.
  531. if(!empty($this->ldapconnection)) {
  532. $this->ldapconns++;
  533. return $this->ldapconnection;
  534. }
  535. if ($ldapconnection = ldap_connect_moodle($this->get_config('host_url'), $this->get_config('ldap_version'),
  536. $this->get_config('user_type'), $this->get_config('bind_dn'),
  537. $this->get_config('bind_pw'), $this->get_config('opt_deref'),
  538. $debuginfo)) {
  539. $this->ldapconns = 1;
  540. $this->ldapconnection = $ldapconnection;
  541. return $ldapconnection;
  542. }
  543. // Log the problem, but don't show it to the user. She doesn't
  544. // even have a chance to see it, as we redirect instantly to
  545. // the user/front page.
  546. error_log($this->errorlogtag.$debuginfo);
  547. return false;
  548. }
  549. /**
  550. * Disconnects from a LDAP server
  551. *
  552. */
  553. protected function ldap_close() {
  554. $this->ldapconns--;
  555. if($this->ldapconns == 0) {
  556. @ldap_close($this->ldapconnection);
  557. unset($this->ldapconnection);
  558. }
  559. }
  560. /**
  561. * Return multidimensional array with details of user courses (at
  562. * least dn and idnumber).
  563. *
  564. * @param resource $ldapconnection a valid LDAP connection.
  565. * @param string $memberuid user idnumber (without magic quotes).
  566. * @param object role is a record from the mdl_role table.
  567. * @return array
  568. */
  569. protected function find_ext_enrolments ($ldapconnection, $memberuid, $role) {
  570. global $CFG;
  571. require_once($CFG->libdir.'/ldaplib.php');
  572. if (empty($memberuid)) {
  573. // No "idnumber" stored for this user, so no LDAP enrolments
  574. return array();
  575. }
  576. $ldap_contexts = trim($this->get_config('contexts_role'.$role->id));
  577. if (empty($ldap_contexts)) {
  578. // No role contexts, so no LDAP enrolments
  579. return array();
  580. }
  581. $textlib = textlib_get_instance();
  582. $extmemberuid = $textlib->convert($memberuid, 'utf-8', $this->get_config('ldapencoding'));
  583. if($this->get_config('memberattribute_isdn')) {
  584. if (!($extmemberuid = $this->ldap_find_userdn ($ldapconnection, $extmemberuid))) {
  585. return array();
  586. }
  587. }
  588. $ldap_search_pattern = '';
  589. if($this->get_config('nested_groups')) {
  590. $usergroups = $this->ldap_find_user_groups($ldapconnection, $extmemberuid);
  591. if(count($usergroups) > 0) {
  592. foreach ($usergroups as $group) {
  593. $ldap_search_pattern .= '('.$this->get_config('memberattribute_role'.$role->id).'='.$group.')';
  594. }
  595. }
  596. }
  597. // Default return value
  598. $courses = array();
  599. // Get all the fields we will want for the potential course creation
  600. // as they are light. don't get membership -- potentially a lot of data.
  601. $ldap_fields_wanted = array('dn', $this->get_config('course_idnumber'));
  602. $fullname = $this->get_config('course_fullname');
  603. $shortname = $this->get_config('course_shortname');
  604. $summary = $this->get_config('course_summary');
  605. if (isset($fullname)) {
  606. array_push($ldap_fields_wanted, $fullname);
  607. }
  608. if (isset($shortname)) {
  609. array_push($ldap_fields_wanted, $shortname);
  610. }
  611. if (isset($summary)) {
  612. array_push($ldap_fields_wanted, $summary);
  613. }
  614. // Define the search pattern
  615. if (empty($ldap_search_pattern)) {
  616. $ldap_search_pattern = '('.$this->get_config('memberattribute_role'.$role->id).'='.ldap_filter_addslashes($extmemberuid).')';
  617. } else {
  618. $ldap_search_pattern = '(|' . $ldap_search_pattern .
  619. '('.$this->get_config('memberattribute_role'.$role->id).'='.ldap_filter_addslashes($extmemberuid).')' .
  620. ')';
  621. }
  622. $ldap_search_pattern='(&'.$this->get_config('objectclass').$ldap_search_pattern.')';
  623. // Get all contexts and look for first matching user
  624. $ldap_contexts = explode(';', $ldap_contexts);
  625. foreach ($ldap_contexts as $context) {
  626. $context = trim($context);
  627. if (empty($context)) {
  628. continue;
  629. }
  630. if ($this->get_config('course_search_sub')) {
  631. // Use ldap_search to find first user from subtree
  632. $ldap_result = @ldap_search($ldapconnection,
  633. $context,
  634. $ldap_search_pattern,
  635. $ldap_fields_wanted);
  636. } else {
  637. // Search only in this context
  638. $ldap_result = @ldap_list($ldapconnection,
  639. $context,
  640. $ldap_search_pattern,
  641. $ldap_fields_wanted);
  642. }
  643. if (!$ldap_result) {
  644. continue;
  645. }
  646. // Check and push results. ldap_get_entries() already
  647. // lowercases the attribute index, so there's no need to
  648. // use array_change_key_case() later.
  649. $records = ldap_get_entries($ldapconnection, $ldap_result);
  650. // LDAP libraries return an odd array, really. Fix it.
  651. $flat_records = array();
  652. for ($c = 0; $c < $records['count']; $c++) {
  653. array_push($flat_records, $records[$c]);
  654. }
  655. unset($records);
  656. if (count($flat_records)) {
  657. $courses = array_merge($courses, $flat_records);
  658. }
  659. }
  660. return $courses;
  661. }
  662. /**
  663. * Search specified contexts for the specified userid and return the
  664. * user dn like: cn=username,ou=suborg,o=org. It's actually a wrapper
  665. * around ldap_find_userdn().
  666. *
  667. * @param resource $ldapconnection a valid LDAP connection
  668. * @param string $userid the userid to search for (in external LDAP encoding, no magic quotes).
  669. * @return mixed the user dn or false
  670. */
  671. protected function ldap_find_userdn($ldapconnection, $userid) {
  672. global $CFG;
  673. require_once($CFG->libdir.'/ldaplib.php');
  674. $ldap_contexts = explode(';', $this->get_config('user_contexts'));
  675. $ldap_defaults = ldap_getdefaults();
  676. return ldap_find_userdn($ldapconnection, $userid, $ldap_contexts,
  677. '(objectClass='.$ldap_defaults['objectclass'][$this->get_config('user_type')].')',
  678. $this->get_config('idnumber_attribute'), $this->get_config('user_search_sub'));
  679. }
  680. /**
  681. * Find the groups a given distinguished name belongs to, both directly
  682. * and indirectly via nested groups membership.
  683. *
  684. * @param resource $ldapconnection a valid LDAP connection
  685. * @param string $memberdn distinguished name to search
  686. * @return array with member groups' distinguished names (can be emtpy)
  687. */
  688. protected function ldap_find_user_groups($ldapconnection, $memberdn) {
  689. $groups = array();
  690. $this->ldap_find_user_groups_recursively($ldapconnection, $memberdn, $groups);
  691. return $groups;
  692. }
  693. /**
  694. * Recursively process the groups the given member distinguished name
  695. * belongs to, adding them to the already processed groups array.
  696. *
  697. * @param resource $ldapconnection
  698. * @param string $memberdn distinguished name to search
  699. * @param array reference &$membergroups array with already found
  700. * groups, where we'll put the newly found
  701. * groups.
  702. */
  703. protected function ldap_find_user_groups_recursively($ldapconnection, $memberdn, &$membergroups) {
  704. $result = @ldap_read ($ldapconnection, $memberdn, '(objectClass=*)', array($this->get_config('group_memberofattribute')));
  705. if (!$result) {
  706. return;
  707. }
  708. if ($entry = ldap_first_entry($ldapconnection, $result)) {
  709. do {
  710. $attributes = ldap_get_attributes($ldapconnection, $entry);
  711. for ($j = 0; $j < $attributes['count']; $j++) {
  712. $groups = ldap_get_values_len($ldapconnection, $entry, $attributes[$j]);
  713. foreach ($groups as $key => $group) {
  714. if ($key === 'count') { // Skip the entries count
  715. continue;
  716. }
  717. if(!in_array($group, $membergroups)) {
  718. // Only push and recurse if we haven't 'seen' this group before
  719. // to prevent loops (MS Active Directory allows them!!).
  720. array_push($membergroups, $group);
  721. $this->ldap_find_user_groups_recursively($ldapconnection, $group, $membergroups);
  722. }
  723. }
  724. }
  725. }
  726. while ($entry = ldap_next_entry($ldapconnection, $entry));
  727. }
  728. }
  729. /**
  730. * Given a group name (either a RDN or a DN), get the list of users
  731. * belonging to that group. If the group has nested groups, expand all
  732. * the intermediate groups and return the full list of users that
  733. * directly or indirectly belong to the group.
  734. *
  735. * @param resource $ldapconnection a valid LDAP connection
  736. * @param string $group the group name to search
  737. * @param string $memberattibute the attribute that holds the members of the group
  738. * @return array the list of users belonging to the group. If $group
  739. * is not actually a group, returns array($group).
  740. */
  741. protected function ldap_explode_group($ldapconnection, $group, $memberattribute) {
  742. switch ($this->get_config('user_type')) {
  743. case 'ad':
  744. // $group is already the distinguished name to search.
  745. $dn = $group;
  746. $result = ldap_read($ldapconnection, $dn, '(objectClass=*)', array('objectClass'));
  747. $entry = ldap_first_entry($ldapconnection, $result);
  748. $objectclass = ldap_get_values($ldapconnection, $entry, 'objectClass');
  749. if (!in_array('group', $objectclass)) {
  750. // Not a group, so return immediately.
  751. return array($group);
  752. }
  753. $result = ldap_read($ldapconnection, $dn, '(objectClass=*)', array($memberattribute));
  754. $entry = ldap_first_entry($ldapconnection, $result);
  755. $members = @ldap_get_values($ldapconnection, $entry, $memberattribute); // Can be empty and throws a warning
  756. if ($members['count'] == 0) {
  757. // There are no members in this group, return nothing.
  758. return array();
  759. }
  760. unset($members['count']);
  761. $users = array();
  762. foreach ($members as $member) {
  763. $group_members = $this->ldap_explode_group($ldapconnection, $member, $memberattribute);
  764. $users = array_merge($users, $group_members);
  765. }
  766. return ($users);
  767. break;
  768. default:
  769. error_log($this->errorlogtag.get_string('explodegroupusertypenotsupported', 'enrol_ldap',
  770. $this->get_config('user_type_name')));
  771. return array($group);
  772. }
  773. }
  774. /**
  775. * Will create the moodle course from the template
  776. * course_ext is an array as obtained from ldap -- flattened somewhat
  777. * NOTE: if you pass true for $skip_fix_course_sortorder
  778. * you will want to call fix_course_sortorder() after your are done
  779. * with course creation.
  780. *
  781. * @param array $course_ext
  782. * @param boolean $skip_fix_course_sortorder
  783. * @return mixed false on error, id for the newly created course otherwise.
  784. */
  785. function create_course($course_ext, $skip_fix_course_sortorder=false) {
  786. global $CFG, $DB;
  787. require_once("$CFG->dirroot/course/lib.php");
  788. // Override defaults with template course
  789. $template = false;
  790. if ($this->get_config('template')) {
  791. if ($template = $DB->get_record('course', array('shortname'=>$this->get_config('template')))) {
  792. unset($template->id); // So we are clear to reinsert the record
  793. unset($template->fullname);
  794. unset($template->shortname);
  795. unset($template->idnumber);
  796. }
  797. }
  798. if (!$template) {
  799. $courseconfig = get_config('moodlecourse');
  800. $template = new stdClass();
  801. $template->summary = '';
  802. $template->summaryformat = FORMAT_HTML;
  803. $template->format = $courseconfig->format;
  804. $template->numsections = $courseconfig->numsections;
  805. $template->hiddensections = $courseconfig->hiddensections;
  806. $template->newsitems = $courseconfig->newsitems;
  807. $template->showgrades = $courseconfig->showgrades;
  808. $template->showreports = $courseconfig->showreports;
  809. $template->maxbytes = $courseconfig->maxbytes;
  810. $template->groupmode = $courseconfig->groupmode;
  811. $template->groupmodeforce = $courseconfig->groupmodeforce;
  812. $template->visible = $courseconfig->visible;
  813. $template->lang = $courseconfig->lang;
  814. $template->groupmodeforce = $courseconfig->groupmodeforce;
  815. }
  816. $course = $template;
  817. $course->category = $this->get_config('category');
  818. if (!$DB->record_exists('course_categories', array('id'=>$this->get_config('category')))) {
  819. $categories = $DB->get_records('course_categories', array(), 'sortorder', 'id', 0, 1);
  820. $first = reset($categories);
  821. $course->category = $first->id;
  822. }
  823. // Override with required ext data
  824. $course->idnumber = $course_ext[$this->get_config('course_idnumber')][0];
  825. $course->fullname = $course_ext[$this->get_config('course_fullname')][0];
  826. $course->shortname = $course_ext[$this->get_config('course_shortname')][0];
  827. if (empty($course->idnumber) || empty($course->fullname) || empty($course->shortname)) {
  828. // We are in trouble!
  829. error_log($this->errorlogtag.get_string('cannotcreatecourse', 'enrol_ldap'));
  830. error_log($this->errorlogtag.var_export($course, true));
  831. return false;
  832. }
  833. $summary = $this->get_config('course_summary');
  834. if (!isset($summary) || empty($course_ext[$summary][0])) {
  835. $course->summary = '';
  836. } else {
  837. $course->summary = $course_ext[$this->get_config('course_summary')][0];
  838. }
  839. $newcourse = create_course($course);
  840. return $newcourse->id;
  841. }
  842. }