Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 4.2.x will end 22 April 2024 (12 months).
  • Bug fixes for security issues in 4.2.x will end 7 October 2024 (18 months).
  • PHP version: minimum PHP 8.0.0 Note: minimum PHP version has increased since Moodle 4.1. PHP 8.1.x is supported too.
/enrol/ldap/ -> lib.php (source)

Differences Between: [Versions 310 and 402] [Versions 311 and 402] [Versions 39 and 402] [Versions 400 and 402] [Versions 401 and 402]

   1  <?php
   2  // This file is part of Moodle - http://moodle.org/
   3  //
   4  // Moodle is free software: you can redistribute it and/or modify
   5  // it under the terms of the GNU General Public License as published by
   6  // the Free Software Foundation, either version 3 of the License, or
   7  // (at your option) any later version.
   8  //
   9  // Moodle is distributed in the hope that it will be useful,
  10  // but WITHOUT ANY WARRANTY; without even the implied warranty of
  11  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  12  // GNU General Public License for more details.
  13  //
  14  // You should have received a copy of the GNU General Public License
  15  // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
  16  
  17  /**
  18   * LDAP enrolment plugin implementation.
  19   *
  20   * This plugin synchronises enrolment and roles with a LDAP server.
  21   *
  22   * @package    enrol_ldap
  23   * @author     Iñaki Arenaza - based on code by Martin Dougiamas, Martin Langhoff and others
  24   * @copyright  1999 onwards Martin Dougiamas {@link http://moodle.com}
  25   * @copyright  2010 Iñaki Arenaza <iarenaza@eps.mondragon.edu>
  26   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  27   */
  28  
  29  defined('MOODLE_INTERNAL') || die();
  30  
  31  class enrol_ldap_plugin extends enrol_plugin {
  32      protected $enrol_localcoursefield = 'idnumber';
  33      protected $enroltype = 'enrol_ldap';
  34      protected $errorlogtag = '[ENROL LDAP] ';
  35  
  36      /**
  37       * The object class to use when finding users.
  38       *
  39       * @var string $userobjectclass
  40       */
  41      protected $userobjectclass;
  42  
  43      /** @var LDAP\Connection LDAP connection. */
  44      protected $ldapconnection;
  45  
  46      /**
  47       * Constructor for the plugin. In addition to calling the parent
  48       * constructor, we define and 'fix' some settings depending on the
  49       * real settings the admin defined.
  50       */
  51      public function __construct() {
  52          global $CFG;
  53          require_once($CFG->libdir.'/ldaplib.php');
  54  
  55          // Do our own stuff to fix the config (it's easier to do it
  56          // here than using the admin settings infrastructure). We
  57          // don't call $this->set_config() for any of the 'fixups'
  58          // (except the objectclass, as it's critical) because the user
  59          // didn't specify any values and relied on the default values
  60          // defined for the user type she chose.
  61          $this->load_config();
  62  
  63          // Make sure we get sane defaults for critical values.
  64          $this->config->ldapencoding = $this->get_config('ldapencoding', 'utf-8');
  65          $this->config->user_type = $this->get_config('user_type', 'default');
  66  
  67          $ldap_usertypes = ldap_supported_usertypes();
  68          $this->config->user_type_name = $ldap_usertypes[$this->config->user_type];
  69          unset($ldap_usertypes);
  70  
  71          $default = ldap_getdefaults();
  72  
  73          // The objectclass in the defaults is for a user.
  74          // This will be required later, but enrol_ldap uses 'objectclass' for its group objectclass.
  75          // Save the normalised user objectclass for later.
  76          $this->userobjectclass = ldap_normalise_objectclass($default['objectclass'][$this->get_config('user_type')]);
  77  
  78          // Remove the objectclass default, as the values specified there are for users, and we are dealing with groups here.
  79          unset($default['objectclass']);
  80  
  81          // Use defaults if values not given. Dont use this->get_config()
  82          // here to be able to check for 0 and false values too.
  83          foreach ($default as $key => $value) {
  84              // Watch out - 0, false are correct values too, so we can't use $this->get_config()
  85              if (!isset($this->config->{$key}) or $this->config->{$key} == '') {
  86                  $this->config->{$key} = $value[$this->config->user_type];
  87              }
  88          }
  89  
  90          // Normalise the objectclass used for groups.
  91          if (empty($this->config->objectclass)) {
  92              // No objectclass set yet - set a default class.
  93              $this->config->objectclass = ldap_normalise_objectclass(null, '*');
  94              $this->set_config('objectclass', $this->config->objectclass);
  95          } else {
  96              $objectclass = ldap_normalise_objectclass($this->config->objectclass);
  97              if ($objectclass !== $this->config->objectclass) {
  98                  // The objectclass was changed during normalisation.
  99                  // Save it in config, and update the local copy of config.
 100                  $this->set_config('objectclass', $objectclass);
 101                  $this->config->objectclass = $objectclass;
 102              }
 103          }
 104      }
 105  
 106      /**
 107       * Is it possible to delete enrol instance via standard UI?
 108       *
 109       * @param object $instance
 110       * @return bool
 111       */
 112      public function can_delete_instance($instance) {
 113          $context = context_course::instance($instance->courseid);
 114          if (!has_capability('enrol/ldap:manage', $context)) {
 115              return false;
 116          }
 117  
 118          if (!enrol_is_enabled('ldap')) {
 119              return true;
 120          }
 121  
 122          if (!$this->get_config('ldap_host') or !$this->get_config('objectclass') or !$this->get_config('course_idnumber')) {
 123              return true;
 124          }
 125  
 126          // TODO: connect to external system and make sure no users are to be enrolled in this course
 127          return false;
 128      }
 129  
 130      /**
 131       * Is it possible to hide/show enrol instance via standard UI?
 132       *
 133       * @param stdClass $instance
 134       * @return bool
 135       */
 136      public function can_hide_show_instance($instance) {
 137          $context = context_course::instance($instance->courseid);
 138          return has_capability('enrol/ldap:manage', $context);
 139      }
 140  
 141      /**
 142       * Forces synchronisation of user enrolments with LDAP server.
 143       * It creates courses if the plugin is configured to do so.
 144       *
 145       * @param object $user user record
 146       * @return void
 147       */
 148      public function sync_user_enrolments($user) {
 149          global $DB;
 150  
 151          // Do not try to print anything to the output because this method is called during interactive login.
 152          if (PHPUNIT_TEST) {
 153              $trace = new null_progress_trace();
 154          } else {
 155              $trace = new error_log_progress_trace($this->errorlogtag);
 156          }
 157  
 158          if (!$this->ldap_connect($trace)) {
 159              $trace->finished();
 160              return;
 161          }
 162  
 163          if (!is_object($user) or !property_exists($user, 'id')) {
 164              throw new coding_exception('Invalid $user parameter in sync_user_enrolments()');
 165          }
 166  
 167          if (!property_exists($user, 'idnumber')) {
 168              debugging('Invalid $user parameter in sync_user_enrolments(), missing idnumber');
 169              $user = $DB->get_record('user', array('id'=>$user->id));
 170          }
 171  
 172          // We may need a lot of memory here
 173          core_php_time_limit::raise();
 174          raise_memory_limit(MEMORY_HUGE);
 175  
 176          // Get enrolments for each type of role.
 177          $roles = get_all_roles();
 178          $enrolments = array();
 179          foreach($roles as $role) {
 180              // Get external enrolments according to LDAP server
 181              $enrolments[$role->id]['ext'] = $this->find_ext_enrolments($user->idnumber, $role);
 182  
 183              // Get the list of current user enrolments that come from LDAP
 184              $sql= "SELECT e.courseid, ue.status, e.id as enrolid, c.shortname
 185                       FROM {user} u
 186                       JOIN {role_assignments} ra ON (ra.userid = u.id AND ra.component = 'enrol_ldap' AND ra.roleid = :roleid)
 187                       JOIN {user_enrolments} ue ON (ue.userid = u.id AND ue.enrolid = ra.itemid)
 188                       JOIN {enrol} e ON (e.id = ue.enrolid)
 189                       JOIN {course} c ON (c.id = e.courseid)
 190                      WHERE u.deleted = 0 AND u.id = :userid";
 191              $params = array ('roleid'=>$role->id, 'userid'=>$user->id);
 192              $enrolments[$role->id]['current'] = $DB->get_records_sql($sql, $params);
 193          }
 194  
 195          $ignorehidden = $this->get_config('ignorehiddencourses');
 196          $courseidnumber = $this->get_config('course_idnumber');
 197          foreach($roles as $role) {
 198              foreach ($enrolments[$role->id]['ext'] as $enrol) {
 199                  $course_ext_id = $enrol[$courseidnumber][0];
 200                  if (empty($course_ext_id)) {
 201                      $trace->output(get_string('extcourseidinvalid', 'enrol_ldap'));
 202                      continue; // Next; skip this one!
 203                  }
 204  
 205                  // Create the course if required
 206                  $course = $DB->get_record('course', array($this->enrol_localcoursefield=>$course_ext_id));
 207                  if (empty($course)) { // Course doesn't exist
 208                      if ($this->get_config('autocreate')) { // Autocreate
 209                          $trace->output(get_string('createcourseextid', 'enrol_ldap', array('courseextid'=>$course_ext_id)));
 210                          if (!$newcourseid = $this->create_course($enrol, $trace)) {
 211                              continue;
 212                          }
 213                          $course = $DB->get_record('course', array('id'=>$newcourseid));
 214                      } else {
 215                          $trace->output(get_string('createnotcourseextid', 'enrol_ldap', array('courseextid'=>$course_ext_id)));
 216                          continue; // Next; skip this one!
 217                      }
 218                  }
 219  
 220                  // Deal with enrolment in the moodle db
 221                  // Add necessary enrol instance if not present yet;
 222                  $sql = "SELECT c.id, c.visible, e.id as enrolid
 223                            FROM {course} c
 224                            JOIN {enrol} e ON (e.courseid = c.id AND e.enrol = 'ldap')
 225                           WHERE c.id = :courseid";
 226                  $params = array('courseid'=>$course->id);
 227                  if (!($course_instance = $DB->get_record_sql($sql, $params, IGNORE_MULTIPLE))) {
 228                      $course_instance = new stdClass();
 229                      $course_instance->id = $course->id;
 230                      $course_instance->visible = $course->visible;
 231                      $course_instance->enrolid = $this->add_instance($course_instance);
 232                  }
 233  
 234                  if (!$instance = $DB->get_record('enrol', array('id'=>$course_instance->enrolid))) {
 235                      continue; // Weird; skip this one.
 236                  }
 237  
 238                  if ($ignorehidden && !$course_instance->visible) {
 239                      continue;
 240                  }
 241  
 242                  if (empty($enrolments[$role->id]['current'][$course->id])) {
 243                      // Enrol the user in the given course, with that role.
 244                      $this->enrol_user($instance, $user->id, $role->id);
 245                      // Make sure we set the enrolment status to active. If the user wasn't
 246                      // previously enrolled to the course, enrol_user() sets it. But if we
 247                      // configured the plugin to suspend the user enrolments _AND_ remove
 248                      // the role assignments on external unenrol, then enrol_user() doesn't
 249                      // set it back to active on external re-enrolment. So set it
 250                      // unconditionnally to cover both cases.
 251                      $DB->set_field('user_enrolments', 'status', ENROL_USER_ACTIVE, array('enrolid'=>$instance->id, 'userid'=>$user->id));
 252                      $trace->output(get_string('enroluser', 'enrol_ldap',
 253                          array('user_username'=> $user->username,
 254                                'course_shortname'=>$course->shortname,
 255                                'course_id'=>$course->id)));
 256                  } else {
 257                      if ($enrolments[$role->id]['current'][$course->id]->status == ENROL_USER_SUSPENDED) {
 258                          // Reenable enrolment that was previously disabled. Enrolment refreshed
 259                          $DB->set_field('user_enrolments', 'status', ENROL_USER_ACTIVE, array('enrolid'=>$instance->id, 'userid'=>$user->id));
 260                          $trace->output(get_string('enroluserenable', 'enrol_ldap',
 261                              array('user_username'=> $user->username,
 262                                    'course_shortname'=>$course->shortname,
 263                                    'course_id'=>$course->id)));
 264                      }
 265                  }
 266  
 267                  // Remove this course from the current courses, to be able to detect
 268                  // which current courses should be unenroled from when we finish processing
 269                  // external enrolments.
 270                  unset($enrolments[$role->id]['current'][$course->id]);
 271              }
 272  
 273              // Deal with unenrolments.
 274              $transaction = $DB->start_delegated_transaction();
 275              foreach ($enrolments[$role->id]['current'] as $course) {
 276                  $context = context_course::instance($course->courseid);
 277                  $instance = $DB->get_record('enrol', array('id'=>$course->enrolid));
 278                  switch ($this->get_config('unenrolaction')) {
 279                      case ENROL_EXT_REMOVED_UNENROL:
 280                          $this->unenrol_user($instance, $user->id);
 281                          $trace->output(get_string('extremovedunenrol', 'enrol_ldap',
 282                              array('user_username'=> $user->username,
 283                                    'course_shortname'=>$course->shortname,
 284                                    'course_id'=>$course->courseid)));
 285                          break;
 286                      case ENROL_EXT_REMOVED_KEEP:
 287                          // Keep - only adding enrolments
 288                          break;
 289                      case ENROL_EXT_REMOVED_SUSPEND:
 290                          if ($course->status != ENROL_USER_SUSPENDED) {
 291                              $DB->set_field('user_enrolments', 'status', ENROL_USER_SUSPENDED, array('enrolid'=>$instance->id, 'userid'=>$user->id));
 292                              $trace->output(get_string('extremovedsuspend', 'enrol_ldap',
 293                                  array('user_username'=> $user->username,
 294                                        'course_shortname'=>$course->shortname,
 295                                        'course_id'=>$course->courseid)));
 296                          }
 297                          break;
 298                      case ENROL_EXT_REMOVED_SUSPENDNOROLES:
 299                          if ($course->status != ENROL_USER_SUSPENDED) {
 300                              $DB->set_field('user_enrolments', 'status', ENROL_USER_SUSPENDED, array('enrolid'=>$instance->id, 'userid'=>$user->id));
 301                          }
 302                          role_unassign_all(array('contextid'=>$context->id, 'userid'=>$user->id, 'component'=>'enrol_ldap', 'itemid'=>$instance->id));
 303                          $trace->output(get_string('extremovedsuspendnoroles', 'enrol_ldap',
 304                              array('user_username'=> $user->username,
 305                                    'course_shortname'=>$course->shortname,
 306                                    'course_id'=>$course->courseid)));
 307                          break;
 308                  }
 309              }
 310              $transaction->allow_commit();
 311          }
 312  
 313          $this->ldap_close();
 314  
 315          $trace->finished();
 316      }
 317  
 318      /**
 319       * Forces synchronisation of all enrolments with LDAP server.
 320       * It creates courses if the plugin is configured to do so.
 321       *
 322       * @param progress_trace $trace
 323       * @param int|null $onecourse limit sync to one course->id, null if all courses
 324       * @return void
 325       */
 326      public function sync_enrolments(progress_trace $trace, $onecourse = null) {
 327          global $CFG, $DB;
 328  
 329          if (!$this->ldap_connect($trace)) {
 330              $trace->finished();
 331              return;
 332          }
 333  
 334          $ldap_pagedresults = ldap_paged_results_supported($this->get_config('ldap_version'), $this->ldapconnection);
 335  
 336          // we may need a lot of memory here
 337          core_php_time_limit::raise();
 338          raise_memory_limit(MEMORY_HUGE);
 339  
 340          $oneidnumber = null;
 341          if ($onecourse) {
 342              if (!$course = $DB->get_record('course', array('id'=>$onecourse), 'id,'.$this->enrol_localcoursefield)) {
 343                  // Course does not exist, nothing to do.
 344                  $trace->output("Requested course $onecourse does not exist, no sync performed.");
 345                  $trace->finished();
 346                  return;
 347              }
 348              if (empty($course->{$this->enrol_localcoursefield})) {
 349                  $trace->output("Requested course $onecourse does not have {$this->enrol_localcoursefield}, no sync performed.");
 350                  $trace->finished();
 351                  return;
 352              }
 353              $oneidnumber = ldap_filter_addslashes(core_text::convert($course->idnumber, 'utf-8', $this->get_config('ldapencoding')));
 354          }
 355  
 356          // Get enrolments for each type of role.
 357          $roles = get_all_roles();
 358          $enrolments = array();
 359          foreach($roles as $role) {
 360              // Get all contexts
 361              $ldap_contexts = explode(';', $this->config->{'contexts_role'.$role->id});
 362  
 363              // Get all the fields we will want for the potential course creation
 364              // as they are light. Don't get membership -- potentially a lot of data.
 365              $ldap_fields_wanted = array('dn', $this->config->course_idnumber);
 366              if (!empty($this->config->course_fullname)) {
 367                  array_push($ldap_fields_wanted, $this->config->course_fullname);
 368              }
 369              if (!empty($this->config->course_shortname)) {
 370                  array_push($ldap_fields_wanted, $this->config->course_shortname);
 371              }
 372              if (!empty($this->config->course_summary)) {
 373                  array_push($ldap_fields_wanted, $this->config->course_summary);
 374              }
 375              array_push($ldap_fields_wanted, $this->config->{'memberattribute_role'.$role->id});
 376  
 377              // Define the search pattern
 378              $ldap_search_pattern = $this->config->objectclass;
 379  
 380              if ($oneidnumber !== null) {
 381                  $ldap_search_pattern = "(&$ldap_search_pattern({$this->config->course_idnumber}=$oneidnumber))";
 382              }
 383  
 384              $ldap_cookie = '';
 385              $servercontrols = array();
 386              foreach ($ldap_contexts as $ldap_context) {
 387                  $ldap_context = trim($ldap_context);
 388                  if (empty($ldap_context)) {
 389                      continue; // Next;
 390                  }
 391  
 392                  $flat_records = array();
 393                  do {
 394                      if ($ldap_pagedresults) {
 395                          $servercontrols = array(array(
 396                              'oid' => LDAP_CONTROL_PAGEDRESULTS, 'value' => array(
 397                                  'size' => $this->config->pagesize, 'cookie' => $ldap_cookie)));
 398                      }
 399  
 400                      if ($this->config->course_search_sub) {
 401                          // Use ldap_search to find first user from subtree
 402                          $ldap_result = @ldap_search($this->ldapconnection, $ldap_context,
 403                              $ldap_search_pattern, $ldap_fields_wanted,
 404                              0, -1, -1, LDAP_DEREF_NEVER, $servercontrols);
 405                      } else {
 406                          // Search only in this context
 407                          $ldap_result = @ldap_list($this->ldapconnection, $ldap_context,
 408                              $ldap_search_pattern, $ldap_fields_wanted,
 409                              0, -1, -1, LDAP_DEREF_NEVER, $servercontrols);
 410                      }
 411                      if (!$ldap_result) {
 412                          continue; // Next
 413                      }
 414  
 415                      if ($ldap_pagedresults) {
 416                          // Get next server cookie to know if we'll need to continue searching.
 417                          $ldap_cookie = '';
 418                          // Get next cookie from controls.
 419                          ldap_parse_result($this->ldapconnection, $ldap_result, $errcode, $matcheddn,
 420                              $errmsg, $referrals, $controls);
 421                          if (isset($controls[LDAP_CONTROL_PAGEDRESULTS]['value']['cookie'])) {
 422                              $ldap_cookie = $controls[LDAP_CONTROL_PAGEDRESULTS]['value']['cookie'];
 423                          }
 424                      }
 425  
 426                      // Check and push results
 427                      $records = ldap_get_entries($this->ldapconnection, $ldap_result);
 428  
 429                      // LDAP libraries return an odd array, really. fix it:
 430                      for ($c = 0; $c < $records['count']; $c++) {
 431                          array_push($flat_records, $records[$c]);
 432                      }
 433                      // Free some mem
 434                      unset($records);
 435                  } while ($ldap_pagedresults && !empty($ldap_cookie));
 436  
 437                  // If LDAP paged results were used, the current connection must be completely
 438                  // closed and a new one created, to work without paged results from here on.
 439                  if ($ldap_pagedresults) {
 440                      $this->ldap_close();
 441                      $this->ldap_connect($trace);
 442                  }
 443  
 444                  if (count($flat_records)) {
 445                      $ignorehidden = $this->get_config('ignorehiddencourses');
 446                      foreach($flat_records as $course) {
 447                          $course = array_change_key_case($course, CASE_LOWER);
 448                          $idnumber = $course[$this->config->course_idnumber][0];
 449                          $trace->output(get_string('synccourserole', 'enrol_ldap', array('idnumber'=>$idnumber, 'role_shortname'=>$role->shortname)));
 450  
 451                          // Does the course exist in moodle already?
 452                          $course_obj = $DB->get_record('course', array($this->enrol_localcoursefield=>$idnumber));
 453                          if (empty($course_obj)) { // Course doesn't exist
 454                              if ($this->get_config('autocreate')) { // Autocreate
 455                                  $trace->output(get_string('createcourseextid', 'enrol_ldap', array('courseextid'=>$idnumber)));
 456                                  if (!$newcourseid = $this->create_course($course, $trace)) {
 457                                      continue;
 458                                  }
 459                                  $course_obj = $DB->get_record('course', array('id'=>$newcourseid));
 460                              } else {
 461                                  $trace->output(get_string('createnotcourseextid', 'enrol_ldap', array('courseextid'=>$idnumber)));
 462                                  continue; // Next; skip this one!
 463                              }
 464                          } else {  // Check if course needs update & update as needed.
 465                              $this->update_course($course_obj, $course, $trace);
 466                          }
 467  
 468                          // Enrol & unenrol
 469  
 470                          // Pull the ldap membership into a nice array
 471                          // this is an odd array -- mix of hash and array --
 472                          $ldapmembers = array();
 473  
 474                          if (property_exists($this->config, 'memberattribute_role'.$role->id)
 475                              && !empty($this->config->{'memberattribute_role'.$role->id})
 476                              && !empty($course[$this->config->{'memberattribute_role'.$role->id}])) { // May have no membership!
 477  
 478                              $ldapmembers = $course[$this->config->{'memberattribute_role'.$role->id}];
 479                              unset($ldapmembers['count']); // Remove oddity ;)
 480  
 481                              // If we have enabled nested groups, we need to expand
 482                              // the groups to get the real user list. We need to do
 483                              // this before dealing with 'memberattribute_isdn'.
 484                              if ($this->config->nested_groups) {
 485                                  $users = array();
 486                                  foreach ($ldapmembers as $ldapmember) {
 487                                      $grpusers = $this->ldap_explode_group($ldapmember,
 488                                                                            $this->config->{'memberattribute_role'.$role->id});
 489  
 490                                      $users = array_merge($users, $grpusers);
 491                                  }
 492                                  $ldapmembers = array_unique($users); // There might be duplicates.
 493                              }
 494  
 495                              // Deal with the case where the member attribute holds distinguished names,
 496                              // but only if the user attribute is not a distinguished name itself.
 497                              if ($this->config->memberattribute_isdn
 498                                  && ($this->config->idnumber_attribute !== 'dn')
 499                                  && ($this->config->idnumber_attribute !== 'distinguishedname')) {
 500                                  // We need to retrieve the idnumber for all the users in $ldapmembers,
 501                                  // as the idnumber does not match their dn and we get dn's from membership.
 502                                  $memberidnumbers = array();
 503                                  foreach ($ldapmembers as $ldapmember) {
 504                                      $result = ldap_read($this->ldapconnection, $ldapmember, $this->userobjectclass,
 505                                                          array($this->config->idnumber_attribute));
 506                                      $entry = ldap_first_entry($this->ldapconnection, $result);
 507                                      $values = ldap_get_values($this->ldapconnection, $entry, $this->config->idnumber_attribute);
 508                                      array_push($memberidnumbers, $values[0]);
 509                                  }
 510  
 511                                  $ldapmembers = $memberidnumbers;
 512                              }
 513                          }
 514  
 515                          // Prune old ldap enrolments
 516                          // hopefully they'll fit in the max buffer size for the RDBMS
 517                          $sql= "SELECT u.id as userid, u.username, ue.status,
 518                                        ra.contextid, ra.itemid as instanceid
 519                                   FROM {user} u
 520                                   JOIN {role_assignments} ra ON (ra.userid = u.id AND ra.component = 'enrol_ldap' AND ra.roleid = :roleid)
 521                                   JOIN {user_enrolments} ue ON (ue.userid = u.id AND ue.enrolid = ra.itemid)
 522                                   JOIN {enrol} e ON (e.id = ue.enrolid)
 523                                  WHERE u.deleted = 0 AND e.courseid = :courseid ";
 524                          $params = array('roleid'=>$role->id, 'courseid'=>$course_obj->id);
 525                          $context = context_course::instance($course_obj->id);
 526                          if (!empty($ldapmembers)) {
 527                              list($ldapml, $params2) = $DB->get_in_or_equal($ldapmembers, SQL_PARAMS_NAMED, 'm', false);
 528                              $sql .= "AND u.idnumber $ldapml";
 529                              $params = array_merge($params, $params2);
 530                              unset($params2);
 531                          } else {
 532                              $shortname = format_string($course_obj->shortname, true, array('context' => $context));
 533                              $trace->output(get_string('emptyenrolment', 'enrol_ldap',
 534                                           array('role_shortname'=> $role->shortname,
 535                                                 'course_shortname' => $shortname)));
 536                          }
 537                          $todelete = $DB->get_records_sql($sql, $params);
 538  
 539                          if (!empty($todelete)) {
 540                              $transaction = $DB->start_delegated_transaction();
 541                              foreach ($todelete as $row) {
 542                                  $instance = $DB->get_record('enrol', array('id'=>$row->instanceid));
 543                                  switch ($this->get_config('unenrolaction')) {
 544                                  case ENROL_EXT_REMOVED_UNENROL:
 545                                      $this->unenrol_user($instance, $row->userid);
 546                                      $trace->output(get_string('extremovedunenrol', 'enrol_ldap',
 547                                          array('user_username'=> $row->username,
 548                                                'course_shortname'=>$course_obj->shortname,
 549                                                'course_id'=>$course_obj->id)));
 550                                      break;
 551                                  case ENROL_EXT_REMOVED_KEEP:
 552                                      // Keep - only adding enrolments
 553                                      break;
 554                                  case ENROL_EXT_REMOVED_SUSPEND:
 555                                      if ($row->status != ENROL_USER_SUSPENDED) {
 556                                          $DB->set_field('user_enrolments', 'status', ENROL_USER_SUSPENDED, array('enrolid'=>$instance->id, 'userid'=>$row->userid));
 557                                          $trace->output(get_string('extremovedsuspend', 'enrol_ldap',
 558                                              array('user_username'=> $row->username,
 559                                                    'course_shortname'=>$course_obj->shortname,
 560                                                    'course_id'=>$course_obj->id)));
 561                                      }
 562                                      break;
 563                                  case ENROL_EXT_REMOVED_SUSPENDNOROLES:
 564                                      if ($row->status != ENROL_USER_SUSPENDED) {
 565                                          $DB->set_field('user_enrolments', 'status', ENROL_USER_SUSPENDED, array('enrolid'=>$instance->id, 'userid'=>$row->userid));
 566                                      }
 567                                      role_unassign_all(array('contextid'=>$row->contextid, 'userid'=>$row->userid, 'component'=>'enrol_ldap', 'itemid'=>$instance->id));
 568                                      $trace->output(get_string('extremovedsuspendnoroles', 'enrol_ldap',
 569                                          array('user_username'=> $row->username,
 570                                                'course_shortname'=>$course_obj->shortname,
 571                                                'course_id'=>$course_obj->id)));
 572                                      break;
 573                                  }
 574                              }
 575                              $transaction->allow_commit();
 576                          }
 577  
 578                          // Insert current enrolments
 579                          // bad we can't do INSERT IGNORE with postgres...
 580  
 581                          // Add necessary enrol instance if not present yet;
 582                          $sql = "SELECT c.id, c.visible, e.id as enrolid
 583                                    FROM {course} c
 584                                    JOIN {enrol} e ON (e.courseid = c.id AND e.enrol = 'ldap')
 585                                   WHERE c.id = :courseid";
 586                          $params = array('courseid'=>$course_obj->id);
 587                          if (!($course_instance = $DB->get_record_sql($sql, $params, IGNORE_MULTIPLE))) {
 588                              $course_instance = new stdClass();
 589                              $course_instance->id = $course_obj->id;
 590                              $course_instance->visible = $course_obj->visible;
 591                              $course_instance->enrolid = $this->add_instance($course_instance);
 592                          }
 593  
 594                          if (!$instance = $DB->get_record('enrol', array('id'=>$course_instance->enrolid))) {
 595                              continue; // Weird; skip this one.
 596                          }
 597  
 598                          if ($ignorehidden && !$course_instance->visible) {
 599                              continue;
 600                          }
 601  
 602                          $transaction = $DB->start_delegated_transaction();
 603                          foreach ($ldapmembers as $ldapmember) {
 604                              $sql = 'SELECT id,username,1 FROM {user} WHERE idnumber = ? AND deleted = 0';
 605                              $member = $DB->get_record_sql($sql, array($ldapmember));
 606                              if(empty($member) || empty($member->id)){
 607                                  $trace->output(get_string('couldnotfinduser', 'enrol_ldap', $ldapmember));
 608                                  continue;
 609                              }
 610  
 611                              $sql= "SELECT ue.status
 612                                       FROM {user_enrolments} ue
 613                                       JOIN {enrol} e ON (e.id = ue.enrolid AND e.enrol = 'ldap')
 614                                      WHERE e.courseid = :courseid AND ue.userid = :userid";
 615                              $params = array('courseid'=>$course_obj->id, 'userid'=>$member->id);
 616                              $userenrolment = $DB->get_record_sql($sql, $params, IGNORE_MULTIPLE);
 617  
 618                              if (empty($userenrolment)) {
 619                                  $this->enrol_user($instance, $member->id, $role->id);
 620                                  // Make sure we set the enrolment status to active. If the user wasn't
 621                                  // previously enrolled to the course, enrol_user() sets it. But if we
 622                                  // configured the plugin to suspend the user enrolments _AND_ remove
 623                                  // the role assignments on external unenrol, then enrol_user() doesn't
 624                                  // set it back to active on external re-enrolment. So set it
 625                                  // unconditionally to cover both cases.
 626                                  $DB->set_field('user_enrolments', 'status', ENROL_USER_ACTIVE, array('enrolid'=>$instance->id, 'userid'=>$member->id));
 627                                  $trace->output(get_string('enroluser', 'enrol_ldap',
 628                                      array('user_username'=> $member->username,
 629                                            'course_shortname'=>$course_obj->shortname,
 630                                            'course_id'=>$course_obj->id)));
 631  
 632                              } else {
 633                                  if (!$DB->record_exists('role_assignments', array('roleid'=>$role->id, 'userid'=>$member->id, 'contextid'=>$context->id, 'component'=>'enrol_ldap', 'itemid'=>$instance->id))) {
 634                                      // This happens when reviving users or when user has multiple roles in one course.
 635                                      $context = context_course::instance($course_obj->id);
 636                                      role_assign($role->id, $member->id, $context->id, 'enrol_ldap', $instance->id);
 637                                      $trace->output("Assign role to user '$member->username' in course '$course_obj->shortname ($course_obj->id)'");
 638                                  }
 639                                  if ($userenrolment->status == ENROL_USER_SUSPENDED) {
 640                                      // Reenable enrolment that was previously disabled. Enrolment refreshed
 641                                      $DB->set_field('user_enrolments', 'status', ENROL_USER_ACTIVE, array('enrolid'=>$instance->id, 'userid'=>$member->id));
 642                                      $trace->output(get_string('enroluserenable', 'enrol_ldap',
 643                                          array('user_username'=> $member->username,
 644                                                'course_shortname'=>$course_obj->shortname,
 645                                                'course_id'=>$course_obj->id)));
 646                                  }
 647                              }
 648                          }
 649                          $transaction->allow_commit();
 650                      }
 651                  }
 652              }
 653          }
 654          @$this->ldap_close();
 655          $trace->finished();
 656      }
 657  
 658      /**
 659       * Connect to the LDAP server, using the plugin configured
 660       * settings. It's actually a wrapper around ldap_connect_moodle()
 661       *
 662       * @param progress_trace $trace
 663       * @return bool success
 664       */
 665      protected function ldap_connect(progress_trace $trace = null) {
 666          global $CFG;
 667          require_once($CFG->libdir.'/ldaplib.php');
 668  
 669          if (isset($this->ldapconnection)) {
 670              return true;
 671          }
 672  
 673          if ($ldapconnection = ldap_connect_moodle($this->get_config('host_url'), $this->get_config('ldap_version'),
 674                                                    $this->get_config('user_type'), $this->get_config('bind_dn'),
 675                                                    $this->get_config('bind_pw'), $this->get_config('opt_deref'),
 676                                                    $debuginfo, $this->get_config('start_tls'))) {
 677              $this->ldapconnection = $ldapconnection;
 678              return true;
 679          }
 680  
 681          if ($trace) {
 682              $trace->output($debuginfo);
 683          } else {
 684              error_log($this->errorlogtag.$debuginfo);
 685          }
 686  
 687          return false;
 688      }
 689  
 690      /**
 691       * Disconnects from a LDAP server
 692       *
 693       */
 694      protected function ldap_close() {
 695          if (isset($this->ldapconnection)) {
 696              @ldap_close($this->ldapconnection);
 697              $this->ldapconnection = null;
 698          }
 699          return;
 700      }
 701  
 702      /**
 703       * Return multidimensional array with details of user courses (at
 704       * least dn and idnumber).
 705       *
 706       * @param string $memberuid user idnumber (without magic quotes).
 707       * @param object role is a record from the mdl_role table.
 708       * @return array
 709       */
 710      protected function find_ext_enrolments($memberuid, $role) {
 711          global $CFG;
 712          require_once($CFG->libdir.'/ldaplib.php');
 713  
 714          if (empty($memberuid)) {
 715              // No "idnumber" stored for this user, so no LDAP enrolments
 716              return array();
 717          }
 718  
 719          $ldap_contexts = trim($this->get_config('contexts_role'.$role->id));
 720          if (empty($ldap_contexts)) {
 721              // No role contexts, so no LDAP enrolments
 722              return array();
 723          }
 724  
 725          $extmemberuid = core_text::convert($memberuid, 'utf-8', $this->get_config('ldapencoding'));
 726  
 727          if($this->get_config('memberattribute_isdn')) {
 728              if (!($extmemberuid = $this->ldap_find_userdn($extmemberuid))) {
 729                  return array();
 730              }
 731          }
 732  
 733          $ldap_search_pattern = '';
 734          if($this->get_config('nested_groups')) {
 735              $usergroups = $this->ldap_find_user_groups($extmemberuid);
 736              if(count($usergroups) > 0) {
 737                  foreach ($usergroups as $group) {
 738                      $group = ldap_filter_addslashes($group);
 739                      $ldap_search_pattern .= '('.$this->get_config('memberattribute_role'.$role->id).'='.$group.')';
 740                  }
 741              }
 742          }
 743  
 744          // Default return value
 745          $courses = array();
 746  
 747          // Get all the fields we will want for the potential course creation
 748          // as they are light. don't get membership -- potentially a lot of data.
 749          $ldap_fields_wanted = array('dn', $this->get_config('course_idnumber'));
 750          $fullname  = $this->get_config('course_fullname');
 751          $shortname = $this->get_config('course_shortname');
 752          $summary   = $this->get_config('course_summary');
 753          if (isset($fullname)) {
 754              array_push($ldap_fields_wanted, $fullname);
 755          }
 756          if (isset($shortname)) {
 757              array_push($ldap_fields_wanted, $shortname);
 758          }
 759          if (isset($summary)) {
 760              array_push($ldap_fields_wanted, $summary);
 761          }
 762  
 763          // Define the search pattern
 764          if (empty($ldap_search_pattern)) {
 765              $ldap_search_pattern = '('.$this->get_config('memberattribute_role'.$role->id).'='.ldap_filter_addslashes($extmemberuid).')';
 766          } else {
 767              $ldap_search_pattern = '(|' . $ldap_search_pattern .
 768                                         '('.$this->get_config('memberattribute_role'.$role->id).'='.ldap_filter_addslashes($extmemberuid).')' .
 769                                     ')';
 770          }
 771          $ldap_search_pattern='(&'.$this->get_config('objectclass').$ldap_search_pattern.')';
 772  
 773          // Get all contexts and look for first matching user
 774          $ldap_contexts = explode(';', $ldap_contexts);
 775          $ldap_pagedresults = ldap_paged_results_supported($this->get_config('ldap_version'), $this->ldapconnection);
 776          foreach ($ldap_contexts as $context) {
 777              $context = trim($context);
 778              if (empty($context)) {
 779                  continue;
 780              }
 781  
 782              $ldap_cookie = '';
 783              $servercontrols = array();
 784              $flat_records = array();
 785              do {
 786                  if ($ldap_pagedresults) {
 787                      $servercontrols = array(array(
 788                          'oid' => LDAP_CONTROL_PAGEDRESULTS, 'value' => array(
 789                              'size' => $this->config->pagesize, 'cookie' => $ldap_cookie)));
 790                  }
 791  
 792                  if ($this->get_config('course_search_sub')) {
 793                      // Use ldap_search to find first user from subtree
 794                      $ldap_result = @ldap_search($this->ldapconnection, $context,
 795                          $ldap_search_pattern, $ldap_fields_wanted,
 796                          0, -1, -1, LDAP_DEREF_NEVER, $servercontrols);
 797                  } else {
 798                      // Search only in this context
 799                      $ldap_result = @ldap_list($this->ldapconnection, $context,
 800                          $ldap_search_pattern, $ldap_fields_wanted,
 801                          0, -1, -1, LDAP_DEREF_NEVER, $servercontrols);
 802                  }
 803  
 804                  if (!$ldap_result) {
 805                      continue;
 806                  }
 807  
 808                  if ($ldap_pagedresults) {
 809                      // Get next server cookie to know if we'll need to continue searching.
 810                      $ldap_cookie = '';
 811                      // Get next cookie from controls.
 812                      ldap_parse_result($this->ldapconnection, $ldap_result, $errcode, $matcheddn,
 813                          $errmsg, $referrals, $controls);
 814                      if (isset($controls[LDAP_CONTROL_PAGEDRESULTS]['value']['cookie'])) {
 815                          $ldap_cookie = $controls[LDAP_CONTROL_PAGEDRESULTS]['value']['cookie'];
 816                      }
 817                  }
 818  
 819                  // Check and push results. ldap_get_entries() already
 820                  // lowercases the attribute index, so there's no need to
 821                  // use array_change_key_case() later.
 822                  $records = ldap_get_entries($this->ldapconnection, $ldap_result);
 823  
 824                  // LDAP libraries return an odd array, really. Fix it.
 825                  for ($c = 0; $c < $records['count']; $c++) {
 826                      array_push($flat_records, $records[$c]);
 827                  }
 828                  // Free some mem
 829                  unset($records);
 830              } while ($ldap_pagedresults && !empty($ldap_cookie));
 831  
 832              // If LDAP paged results were used, the current connection must be completely
 833              // closed and a new one created, to work without paged results from here on.
 834              if ($ldap_pagedresults) {
 835                  $this->ldap_close();
 836                  $this->ldap_connect();
 837              }
 838  
 839              if (count($flat_records)) {
 840                  $courses = array_merge($courses, $flat_records);
 841              }
 842          }
 843  
 844          return $courses;
 845      }
 846  
 847      /**
 848       * Search specified contexts for the specified userid and return the
 849       * user dn like: cn=username,ou=suborg,o=org. It's actually a wrapper
 850       * around ldap_find_userdn().
 851       *
 852       * @param string $userid the userid to search for (in external LDAP encoding, no magic quotes).
 853       * @return mixed the user dn or false
 854       */
 855      protected function ldap_find_userdn($userid) {
 856          global $CFG;
 857          require_once($CFG->libdir.'/ldaplib.php');
 858  
 859          $ldap_contexts = explode(';', $this->get_config('user_contexts'));
 860  
 861          return ldap_find_userdn($this->ldapconnection, $userid, $ldap_contexts,
 862                                  $this->userobjectclass,
 863                                  $this->get_config('idnumber_attribute'), $this->get_config('user_search_sub'));
 864      }
 865  
 866      /**
 867       * Find the groups a given distinguished name belongs to, both directly
 868       * and indirectly via nested groups membership.
 869       *
 870       * @param string $memberdn distinguished name to search
 871       * @return array with member groups' distinguished names (can be emtpy)
 872       */
 873      protected function ldap_find_user_groups($memberdn) {
 874          $groups = array();
 875  
 876          $this->ldap_find_user_groups_recursively($memberdn, $groups);
 877          return $groups;
 878      }
 879  
 880      /**
 881       * Recursively process the groups the given member distinguished name
 882       * belongs to, adding them to the already processed groups array.
 883       *
 884       * @param string $memberdn distinguished name to search
 885       * @param array reference &$membergroups array with already found
 886       *                        groups, where we'll put the newly found
 887       *                        groups.
 888       */
 889      protected function ldap_find_user_groups_recursively($memberdn, &$membergroups) {
 890          $result = @ldap_read($this->ldapconnection, $memberdn, '(objectClass=*)', array($this->get_config('group_memberofattribute')));
 891          if (!$result) {
 892              return;
 893          }
 894  
 895          if ($entry = ldap_first_entry($this->ldapconnection, $result)) {
 896              do {
 897                  $attributes = ldap_get_attributes($this->ldapconnection, $entry);
 898                  for ($j = 0; $j < $attributes['count']; $j++) {
 899                      $groups = ldap_get_values_len($this->ldapconnection, $entry, $attributes[$j]);
 900                      foreach ($groups as $key => $group) {
 901                          if ($key === 'count') {  // Skip the entries count
 902                              continue;
 903                          }
 904                          if(!in_array($group, $membergroups)) {
 905                              // Only push and recurse if we haven't 'seen' this group before
 906                              // to prevent loops (MS Active Directory allows them!!).
 907                              array_push($membergroups, $group);
 908                              $this->ldap_find_user_groups_recursively($group, $membergroups);
 909                          }
 910                      }
 911                  }
 912              }
 913              while ($entry = ldap_next_entry($this->ldapconnection, $entry));
 914          }
 915      }
 916  
 917      /**
 918       * Given a group name (either a RDN or a DN), get the list of users
 919       * belonging to that group. If the group has nested groups, expand all
 920       * the intermediate groups and return the full list of users that
 921       * directly or indirectly belong to the group.
 922       *
 923       * @param string $group the group name to search
 924       * @param string $memberattibute the attribute that holds the members of the group
 925       * @return array the list of users belonging to the group. If $group
 926       *         is not actually a group, returns array($group).
 927       */
 928      protected function ldap_explode_group($group, $memberattribute) {
 929          switch ($this->get_config('user_type')) {
 930              case 'ad':
 931                  // $group is already the distinguished name to search.
 932                  $dn = $group;
 933  
 934                  $result = ldap_read($this->ldapconnection, $dn, '(objectClass=*)', array('objectClass'));
 935                  $entry = ldap_first_entry($this->ldapconnection, $result);
 936                  $objectclass = ldap_get_values($this->ldapconnection, $entry, 'objectClass');
 937  
 938                  if (!in_array('group', $objectclass)) {
 939                      // Not a group, so return immediately.
 940                      return array($group);
 941                  }
 942  
 943                  $result = ldap_read($this->ldapconnection, $dn, '(objectClass=*)', array($memberattribute));
 944                  $entry = ldap_first_entry($this->ldapconnection, $result);
 945                  $members = @ldap_get_values($this->ldapconnection, $entry, $memberattribute); // Can be empty and throws a warning
 946                  if ($members['count'] == 0) {
 947                      // There are no members in this group, return nothing.
 948                      return array();
 949                  }
 950                  unset($members['count']);
 951  
 952                  $users = array();
 953                  foreach ($members as $member) {
 954                      $group_members = $this->ldap_explode_group($member, $memberattribute);
 955                      $users = array_merge($users, $group_members);
 956                  }
 957  
 958                  return ($users);
 959                  break;
 960              default:
 961                  error_log($this->errorlogtag.get_string('explodegroupusertypenotsupported', 'enrol_ldap',
 962                                                          $this->get_config('user_type_name')));
 963  
 964                  return array($group);
 965          }
 966      }
 967  
 968      /**
 969       * Will create the moodle course from the template
 970       * course_ext is an array as obtained from ldap -- flattened somewhat
 971       *
 972       * @param array $course_ext
 973       * @param progress_trace $trace
 974       * @return mixed false on error, id for the newly created course otherwise.
 975       */
 976      function create_course($course_ext, progress_trace $trace) {
 977          global $CFG, $DB;
 978  
 979          require_once("$CFG->dirroot/course/lib.php");
 980  
 981          // Override defaults with template course
 982          $template = false;
 983          if ($this->get_config('template')) {
 984              if ($template = $DB->get_record('course', array('shortname'=>$this->get_config('template')))) {
 985                  $template = fullclone(course_get_format($template)->get_course());
 986                  unset($template->id); // So we are clear to reinsert the record
 987                  unset($template->fullname);
 988                  unset($template->shortname);
 989                  unset($template->idnumber);
 990              }
 991          }
 992          if (!$template) {
 993              $courseconfig = get_config('moodlecourse');
 994              $template = new stdClass();
 995              $template->summary        = '';
 996              $template->summaryformat  = FORMAT_HTML;
 997              $template->format         = $courseconfig->format;
 998              $template->newsitems      = $courseconfig->newsitems;
 999              $template->showgrades     = $courseconfig->showgrades;
1000              $template->showreports    = $courseconfig->showreports;
1001              $template->maxbytes       = $courseconfig->maxbytes;
1002              $template->groupmode      = $courseconfig->groupmode;
1003              $template->groupmodeforce = $courseconfig->groupmodeforce;
1004              $template->visible        = $courseconfig->visible;
1005              $template->lang           = $courseconfig->lang;
1006              $template->enablecompletion = $courseconfig->enablecompletion;
1007          }
1008          $course = $template;
1009  
1010          $course->category = $this->get_config('category');
1011          if (!$DB->record_exists('course_categories', array('id'=>$this->get_config('category')))) {
1012              $categories = $DB->get_records('course_categories', array(), 'sortorder', 'id', 0, 1);
1013              $first = reset($categories);
1014              $course->category = $first->id;
1015          }
1016  
1017          // Override with required ext data
1018          $course->idnumber  = $course_ext[$this->get_config('course_idnumber')][0];
1019          $course->fullname  = $course_ext[$this->get_config('course_fullname')][0];
1020          $course->shortname = $course_ext[$this->get_config('course_shortname')][0];
1021          if (empty($course->idnumber) || empty($course->fullname) || empty($course->shortname)) {
1022              // We are in trouble!
1023              $trace->output(get_string('cannotcreatecourse', 'enrol_ldap').' '.var_export($course, true));
1024              return false;
1025          }
1026  
1027          $summary = $this->get_config('course_summary');
1028          if (!isset($summary) || empty($course_ext[$summary][0])) {
1029              $course->summary = '';
1030          } else {
1031              $course->summary = $course_ext[$this->get_config('course_summary')][0];
1032          }
1033  
1034          // Check if the shortname already exists if it does - skip course creation.
1035          if ($DB->record_exists('course', array('shortname' => $course->shortname))) {
1036              $trace->output(get_string('duplicateshortname', 'enrol_ldap', $course));
1037              return false;
1038          }
1039  
1040          $newcourse = create_course($course);
1041          return $newcourse->id;
1042      }
1043  
1044      /**
1045       * Will update a moodle course with new values from LDAP
1046       * A field will be updated only if it is marked to be updated
1047       * on sync in plugin settings
1048       *
1049       * @param object $course
1050       * @param array $externalcourse
1051       * @param progress_trace $trace
1052       * @return bool
1053       */
1054      protected function update_course($course, $externalcourse, progress_trace $trace) {
1055          global $CFG, $DB;
1056  
1057          $coursefields = array ('shortname', 'fullname', 'summary');
1058          static $shouldupdate;
1059  
1060          // Initialize $shouldupdate variable. Set to true if one or more fields are marked for update.
1061          if (!isset($shouldupdate)) {
1062              $shouldupdate = false;
1063              foreach ($coursefields as $field) {
1064                  $shouldupdate = $shouldupdate || $this->get_config('course_'.$field.'_updateonsync');
1065              }
1066          }
1067  
1068          // If we should not update return immediately.
1069          if (!$shouldupdate) {
1070              return false;
1071          }
1072  
1073          require_once("$CFG->dirroot/course/lib.php");
1074          $courseupdated = false;
1075          $updatedcourse = new stdClass();
1076          $updatedcourse->id = $course->id;
1077  
1078          // Update course fields if necessary.
1079          foreach ($coursefields as $field) {
1080              // If field is marked to be updated on sync && field data was changed update it.
1081              if ($this->get_config('course_'.$field.'_updateonsync')
1082                      && isset($externalcourse[$this->get_config('course_'.$field)][0])
1083                      && $course->{$field} != $externalcourse[$this->get_config('course_'.$field)][0]) {
1084                  $updatedcourse->{$field} = $externalcourse[$this->get_config('course_'.$field)][0];
1085                  $courseupdated = true;
1086              }
1087          }
1088  
1089          if (!$courseupdated) {
1090              $trace->output(get_string('courseupdateskipped', 'enrol_ldap', $course));
1091              return false;
1092          }
1093  
1094          // Do not allow empty fullname or shortname.
1095          if ((isset($updatedcourse->fullname) && empty($updatedcourse->fullname))
1096                  || (isset($updatedcourse->shortname) && empty($updatedcourse->shortname))) {
1097              // We are in trouble!
1098              $trace->output(get_string('cannotupdatecourse', 'enrol_ldap', $course));
1099              return false;
1100          }
1101  
1102          // Check if the shortname already exists if it does - skip course updating.
1103          if (isset($updatedcourse->shortname)
1104                  && $DB->record_exists('course', array('shortname' => $updatedcourse->shortname))) {
1105              $trace->output(get_string('cannotupdatecourse_duplicateshortname', 'enrol_ldap', $course));
1106              return false;
1107          }
1108  
1109          // Finally - update course in DB.
1110          update_course($updatedcourse);
1111          $trace->output(get_string('courseupdated', 'enrol_ldap', $course));
1112  
1113          return true;
1114      }
1115  
1116      /**
1117       * Automatic enrol sync executed during restore.
1118       * Useful for automatic sync by course->idnumber or course category.
1119       * @param stdClass $course course record
1120       */
1121      public function restore_sync_course($course) {
1122          // TODO: this can not work because restore always nukes the course->idnumber, do not ask me why (MDL-37312)
1123          // NOTE: for now restore does not do any real logging yet, let's do the same here...
1124          $trace = new error_log_progress_trace();
1125          $this->sync_enrolments($trace, $course->id);
1126      }
1127  
1128      /**
1129       * Restore instance and map settings.
1130       *
1131       * @param restore_enrolments_structure_step $step
1132       * @param stdClass $data
1133       * @param stdClass $course
1134       * @param int $oldid
1135       */
1136      public function restore_instance(restore_enrolments_structure_step $step, stdClass $data, $course, $oldid) {
1137          global $DB;
1138          // There is only 1 ldap enrol instance per course.
1139          if ($instances = $DB->get_records('enrol', array('courseid'=>$data->courseid, 'enrol'=>'ldap'), 'id')) {
1140              $instance = reset($instances);
1141              $instanceid = $instance->id;
1142          } else {
1143              $instanceid = $this->add_instance($course, (array)$data);
1144          }
1145          $step->set_mapping('enrol', $oldid, $instanceid);
1146      }
1147  
1148      /**
1149       * Restore user enrolment.
1150       *
1151       * @param restore_enrolments_structure_step $step
1152       * @param stdClass $data
1153       * @param stdClass $instance
1154       * @param int $oldinstancestatus
1155       * @param int $userid
1156       */
1157      public function restore_user_enrolment(restore_enrolments_structure_step $step, $data, $instance, $userid, $oldinstancestatus) {
1158          global $DB;
1159  
1160          if ($this->get_config('unenrolaction') == ENROL_EXT_REMOVED_UNENROL) {
1161              // Enrolments were already synchronised in restore_instance(), we do not want any suspended leftovers.
1162  
1163          } else if ($this->get_config('unenrolaction') == ENROL_EXT_REMOVED_KEEP) {
1164              if (!$DB->record_exists('user_enrolments', array('enrolid'=>$instance->id, 'userid'=>$userid))) {
1165                  $this->enrol_user($instance, $userid, null, 0, 0, $data->status);
1166              }
1167  
1168          } else {
1169              if (!$DB->record_exists('user_enrolments', array('enrolid'=>$instance->id, 'userid'=>$userid))) {
1170                  $this->enrol_user($instance, $userid, null, 0, 0, ENROL_USER_SUSPENDED);
1171              }
1172          }
1173      }
1174  
1175      /**
1176       * Restore role assignment.
1177       *
1178       * @param stdClass $instance
1179       * @param int $roleid
1180       * @param int $userid
1181       * @param int $contextid
1182       */
1183      public function restore_role_assignment($instance, $roleid, $userid, $contextid) {
1184          global $DB;
1185  
1186          if ($this->get_config('unenrolaction') == ENROL_EXT_REMOVED_UNENROL or $this->get_config('unenrolaction') == ENROL_EXT_REMOVED_SUSPENDNOROLES) {
1187              // Skip any roles restore, they should be already synced automatically.
1188              return;
1189          }
1190  
1191          // Just restore every role.
1192          if ($DB->record_exists('user_enrolments', array('enrolid'=>$instance->id, 'userid'=>$userid))) {
1193              role_assign($roleid, $userid, $contextid, 'enrol_'.$instance->enrol, $instance->id);
1194          }
1195      }
1196  }