Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 3.11.x will end 14 Nov 2022 (12 months plus 6 months extension).
  • Bug fixes for security issues in 3.11.x will end 13 Nov 2023 (18 months plus 12 months extension).
  • PHP version: minimum PHP 7.3.0 Note: minimum PHP version has increased since Moodle 3.10. PHP 7.4.x is supported too.
/enrol/ldap/ -> lib.php (source)

Differences Between: [Versions 310 and 311] [Versions 311 and 402] [Versions 311 and 403] [Versions 39 and 311]

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