Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 3.10.x will end 8 November 2021 (12 months).
  • Bug fixes for security issues in 3.10.x will end 9 May 2022 (18 months).
  • PHP version: minimum PHP 7.2.0 Note: minimum PHP version has increased since Moodle 3.8. PHP 7.3.x and 7.4.x are supported too.
/enrol/ldap/ -> lib.php (source)

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

   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                          // TODO: Remove the old branch of code once PHP 7.3.0 becomes required (Moodle 3.11).
 393                          if (version_compare(PHP_VERSION, '7.3.0', '<')) {
 394                              // Before 7.3, use this function that was deprecated in PHP 7.4.
 395                              ldap_control_paged_result($this->ldapconnection, $this->config->pagesize, true, $ldap_cookie);
 396                          } else {
 397                              // PHP 7.3 and up, use server controls.
 398                              $servercontrols = array(array(
 399                                  'oid' => LDAP_CONTROL_PAGEDRESULTS, 'value' => array(
 400                                      'size' => $this->config->pagesize, 'cookie' => $ldap_cookie)));
 401                          }
 402                      }
 403  
 404                      if ($this->config->course_search_sub) {
 405                          // Use ldap_search to find first user from subtree
 406                          // TODO: Remove the old branch of code once PHP 7.3.0 becomes required (Moodle 3.11).
 407                          if (version_compare(PHP_VERSION, '7.3.0', '<')) {
 408                              $ldap_result = @ldap_search($this->ldapconnection, $ldap_context,
 409                                  $ldap_search_pattern, $ldap_fields_wanted);
 410                          } else {
 411                              $ldap_result = @ldap_search($this->ldapconnection, $ldap_context,
 412                                  $ldap_search_pattern, $ldap_fields_wanted,
 413                                  0, -1, -1, LDAP_DEREF_NEVER, $servercontrols);
 414                          }
 415                      } else {
 416                          // Search only in this context
 417                          // TODO: Remove the old branch of code once PHP 7.3.0 becomes required (Moodle 3.11).
 418                          if (version_compare(PHP_VERSION, '7.3.0', '<')) {
 419                              $ldap_result = @ldap_list($this->ldapconnection, $ldap_context,
 420                                  $ldap_search_pattern, $ldap_fields_wanted);
 421                          } else {
 422                              $ldap_result = @ldap_list($this->ldapconnection, $ldap_context,
 423                                  $ldap_search_pattern, $ldap_fields_wanted,
 424                                  0, -1, -1, LDAP_DEREF_NEVER, $servercontrols);
 425                          }
 426                      }
 427                      if (!$ldap_result) {
 428                          continue; // Next
 429                      }
 430  
 431                      if ($ldap_pagedresults) {
 432                          // Get next server cookie to know if we'll need to continue searching.
 433                          $ldap_cookie = '';
 434                          // TODO: Remove the old branch of code once PHP 7.3.0 becomes required (Moodle 3.11).
 435                          if (version_compare(PHP_VERSION, '7.3.0', '<')) {
 436                              // Before 7.3, use this function that was deprecated in PHP 7.4.
 437                              ldap_control_paged_result_response($this->ldapconnection, $ldap_result, $ldap_cookie);
 438                          } else {
 439                              // Get next cookie from controls.
 440                              ldap_parse_result($this->ldapconnection, $ldap_result, $errcode, $matcheddn,
 441                                  $errmsg, $referrals, $controls);
 442                              if (isset($controls[LDAP_CONTROL_PAGEDRESULTS]['value']['cookie'])) {
 443                                  $ldap_cookie = $controls[LDAP_CONTROL_PAGEDRESULTS]['value']['cookie'];
 444                              }
 445                          }
 446                      }
 447  
 448                      // Check and push results
 449                      $records = ldap_get_entries($this->ldapconnection, $ldap_result);
 450  
 451                      // LDAP libraries return an odd array, really. fix it:
 452                      for ($c = 0; $c < $records['count']; $c++) {
 453                          array_push($flat_records, $records[$c]);
 454                      }
 455                      // Free some mem
 456                      unset($records);
 457                  } while ($ldap_pagedresults && !empty($ldap_cookie));
 458  
 459                  // If LDAP paged results were used, the current connection must be completely
 460                  // closed and a new one created, to work without paged results from here on.
 461                  if ($ldap_pagedresults) {
 462                      $this->ldap_close();
 463                      $this->ldap_connect($trace);
 464                  }
 465  
 466                  if (count($flat_records)) {
 467                      $ignorehidden = $this->get_config('ignorehiddencourses');
 468                      foreach($flat_records as $course) {
 469                          $course = array_change_key_case($course, CASE_LOWER);
 470                          $idnumber = $course[$this->config->course_idnumber][0];
 471                          $trace->output(get_string('synccourserole', 'enrol_ldap', array('idnumber'=>$idnumber, 'role_shortname'=>$role->shortname)));
 472  
 473                          // Does the course exist in moodle already?
 474                          $course_obj = $DB->get_record('course', array($this->enrol_localcoursefield=>$idnumber));
 475                          if (empty($course_obj)) { // Course doesn't exist
 476                              if ($this->get_config('autocreate')) { // Autocreate
 477                                  $trace->output(get_string('createcourseextid', 'enrol_ldap', array('courseextid'=>$idnumber)));
 478                                  if (!$newcourseid = $this->create_course($course, $trace)) {
 479                                      continue;
 480                                  }
 481                                  $course_obj = $DB->get_record('course', array('id'=>$newcourseid));
 482                              } else {
 483                                  $trace->output(get_string('createnotcourseextid', 'enrol_ldap', array('courseextid'=>$idnumber)));
 484                                  continue; // Next; skip this one!
 485                              }
 486                          } else {  // Check if course needs update & update as needed.
 487                              $this->update_course($course_obj, $course, $trace);
 488                          }
 489  
 490                          // Enrol & unenrol
 491  
 492                          // Pull the ldap membership into a nice array
 493                          // this is an odd array -- mix of hash and array --
 494                          $ldapmembers = array();
 495  
 496                          if (property_exists($this->config, 'memberattribute_role'.$role->id)
 497                              && !empty($this->config->{'memberattribute_role'.$role->id})
 498                              && !empty($course[$this->config->{'memberattribute_role'.$role->id}])) { // May have no membership!
 499  
 500                              $ldapmembers = $course[$this->config->{'memberattribute_role'.$role->id}];
 501                              unset($ldapmembers['count']); // Remove oddity ;)
 502  
 503                              // If we have enabled nested groups, we need to expand
 504                              // the groups to get the real user list. We need to do
 505                              // this before dealing with 'memberattribute_isdn'.
 506                              if ($this->config->nested_groups) {
 507                                  $users = array();
 508                                  foreach ($ldapmembers as $ldapmember) {
 509                                      $grpusers = $this->ldap_explode_group($ldapmember,
 510                                                                            $this->config->{'memberattribute_role'.$role->id});
 511  
 512                                      $users = array_merge($users, $grpusers);
 513                                  }
 514                                  $ldapmembers = array_unique($users); // There might be duplicates.
 515                              }
 516  
 517                              // Deal with the case where the member attribute holds distinguished names,
 518                              // but only if the user attribute is not a distinguished name itself.
 519                              if ($this->config->memberattribute_isdn
 520                                  && ($this->config->idnumber_attribute !== 'dn')
 521                                  && ($this->config->idnumber_attribute !== 'distinguishedname')) {
 522                                  // We need to retrieve the idnumber for all the users in $ldapmembers,
 523                                  // as the idnumber does not match their dn and we get dn's from membership.
 524                                  $memberidnumbers = array();
 525                                  foreach ($ldapmembers as $ldapmember) {
 526                                      $result = ldap_read($this->ldapconnection, $ldapmember, $this->userobjectclass,
 527                                                          array($this->config->idnumber_attribute));
 528                                      $entry = ldap_first_entry($this->ldapconnection, $result);
 529                                      $values = ldap_get_values($this->ldapconnection, $entry, $this->config->idnumber_attribute);
 530                                      array_push($memberidnumbers, $values[0]);
 531                                  }
 532  
 533                                  $ldapmembers = $memberidnumbers;
 534                              }
 535                          }
 536  
 537                          // Prune old ldap enrolments
 538                          // hopefully they'll fit in the max buffer size for the RDBMS
 539                          $sql= "SELECT u.id as userid, u.username, ue.status,
 540                                        ra.contextid, ra.itemid as instanceid
 541                                   FROM {user} u
 542                                   JOIN {role_assignments} ra ON (ra.userid = u.id AND ra.component = 'enrol_ldap' AND ra.roleid = :roleid)
 543                                   JOIN {user_enrolments} ue ON (ue.userid = u.id AND ue.enrolid = ra.itemid)
 544                                   JOIN {enrol} e ON (e.id = ue.enrolid)
 545                                  WHERE u.deleted = 0 AND e.courseid = :courseid ";
 546                          $params = array('roleid'=>$role->id, 'courseid'=>$course_obj->id);
 547                          $context = context_course::instance($course_obj->id);
 548                          if (!empty($ldapmembers)) {
 549                              list($ldapml, $params2) = $DB->get_in_or_equal($ldapmembers, SQL_PARAMS_NAMED, 'm', false);
 550                              $sql .= "AND u.idnumber $ldapml";
 551                              $params = array_merge($params, $params2);
 552                              unset($params2);
 553                          } else {
 554                              $shortname = format_string($course_obj->shortname, true, array('context' => $context));
 555                              $trace->output(get_string('emptyenrolment', 'enrol_ldap',
 556                                           array('role_shortname'=> $role->shortname,
 557                                                 'course_shortname' => $shortname)));
 558                          }
 559                          $todelete = $DB->get_records_sql($sql, $params);
 560  
 561                          if (!empty($todelete)) {
 562                              $transaction = $DB->start_delegated_transaction();
 563                              foreach ($todelete as $row) {
 564                                  $instance = $DB->get_record('enrol', array('id'=>$row->instanceid));
 565                                  switch ($this->get_config('unenrolaction')) {
 566                                  case ENROL_EXT_REMOVED_UNENROL:
 567                                      $this->unenrol_user($instance, $row->userid);
 568                                      $trace->output(get_string('extremovedunenrol', 'enrol_ldap',
 569                                          array('user_username'=> $row->username,
 570                                                'course_shortname'=>$course_obj->shortname,
 571                                                'course_id'=>$course_obj->id)));
 572                                      break;
 573                                  case ENROL_EXT_REMOVED_KEEP:
 574                                      // Keep - only adding enrolments
 575                                      break;
 576                                  case ENROL_EXT_REMOVED_SUSPEND:
 577                                      if ($row->status != ENROL_USER_SUSPENDED) {
 578                                          $DB->set_field('user_enrolments', 'status', ENROL_USER_SUSPENDED, array('enrolid'=>$instance->id, 'userid'=>$row->userid));
 579                                          $trace->output(get_string('extremovedsuspend', 'enrol_ldap',
 580                                              array('user_username'=> $row->username,
 581                                                    'course_shortname'=>$course_obj->shortname,
 582                                                    'course_id'=>$course_obj->id)));
 583                                      }
 584                                      break;
 585                                  case ENROL_EXT_REMOVED_SUSPENDNOROLES:
 586                                      if ($row->status != ENROL_USER_SUSPENDED) {
 587                                          $DB->set_field('user_enrolments', 'status', ENROL_USER_SUSPENDED, array('enrolid'=>$instance->id, 'userid'=>$row->userid));
 588                                      }
 589                                      role_unassign_all(array('contextid'=>$row->contextid, 'userid'=>$row->userid, 'component'=>'enrol_ldap', 'itemid'=>$instance->id));
 590                                      $trace->output(get_string('extremovedsuspendnoroles', 'enrol_ldap',
 591                                          array('user_username'=> $row->username,
 592                                                'course_shortname'=>$course_obj->shortname,
 593                                                'course_id'=>$course_obj->id)));
 594                                      break;
 595                                  }
 596                              }
 597                              $transaction->allow_commit();
 598                          }
 599  
 600                          // Insert current enrolments
 601                          // bad we can't do INSERT IGNORE with postgres...
 602  
 603                          // Add necessary enrol instance if not present yet;
 604                          $sql = "SELECT c.id, c.visible, e.id as enrolid
 605                                    FROM {course} c
 606                                    JOIN {enrol} e ON (e.courseid = c.id AND e.enrol = 'ldap')
 607                                   WHERE c.id = :courseid";
 608                          $params = array('courseid'=>$course_obj->id);
 609                          if (!($course_instance = $DB->get_record_sql($sql, $params, IGNORE_MULTIPLE))) {
 610                              $course_instance = new stdClass();
 611                              $course_instance->id = $course_obj->id;
 612                              $course_instance->visible = $course_obj->visible;
 613                              $course_instance->enrolid = $this->add_instance($course_instance);
 614                          }
 615  
 616                          if (!$instance = $DB->get_record('enrol', array('id'=>$course_instance->enrolid))) {
 617                              continue; // Weird; skip this one.
 618                          }
 619  
 620                          if ($ignorehidden && !$course_instance->visible) {
 621                              continue;
 622                          }
 623  
 624                          $transaction = $DB->start_delegated_transaction();
 625                          foreach ($ldapmembers as $ldapmember) {
 626                              $sql = 'SELECT id,username,1 FROM {user} WHERE idnumber = ? AND deleted = 0';
 627                              $member = $DB->get_record_sql($sql, array($ldapmember));
 628                              if(empty($member) || empty($member->id)){
 629                                  $trace->output(get_string('couldnotfinduser', 'enrol_ldap', $ldapmember));
 630                                  continue;
 631                              }
 632  
 633                              $sql= "SELECT ue.status
 634                                       FROM {user_enrolments} ue
 635                                       JOIN {enrol} e ON (e.id = ue.enrolid AND e.enrol = 'ldap')
 636                                      WHERE e.courseid = :courseid AND ue.userid = :userid";
 637                              $params = array('courseid'=>$course_obj->id, 'userid'=>$member->id);
 638                              $userenrolment = $DB->get_record_sql($sql, $params, IGNORE_MULTIPLE);
 639  
 640                              if (empty($userenrolment)) {
 641                                  $this->enrol_user($instance, $member->id, $role->id);
 642                                  // Make sure we set the enrolment status to active. If the user wasn't
 643                                  // previously enrolled to the course, enrol_user() sets it. But if we
 644                                  // configured the plugin to suspend the user enrolments _AND_ remove
 645                                  // the role assignments on external unenrol, then enrol_user() doesn't
 646                                  // set it back to active on external re-enrolment. So set it
 647                                  // unconditionally to cover both cases.
 648                                  $DB->set_field('user_enrolments', 'status', ENROL_USER_ACTIVE, array('enrolid'=>$instance->id, 'userid'=>$member->id));
 649                                  $trace->output(get_string('enroluser', 'enrol_ldap',
 650                                      array('user_username'=> $member->username,
 651                                            'course_shortname'=>$course_obj->shortname,
 652                                            'course_id'=>$course_obj->id)));
 653  
 654                              } else {
 655                                  if (!$DB->record_exists('role_assignments', array('roleid'=>$role->id, 'userid'=>$member->id, 'contextid'=>$context->id, 'component'=>'enrol_ldap', 'itemid'=>$instance->id))) {
 656                                      // This happens when reviving users or when user has multiple roles in one course.
 657                                      $context = context_course::instance($course_obj->id);
 658                                      role_assign($role->id, $member->id, $context->id, 'enrol_ldap', $instance->id);
 659                                      $trace->output("Assign role to user '$member->username' in course '$course_obj->shortname ($course_obj->id)'");
 660                                  }
 661                                  if ($userenrolment->status == ENROL_USER_SUSPENDED) {
 662                                      // Reenable enrolment that was previously disabled. Enrolment refreshed
 663                                      $DB->set_field('user_enrolments', 'status', ENROL_USER_ACTIVE, array('enrolid'=>$instance->id, 'userid'=>$member->id));
 664                                      $trace->output(get_string('enroluserenable', 'enrol_ldap',
 665                                          array('user_username'=> $member->username,
 666                                                'course_shortname'=>$course_obj->shortname,
 667                                                'course_id'=>$course_obj->id)));
 668                                  }
 669                              }
 670                          }
 671                          $transaction->allow_commit();
 672                      }
 673                  }
 674              }
 675          }
 676          @$this->ldap_close();
 677          $trace->finished();
 678      }
 679  
 680      /**
 681       * Connect to the LDAP server, using the plugin configured
 682       * settings. It's actually a wrapper around ldap_connect_moodle()
 683       *
 684       * @param progress_trace $trace
 685       * @return bool success
 686       */
 687      protected function ldap_connect(progress_trace $trace = null) {
 688          global $CFG;
 689          require_once($CFG->libdir.'/ldaplib.php');
 690  
 691          if (isset($this->ldapconnection)) {
 692              return true;
 693          }
 694  
 695          if ($ldapconnection = ldap_connect_moodle($this->get_config('host_url'), $this->get_config('ldap_version'),
 696                                                    $this->get_config('user_type'), $this->get_config('bind_dn'),
 697                                                    $this->get_config('bind_pw'), $this->get_config('opt_deref'),
 698                                                    $debuginfo, $this->get_config('start_tls'))) {
 699              $this->ldapconnection = $ldapconnection;
 700              return true;
 701          }
 702  
 703          if ($trace) {
 704              $trace->output($debuginfo);
 705          } else {
 706              error_log($this->errorlogtag.$debuginfo);
 707          }
 708  
 709          return false;
 710      }
 711  
 712      /**
 713       * Disconnects from a LDAP server
 714       *
 715       */
 716      protected function ldap_close() {
 717          if (isset($this->ldapconnection)) {
 718              @ldap_close($this->ldapconnection);
 719              $this->ldapconnection = null;
 720          }
 721          return;
 722      }
 723  
 724      /**
 725       * Return multidimensional array with details of user courses (at
 726       * least dn and idnumber).
 727       *
 728       * @param string $memberuid user idnumber (without magic quotes).
 729       * @param object role is a record from the mdl_role table.
 730       * @return array
 731       */
 732      protected function find_ext_enrolments($memberuid, $role) {
 733          global $CFG;
 734          require_once($CFG->libdir.'/ldaplib.php');
 735  
 736          if (empty($memberuid)) {
 737              // No "idnumber" stored for this user, so no LDAP enrolments
 738              return array();
 739          }
 740  
 741          $ldap_contexts = trim($this->get_config('contexts_role'.$role->id));
 742          if (empty($ldap_contexts)) {
 743              // No role contexts, so no LDAP enrolments
 744              return array();
 745          }
 746  
 747          $extmemberuid = core_text::convert($memberuid, 'utf-8', $this->get_config('ldapencoding'));
 748  
 749          if($this->get_config('memberattribute_isdn')) {
 750              if (!($extmemberuid = $this->ldap_find_userdn($extmemberuid))) {
 751                  return array();
 752              }
 753          }
 754  
 755          $ldap_search_pattern = '';
 756          if($this->get_config('nested_groups')) {
 757              $usergroups = $this->ldap_find_user_groups($extmemberuid);
 758              if(count($usergroups) > 0) {
 759                  foreach ($usergroups as $group) {
 760                      $group = ldap_filter_addslashes($group);
 761                      $ldap_search_pattern .= '('.$this->get_config('memberattribute_role'.$role->id).'='.$group.')';
 762                  }
 763              }
 764          }
 765  
 766          // Default return value
 767          $courses = array();
 768  
 769          // Get all the fields we will want for the potential course creation
 770          // as they are light. don't get membership -- potentially a lot of data.
 771          $ldap_fields_wanted = array('dn', $this->get_config('course_idnumber'));
 772          $fullname  = $this->get_config('course_fullname');
 773          $shortname = $this->get_config('course_shortname');
 774          $summary   = $this->get_config('course_summary');
 775          if (isset($fullname)) {
 776              array_push($ldap_fields_wanted, $fullname);
 777          }
 778          if (isset($shortname)) {
 779              array_push($ldap_fields_wanted, $shortname);
 780          }
 781          if (isset($summary)) {
 782              array_push($ldap_fields_wanted, $summary);
 783          }
 784  
 785          // Define the search pattern
 786          if (empty($ldap_search_pattern)) {
 787              $ldap_search_pattern = '('.$this->get_config('memberattribute_role'.$role->id).'='.ldap_filter_addslashes($extmemberuid).')';
 788          } else {
 789              $ldap_search_pattern = '(|' . $ldap_search_pattern .
 790                                         '('.$this->get_config('memberattribute_role'.$role->id).'='.ldap_filter_addslashes($extmemberuid).')' .
 791                                     ')';
 792          }
 793          $ldap_search_pattern='(&'.$this->get_config('objectclass').$ldap_search_pattern.')';
 794  
 795          // Get all contexts and look for first matching user
 796          $ldap_contexts = explode(';', $ldap_contexts);
 797          $ldap_pagedresults = ldap_paged_results_supported($this->get_config('ldap_version'), $this->ldapconnection);
 798          foreach ($ldap_contexts as $context) {
 799              $context = trim($context);
 800              if (empty($context)) {
 801                  continue;
 802              }
 803  
 804              $ldap_cookie = '';
 805              $servercontrols = array();
 806              $flat_records = array();
 807              do {
 808                  if ($ldap_pagedresults) {
 809                      // TODO: Remove the old branch of code once PHP 7.3.0 becomes required (Moodle 3.11).
 810                      if (version_compare(PHP_VERSION, '7.3.0', '<')) {
 811                          // Before 7.3, use this function that was deprecated in PHP 7.4.
 812                          ldap_control_paged_result($this->ldapconnection, $this->config->pagesize, true, $ldap_cookie);
 813                      } else {
 814                          // PHP 7.3 and up, use server controls.
 815                          $servercontrols = array(array(
 816                              'oid' => LDAP_CONTROL_PAGEDRESULTS, 'value' => array(
 817                                  'size' => $this->config->pagesize, 'cookie' => $ldap_cookie)));
 818                      }
 819                  }
 820  
 821                  if ($this->get_config('course_search_sub')) {
 822                      // Use ldap_search to find first user from subtree
 823                      // TODO: Remove the old branch of code once PHP 7.3.0 becomes required (Moodle 3.11).
 824                      if (version_compare(PHP_VERSION, '7.3.0', '<')) {
 825                          $ldap_result = @ldap_search($this->ldapconnection, $context,
 826                              $ldap_search_pattern, $ldap_fields_wanted);
 827                      } else {
 828                          $ldap_result = @ldap_search($this->ldapconnection, $context,
 829                              $ldap_search_pattern, $ldap_fields_wanted,
 830                              0, -1, -1, LDAP_DEREF_NEVER, $servercontrols);
 831                      }
 832                  } else {
 833                      // Search only in this context
 834                      // TODO: Remove the old branch of code once PHP 7.3.0 becomes required (Moodle 3.11).
 835                      if (version_compare(PHP_VERSION, '7.3.0', '<')) {
 836                          $ldap_result = @ldap_list($this->ldapconnection, $context,
 837                              $ldap_search_pattern, $ldap_fields_wanted);
 838                      } else {
 839                          $ldap_result = @ldap_list($this->ldapconnection, $context,
 840                              $ldap_search_pattern, $ldap_fields_wanted,
 841                              0, -1, -1, LDAP_DEREF_NEVER, $servercontrols);
 842                      }
 843                  }
 844  
 845                  if (!$ldap_result) {
 846                      continue;
 847                  }
 848  
 849                  if ($ldap_pagedresults) {
 850                      // Get next server cookie to know if we'll need to continue searching.
 851                      $ldap_cookie = '';
 852                      // TODO: Remove the old branch of code once PHP 7.3.0 becomes required (Moodle 3.11).
 853                      if (version_compare(PHP_VERSION, '7.3.0', '<')) {
 854                          // Before 7.3, use this function that was deprecated in PHP 7.4.
 855                          ldap_control_paged_result_response($this->ldapconnection, $ldap_result, $ldap_cookie);
 856                      } else {
 857                          // Get next cookie from controls.
 858                          ldap_parse_result($this->ldapconnection, $ldap_result, $errcode, $matcheddn,
 859                              $errmsg, $referrals, $controls);
 860                          if (isset($controls[LDAP_CONTROL_PAGEDRESULTS]['value']['cookie'])) {
 861                              $ldap_cookie = $controls[LDAP_CONTROL_PAGEDRESULTS]['value']['cookie'];
 862                          }
 863                      }
 864                  }
 865  
 866                  // Check and push results. ldap_get_entries() already
 867                  // lowercases the attribute index, so there's no need to
 868                  // use array_change_key_case() later.
 869                  $records = ldap_get_entries($this->ldapconnection, $ldap_result);
 870  
 871                  // LDAP libraries return an odd array, really. Fix it.
 872                  for ($c = 0; $c < $records['count']; $c++) {
 873                      array_push($flat_records, $records[$c]);
 874                  }
 875                  // Free some mem
 876                  unset($records);
 877              } while ($ldap_pagedresults && !empty($ldap_cookie));
 878  
 879              // If LDAP paged results were used, the current connection must be completely
 880              // closed and a new one created, to work without paged results from here on.
 881              if ($ldap_pagedresults) {
 882                  $this->ldap_close();
 883                  $this->ldap_connect();
 884              }
 885  
 886              if (count($flat_records)) {
 887                  $courses = array_merge($courses, $flat_records);
 888              }
 889          }
 890  
 891          return $courses;
 892      }
 893  
 894      /**
 895       * Search specified contexts for the specified userid and return the
 896       * user dn like: cn=username,ou=suborg,o=org. It's actually a wrapper
 897       * around ldap_find_userdn().
 898       *
 899       * @param string $userid the userid to search for (in external LDAP encoding, no magic quotes).
 900       * @return mixed the user dn or false
 901       */
 902      protected function ldap_find_userdn($userid) {
 903          global $CFG;
 904          require_once($CFG->libdir.'/ldaplib.php');
 905  
 906          $ldap_contexts = explode(';', $this->get_config('user_contexts'));
 907  
 908          return ldap_find_userdn($this->ldapconnection, $userid, $ldap_contexts,
 909                                  $this->userobjectclass,
 910                                  $this->get_config('idnumber_attribute'), $this->get_config('user_search_sub'));
 911      }
 912  
 913      /**
 914       * Find the groups a given distinguished name belongs to, both directly
 915       * and indirectly via nested groups membership.
 916       *
 917       * @param string $memberdn distinguished name to search
 918       * @return array with member groups' distinguished names (can be emtpy)
 919       */
 920      protected function ldap_find_user_groups($memberdn) {
 921          $groups = array();
 922  
 923          $this->ldap_find_user_groups_recursively($memberdn, $groups);
 924          return $groups;
 925      }
 926  
 927      /**
 928       * Recursively process the groups the given member distinguished name
 929       * belongs to, adding them to the already processed groups array.
 930       *
 931       * @param string $memberdn distinguished name to search
 932       * @param array reference &$membergroups array with already found
 933       *                        groups, where we'll put the newly found
 934       *                        groups.
 935       */
 936      protected function ldap_find_user_groups_recursively($memberdn, &$membergroups) {
 937          $result = @ldap_read($this->ldapconnection, $memberdn, '(objectClass=*)', array($this->get_config('group_memberofattribute')));
 938          if (!$result) {
 939              return;
 940          }
 941  
 942          if ($entry = ldap_first_entry($this->ldapconnection, $result)) {
 943              do {
 944                  $attributes = ldap_get_attributes($this->ldapconnection, $entry);
 945                  for ($j = 0; $j < $attributes['count']; $j++) {
 946                      $groups = ldap_get_values_len($this->ldapconnection, $entry, $attributes[$j]);
 947                      foreach ($groups as $key => $group) {
 948                          if ($key === 'count') {  // Skip the entries count
 949                              continue;
 950                          }
 951                          if(!in_array($group, $membergroups)) {
 952                              // Only push and recurse if we haven't 'seen' this group before
 953                              // to prevent loops (MS Active Directory allows them!!).
 954                              array_push($membergroups, $group);
 955                              $this->ldap_find_user_groups_recursively($group, $membergroups);
 956                          }
 957                      }
 958                  }
 959              }
 960              while ($entry = ldap_next_entry($this->ldapconnection, $entry));
 961          }
 962      }
 963  
 964      /**
 965       * Given a group name (either a RDN or a DN), get the list of users
 966       * belonging to that group. If the group has nested groups, expand all
 967       * the intermediate groups and return the full list of users that
 968       * directly or indirectly belong to the group.
 969       *
 970       * @param string $group the group name to search
 971       * @param string $memberattibute the attribute that holds the members of the group
 972       * @return array the list of users belonging to the group. If $group
 973       *         is not actually a group, returns array($group).
 974       */
 975      protected function ldap_explode_group($group, $memberattribute) {
 976          switch ($this->get_config('user_type')) {
 977              case 'ad':
 978                  // $group is already the distinguished name to search.
 979                  $dn = $group;
 980  
 981                  $result = ldap_read($this->ldapconnection, $dn, '(objectClass=*)', array('objectClass'));
 982                  $entry = ldap_first_entry($this->ldapconnection, $result);
 983                  $objectclass = ldap_get_values($this->ldapconnection, $entry, 'objectClass');
 984  
 985                  if (!in_array('group', $objectclass)) {
 986                      // Not a group, so return immediately.
 987                      return array($group);
 988                  }
 989  
 990                  $result = ldap_read($this->ldapconnection, $dn, '(objectClass=*)', array($memberattribute));
 991                  $entry = ldap_first_entry($this->ldapconnection, $result);
 992                  $members = @ldap_get_values($this->ldapconnection, $entry, $memberattribute); // Can be empty and throws a warning
 993                  if ($members['count'] == 0) {
 994                      // There are no members in this group, return nothing.
 995                      return array();
 996                  }
 997                  unset($members['count']);
 998  
 999                  $users = array();
1000                  foreach ($members as $member) {
1001                      $group_members = $this->ldap_explode_group($member, $memberattribute);
1002                      $users = array_merge($users, $group_members);
1003                  }
1004  
1005                  return ($users);
1006                  break;
1007              default:
1008                  error_log($this->errorlogtag.get_string('explodegroupusertypenotsupported', 'enrol_ldap',
1009                                                          $this->get_config('user_type_name')));
1010  
1011                  return array($group);
1012          }
1013      }
1014  
1015      /**
1016       * Will create the moodle course from the template
1017       * course_ext is an array as obtained from ldap -- flattened somewhat
1018       *
1019       * @param array $course_ext
1020       * @param progress_trace $trace
1021       * @return mixed false on error, id for the newly created course otherwise.
1022       */
1023      function create_course($course_ext, progress_trace $trace) {
1024          global $CFG, $DB;
1025  
1026          require_once("$CFG->dirroot/course/lib.php");
1027  
1028          // Override defaults with template course
1029          $template = false;
1030          if ($this->get_config('template')) {
1031              if ($template = $DB->get_record('course', array('shortname'=>$this->get_config('template')))) {
1032                  $template = fullclone(course_get_format($template)->get_course());
1033                  unset($template->id); // So we are clear to reinsert the record
1034                  unset($template->fullname);
1035                  unset($template->shortname);
1036                  unset($template->idnumber);
1037              }
1038          }
1039          if (!$template) {
1040              $courseconfig = get_config('moodlecourse');
1041              $template = new stdClass();
1042              $template->summary        = '';
1043              $template->summaryformat  = FORMAT_HTML;
1044              $template->format         = $courseconfig->format;
1045              $template->newsitems      = $courseconfig->newsitems;
1046              $template->showgrades     = $courseconfig->showgrades;
1047              $template->showreports    = $courseconfig->showreports;
1048              $template->maxbytes       = $courseconfig->maxbytes;
1049              $template->groupmode      = $courseconfig->groupmode;
1050              $template->groupmodeforce = $courseconfig->groupmodeforce;
1051              $template->visible        = $courseconfig->visible;
1052              $template->lang           = $courseconfig->lang;
1053              $template->enablecompletion = $courseconfig->enablecompletion;
1054          }
1055          $course = $template;
1056  
1057          $course->category = $this->get_config('category');
1058          if (!$DB->record_exists('course_categories', array('id'=>$this->get_config('category')))) {
1059              $categories = $DB->get_records('course_categories', array(), 'sortorder', 'id', 0, 1);
1060              $first = reset($categories);
1061              $course->category = $first->id;
1062          }
1063  
1064          // Override with required ext data
1065          $course->idnumber  = $course_ext[$this->get_config('course_idnumber')][0];
1066          $course->fullname  = $course_ext[$this->get_config('course_fullname')][0];
1067          $course->shortname = $course_ext[$this->get_config('course_shortname')][0];
1068          if (empty($course->idnumber) || empty($course->fullname) || empty($course->shortname)) {
1069              // We are in trouble!
1070              $trace->output(get_string('cannotcreatecourse', 'enrol_ldap').' '.var_export($course, true));
1071              return false;
1072          }
1073  
1074          $summary = $this->get_config('course_summary');
1075          if (!isset($summary) || empty($course_ext[$summary][0])) {
1076              $course->summary = '';
1077          } else {
1078              $course->summary = $course_ext[$this->get_config('course_summary')][0];
1079          }
1080  
1081          // Check if the shortname already exists if it does - skip course creation.
1082          if ($DB->record_exists('course', array('shortname' => $course->shortname))) {
1083              $trace->output(get_string('duplicateshortname', 'enrol_ldap', $course));
1084              return false;
1085          }
1086  
1087          $newcourse = create_course($course);
1088          return $newcourse->id;
1089      }
1090  
1091      /**
1092       * Will update a moodle course with new values from LDAP
1093       * A field will be updated only if it is marked to be updated
1094       * on sync in plugin settings
1095       *
1096       * @param object $course
1097       * @param array $externalcourse
1098       * @param progress_trace $trace
1099       * @return bool
1100       */
1101      protected function update_course($course, $externalcourse, progress_trace $trace) {
1102          global $CFG, $DB;
1103  
1104          $coursefields = array ('shortname', 'fullname', 'summary');
1105          static $shouldupdate;
1106  
1107          // Initialize $shouldupdate variable. Set to true if one or more fields are marked for update.
1108          if (!isset($shouldupdate)) {
1109              $shouldupdate = false;
1110              foreach ($coursefields as $field) {
1111                  $shouldupdate = $shouldupdate || $this->get_config('course_'.$field.'_updateonsync');
1112              }
1113          }
1114  
1115          // If we should not update return immediately.
1116          if (!$shouldupdate) {
1117              return false;
1118          }
1119  
1120          require_once("$CFG->dirroot/course/lib.php");
1121          $courseupdated = false;
1122          $updatedcourse = new stdClass();
1123          $updatedcourse->id = $course->id;
1124  
1125          // Update course fields if necessary.
1126          foreach ($coursefields as $field) {
1127              // If field is marked to be updated on sync && field data was changed update it.
1128              if ($this->get_config('course_'.$field.'_updateonsync')
1129                      && isset($externalcourse[$this->get_config('course_'.$field)][0])
1130                      && $course->{$field} != $externalcourse[$this->get_config('course_'.$field)][0]) {
1131                  $updatedcourse->{$field} = $externalcourse[$this->get_config('course_'.$field)][0];
1132                  $courseupdated = true;
1133              }
1134          }
1135  
1136          if (!$courseupdated) {
1137              $trace->output(get_string('courseupdateskipped', 'enrol_ldap', $course));
1138              return false;
1139          }
1140  
1141          // Do not allow empty fullname or shortname.
1142          if ((isset($updatedcourse->fullname) && empty($updatedcourse->fullname))
1143                  || (isset($updatedcourse->shortname) && empty($updatedcourse->shortname))) {
1144              // We are in trouble!
1145              $trace->output(get_string('cannotupdatecourse', 'enrol_ldap', $course));
1146              return false;
1147          }
1148  
1149          // Check if the shortname already exists if it does - skip course updating.
1150          if (isset($updatedcourse->shortname)
1151                  && $DB->record_exists('course', array('shortname' => $updatedcourse->shortname))) {
1152              $trace->output(get_string('cannotupdatecourse_duplicateshortname', 'enrol_ldap', $course));
1153              return false;
1154          }
1155  
1156          // Finally - update course in DB.
1157          update_course($updatedcourse);
1158          $trace->output(get_string('courseupdated', 'enrol_ldap', $course));
1159  
1160          return true;
1161      }
1162  
1163      /**
1164       * Automatic enrol sync executed during restore.
1165       * Useful for automatic sync by course->idnumber or course category.
1166       * @param stdClass $course course record
1167       */
1168      public function restore_sync_course($course) {
1169          // TODO: this can not work because restore always nukes the course->idnumber, do not ask me why (MDL-37312)
1170          // NOTE: for now restore does not do any real logging yet, let's do the same here...
1171          $trace = new error_log_progress_trace();
1172          $this->sync_enrolments($trace, $course->id);
1173      }
1174  
1175      /**
1176       * Restore instance and map settings.
1177       *
1178       * @param restore_enrolments_structure_step $step
1179       * @param stdClass $data
1180       * @param stdClass $course
1181       * @param int $oldid
1182       */
1183      public function restore_instance(restore_enrolments_structure_step $step, stdClass $data, $course, $oldid) {
1184          global $DB;
1185          // There is only 1 ldap enrol instance per course.
1186          if ($instances = $DB->get_records('enrol', array('courseid'=>$data->courseid, 'enrol'=>'ldap'), 'id')) {
1187              $instance = reset($instances);
1188              $instanceid = $instance->id;
1189          } else {
1190              $instanceid = $this->add_instance($course, (array)$data);
1191          }
1192          $step->set_mapping('enrol', $oldid, $instanceid);
1193      }
1194  
1195      /**
1196       * Restore user enrolment.
1197       *
1198       * @param restore_enrolments_structure_step $step
1199       * @param stdClass $data
1200       * @param stdClass $instance
1201       * @param int $oldinstancestatus
1202       * @param int $userid
1203       */
1204      public function restore_user_enrolment(restore_enrolments_structure_step $step, $data, $instance, $userid, $oldinstancestatus) {
1205          global $DB;
1206  
1207          if ($this->get_config('unenrolaction') == ENROL_EXT_REMOVED_UNENROL) {
1208              // Enrolments were already synchronised in restore_instance(), we do not want any suspended leftovers.
1209  
1210          } else if ($this->get_config('unenrolaction') == ENROL_EXT_REMOVED_KEEP) {
1211              if (!$DB->record_exists('user_enrolments', array('enrolid'=>$instance->id, 'userid'=>$userid))) {
1212                  $this->enrol_user($instance, $userid, null, 0, 0, $data->status);
1213              }
1214  
1215          } else {
1216              if (!$DB->record_exists('user_enrolments', array('enrolid'=>$instance->id, 'userid'=>$userid))) {
1217                  $this->enrol_user($instance, $userid, null, 0, 0, ENROL_USER_SUSPENDED);
1218              }
1219          }
1220      }
1221  
1222      /**
1223       * Restore role assignment.
1224       *
1225       * @param stdClass $instance
1226       * @param int $roleid
1227       * @param int $userid
1228       * @param int $contextid
1229       */
1230      public function restore_role_assignment($instance, $roleid, $userid, $contextid) {
1231          global $DB;
1232  
1233          if ($this->get_config('unenrolaction') == ENROL_EXT_REMOVED_UNENROL or $this->get_config('unenrolaction') == ENROL_EXT_REMOVED_SUSPENDNOROLES) {
1234              // Skip any roles restore, they should be already synced automatically.
1235              return;
1236          }
1237  
1238          // Just restore every role.
1239          if ($DB->record_exists('user_enrolments', array('enrolid'=>$instance->id, 'userid'=>$userid))) {
1240              role_assign($roleid, $userid, $contextid, 'enrol_'.$instance->enrol, $instance->id);
1241          }
1242      }
1243  }