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.

Differences Between: [Versions 311 and 401] [Versions 311 and 402] [Versions 311 and 403]

   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   * Local stuff for meta course enrolment plugin.
  19   *
  20   * @package    enrol_meta
  21   * @copyright  2010 Petr Skoda {@link http://skodak.org}
  22   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  23   */
  24  
  25  defined('MOODLE_INTERNAL') || die();
  26  
  27  
  28  /**
  29   * Event handler for meta enrolment plugin.
  30   *
  31   * We try to keep everything in sync via listening to events,
  32   * it may fail sometimes, so we always do a full sync in cron too.
  33   */
  34  class enrol_meta_handler {
  35  
  36      /**
  37       * Synchronise meta enrolments of this user in this course
  38       * @static
  39       * @param int $courseid
  40       * @param int $userid
  41       * @return void
  42       */
  43      protected static function sync_course_instances($courseid, $userid) {
  44          global $DB;
  45  
  46          static $preventrecursion = false;
  47  
  48          // does anything want to sync with this parent?
  49          if (!$enrols = $DB->get_records('enrol', array('customint1'=>$courseid, 'enrol'=>'meta'), 'id ASC')) {
  50              return;
  51          }
  52  
  53          if ($preventrecursion) {
  54              return;
  55          }
  56  
  57          $preventrecursion = true;
  58  
  59          try {
  60              foreach ($enrols as $enrol) {
  61                  self::sync_with_parent_course($enrol, $userid);
  62              }
  63          } catch (Exception $e) {
  64              $preventrecursion = false;
  65              throw $e;
  66          }
  67  
  68          $preventrecursion = false;
  69      }
  70  
  71      /**
  72       * Synchronise user enrolments in given instance as fast as possible.
  73       *
  74       * All roles are removed if the meta plugin disabled.
  75       *
  76       * @static
  77       * @param stdClass $instance
  78       * @param int $userid
  79       * @return void
  80       */
  81      protected static function sync_with_parent_course(stdClass $instance, $userid) {
  82          global $DB, $CFG;
  83          require_once($CFG->dirroot . '/group/lib.php');
  84  
  85          $plugin = enrol_get_plugin('meta');
  86  
  87          if ($instance->customint1 == $instance->courseid) {
  88              // can not sync with self!!!
  89              return;
  90          }
  91  
  92          $context = context_course::instance($instance->courseid);
  93  
  94          // list of enrolments in parent course (we ignore meta enrols in parents completely)
  95          list($enabled, $params) = $DB->get_in_or_equal(explode(',', $CFG->enrol_plugins_enabled), SQL_PARAMS_NAMED, 'e');
  96          $params['userid'] = $userid;
  97          $params['parentcourse'] = $instance->customint1;
  98          $sql = "SELECT ue.*, e.status AS enrolstatus
  99                    FROM {user_enrolments} ue
 100                    JOIN {enrol} e ON (e.id = ue.enrolid AND e.enrol <> 'meta' AND e.courseid = :parentcourse AND e.enrol $enabled)
 101                   WHERE ue.userid = :userid";
 102          $parentues = $DB->get_records_sql($sql, $params);
 103          // current enrolments for this instance
 104          $ue = $DB->get_record('user_enrolments', array('enrolid'=>$instance->id, 'userid'=>$userid));
 105  
 106          // first deal with users that are not enrolled in parent
 107          if (empty($parentues)) {
 108              self::user_not_supposed_to_be_here($instance, $ue, $context, $plugin);
 109              return;
 110          }
 111  
 112          if (!$parentcontext = context_course::instance($instance->customint1, IGNORE_MISSING)) {
 113              // Weird, we should not get here.
 114              return;
 115          }
 116  
 117          $skiproles = $plugin->get_config('nosyncroleids', '');
 118          $skiproles = empty($skiproles) ? array() : explode(',', $skiproles);
 119          $syncall   = $plugin->get_config('syncall', 1);
 120  
 121          // roles in parent course (meta enrols must be ignored!)
 122          $parentroles = array();
 123          list($ignoreroles, $params) = $DB->get_in_or_equal($skiproles, SQL_PARAMS_NAMED, 'ri', false, -1);
 124          $params['contextid'] = $parentcontext->id;
 125          $params['userid'] = $userid;
 126          $select = "contextid = :contextid AND userid = :userid AND component <> 'enrol_meta' AND roleid $ignoreroles";
 127          foreach($DB->get_records_select('role_assignments', $select, $params) as $ra) {
 128              $parentroles[$ra->roleid] = $ra->roleid;
 129          }
 130  
 131          // roles from this instance
 132          $roles = array();
 133          $ras = $DB->get_records('role_assignments', array('contextid'=>$context->id, 'userid'=>$userid, 'component'=>'enrol_meta', 'itemid'=>$instance->id));
 134          foreach($ras as $ra) {
 135              $roles[$ra->roleid] = $ra->roleid;
 136          }
 137          unset($ras);
 138  
 139          // do we want users without roles?
 140          if (!$syncall and empty($parentroles)) {
 141              self::user_not_supposed_to_be_here($instance, $ue, $context, $plugin);
 142              return;
 143          }
 144  
 145          // Is parent enrol active? Find minimum timestart and maximum timeend of all active enrolments.
 146          $parentstatus = ENROL_USER_SUSPENDED;
 147          $parenttimeend = null;
 148          $parenttimestart = null;
 149          foreach ($parentues as $pue) {
 150              if ($pue->status == ENROL_USER_ACTIVE && $pue->enrolstatus == ENROL_INSTANCE_ENABLED) {
 151                  $parentstatus = ENROL_USER_ACTIVE;
 152                  if ($parenttimeend === null || $pue->timeend == 0 || ($parenttimeend && $parenttimeend < $pue->timeend)) {
 153                      $parenttimeend = $pue->timeend;
 154                  }
 155                  if ($parenttimestart === null || $parenttimestart > $pue->timestart) {
 156                      $parenttimestart = $pue->timestart;
 157                  }
 158              }
 159          }
 160  
 161          // Enrol user if not enrolled yet or fix status/timestart/timeend. Use the minimum timestart and maximum timeend found above.
 162          if ($ue) {
 163              if ($parentstatus != $ue->status ||
 164                      ($parentstatus == ENROL_USER_ACTIVE && ($parenttimestart != $ue->timestart || $parenttimeend != $ue->timeend))) {
 165                  $plugin->update_user_enrol($instance, $userid, $parentstatus, $parenttimestart, $parenttimeend);
 166                  $ue->status = $parentstatus;
 167                  $ue->timestart = $parenttimestart;
 168                  $ue->timeend = $parenttimeend;
 169              }
 170          } else {
 171              $plugin->enrol_user($instance, $userid, NULL, (int)$parenttimestart, (int)$parenttimeend, $parentstatus);
 172              $ue = new stdClass();
 173              $ue->userid = $userid;
 174              $ue->enrolid = $instance->id;
 175              $ue->status = $parentstatus;
 176              if ($instance->customint2) {
 177                  groups_add_member($instance->customint2, $userid, 'enrol_meta', $instance->id);
 178              }
 179          }
 180  
 181          $unenrolaction = $plugin->get_config('unenrolaction', ENROL_EXT_REMOVED_SUSPENDNOROLES);
 182  
 183          // Only active users in enabled instances are supposed to have roles (we can reassign the roles any time later).
 184          if ($ue->status != ENROL_USER_ACTIVE or $instance->status != ENROL_INSTANCE_ENABLED or
 185                  ($parenttimeend and $parenttimeend < time()) or ($parenttimestart > time())) {
 186              if ($unenrolaction == ENROL_EXT_REMOVED_SUSPEND) {
 187                  // Always keep the roles.
 188              } else if ($roles) {
 189                  // This will only unassign roles that were assigned in this enrolment method, leaving all manual role assignments intact.
 190                  role_unassign_all(array('userid'=>$userid, 'contextid'=>$context->id, 'component'=>'enrol_meta', 'itemid'=>$instance->id));
 191              }
 192              return;
 193          }
 194  
 195          // add new roles
 196          foreach ($parentroles as $rid) {
 197              if (!isset($roles[$rid])) {
 198                  role_assign($rid, $userid, $context->id, 'enrol_meta', $instance->id);
 199              }
 200          }
 201  
 202          if ($unenrolaction == ENROL_EXT_REMOVED_SUSPEND) {
 203              // Always keep the roles.
 204              return;
 205          }
 206  
 207          // remove roles
 208          foreach ($roles as $rid) {
 209              if (!isset($parentroles[$rid])) {
 210                  role_unassign($rid, $userid, $context->id, 'enrol_meta', $instance->id);
 211              }
 212          }
 213      }
 214  
 215      /**
 216       * Deal with users that are not supposed to be enrolled via this instance
 217       * @static
 218       * @param stdClass $instance
 219       * @param stdClass $ue
 220       * @param context_course $context
 221       * @param enrol_meta $plugin
 222       * @return void
 223       */
 224      protected static function user_not_supposed_to_be_here($instance, $ue, context_course $context, $plugin) {
 225          if (!$ue) {
 226              // Not enrolled yet - simple!
 227              return;
 228          }
 229  
 230          $userid = $ue->userid;
 231          $unenrolaction = $plugin->get_config('unenrolaction', ENROL_EXT_REMOVED_SUSPENDNOROLES);
 232  
 233          if ($unenrolaction == ENROL_EXT_REMOVED_UNENROL) {
 234              // Purges grades, group membership, preferences, etc. - admins were warned!
 235              $plugin->unenrol_user($instance, $userid);
 236  
 237          } else if ($unenrolaction == ENROL_EXT_REMOVED_SUSPEND) {
 238              if ($ue->status != ENROL_USER_SUSPENDED) {
 239                  $plugin->update_user_enrol($instance, $userid, ENROL_USER_SUSPENDED);
 240              }
 241  
 242          } else if ($unenrolaction == ENROL_EXT_REMOVED_SUSPENDNOROLES) {
 243              if ($ue->status != ENROL_USER_SUSPENDED) {
 244                  $plugin->update_user_enrol($instance, $userid, ENROL_USER_SUSPENDED);
 245              }
 246              role_unassign_all(array('userid'=>$userid, 'contextid'=>$context->id, 'component'=>'enrol_meta', 'itemid'=>$instance->id));
 247  
 248          } else {
 249              debugging('Unknown unenrol action '.$unenrolaction);
 250          }
 251      }
 252  }
 253  
 254  /**
 255   * Sync all meta course links.
 256   *
 257   * @param int $courseid one course, empty mean all
 258   * @param bool $verbose verbose CLI output
 259   * @return int 0 means ok, 1 means error, 2 means plugin disabled
 260   */
 261  function enrol_meta_sync($courseid = NULL, $verbose = false) {
 262      global $CFG, $DB;
 263      require_once("{$CFG->dirroot}/group/lib.php");
 264  
 265      // purge all roles if meta sync disabled, those can be recreated later here in cron
 266      if (!enrol_is_enabled('meta')) {
 267          if ($verbose) {
 268              mtrace('Meta sync plugin is disabled, unassigning all plugin roles and stopping.');
 269          }
 270          role_unassign_all(array('component'=>'enrol_meta'));
 271          return 2;
 272      }
 273  
 274      // unfortunately this may take a long time, execution can be interrupted safely
 275      core_php_time_limit::raise();
 276      raise_memory_limit(MEMORY_HUGE);
 277  
 278      if ($verbose) {
 279          mtrace('Starting user enrolment synchronisation...');
 280      }
 281  
 282      $instances = array(); // cache instances
 283  
 284      $meta = enrol_get_plugin('meta');
 285  
 286      $unenrolaction = $meta->get_config('unenrolaction', ENROL_EXT_REMOVED_SUSPENDNOROLES);
 287      $skiproles     = $meta->get_config('nosyncroleids', '');
 288      $skiproles     = empty($skiproles) ? array() : explode(',', $skiproles);
 289      $syncall       = $meta->get_config('syncall', 1);
 290  
 291      $allroles = get_all_roles();
 292  
 293  
 294      // Iterate through all not enrolled yet users. For each active enrolment of each user find the minimum
 295      // enrolment startdate and maximum enrolment enddate.
 296      // This SQL relies on the fact that ENROL_USER_ACTIVE < ENROL_USER_SUSPENDED
 297      // and ENROL_INSTANCE_ENABLED < ENROL_INSTANCE_DISABLED. Condition "pue.status + pe.status = 0" means
 298      // that enrolment is active. When MIN(pue.status + pe.status)=0 it means there exists an active
 299      // enrolment.
 300      $onecourse = $courseid ? "AND e.courseid = :courseid" : "";
 301      list($enabled, $params) = $DB->get_in_or_equal(explode(',', $CFG->enrol_plugins_enabled), SQL_PARAMS_NAMED, 'e');
 302      $params['courseid'] = $courseid;
 303      $sql = "SELECT pue.userid, e.id AS enrolid, MIN(pue.status + pe.status) AS status,
 304                        MIN(CASE WHEN (pue.status + pe.status = 0) THEN pue.timestart ELSE 9999999999 END) AS timestart,
 305                        MAX(CASE WHEN (pue.status + pe.status = 0) THEN
 306                                  (CASE WHEN pue.timeend = 0 THEN 9999999999 ELSE pue.timeend END)
 307                                  ELSE 0 END) AS timeend
 308                FROM {user_enrolments} pue
 309                JOIN {enrol} pe ON (pe.id = pue.enrolid AND pe.enrol <> 'meta' AND pe.enrol $enabled)
 310                JOIN {enrol} e ON (e.customint1 = pe.courseid AND e.enrol = 'meta' AND e.status = :enrolstatus $onecourse)
 311                JOIN {user} u ON (u.id = pue.userid AND u.deleted = 0)
 312           LEFT JOIN {user_enrolments} ue ON (ue.enrolid = e.id AND ue.userid = pue.userid)
 313               WHERE ue.id IS NULL
 314               GROUP BY pue.userid, e.id";
 315      $params['enrolstatus'] = ENROL_INSTANCE_ENABLED;
 316  
 317      $rs = $DB->get_recordset_sql($sql, $params);
 318      foreach($rs as $ue) {
 319          if (!isset($instances[$ue->enrolid])) {
 320              $instances[$ue->enrolid] = $DB->get_record('enrol', array('id'=>$ue->enrolid));
 321          }
 322          $instance = $instances[$ue->enrolid];
 323  
 324          if (!$syncall) {
 325              // this may be slow if very many users are ignored in sync
 326              $parentcontext = context_course::instance($instance->customint1);
 327              list($ignoreroles, $params) = $DB->get_in_or_equal($skiproles, SQL_PARAMS_NAMED, 'ri', false, -1);
 328              $params['contextid'] = $parentcontext->id;
 329              $params['userid'] = $ue->userid;
 330              $select = "contextid = :contextid AND userid = :userid AND component <> 'enrol_meta' AND roleid $ignoreroles";
 331              if (!$DB->record_exists_select('role_assignments', $select, $params)) {
 332                  // bad luck, this user does not have any role we want in parent course
 333                  if ($verbose) {
 334                      mtrace("  skipping enrolling: $ue->userid ==> $instance->courseid (user without role)");
 335                  }
 336                  continue;
 337              }
 338          }
 339  
 340          // So now we have aggregated values that we will use for the meta enrolment status, timeend and timestart.
 341          // Again, we use the fact that active=0 and disabled/suspended=1. Only when MIN(pue.status + pe.status)=0 the enrolment is active:
 342          $ue->status = ($ue->status == ENROL_USER_ACTIVE + ENROL_INSTANCE_ENABLED) ? ENROL_USER_ACTIVE : ENROL_USER_SUSPENDED;
 343          // Timeend 9999999999 was used instead of 0 in the "MAX()" function:
 344          $ue->timeend = ($ue->timeend == 9999999999) ? 0 : (int)$ue->timeend;
 345          // Timestart 9999999999 is only possible when there are no active enrolments:
 346          $ue->timestart = ($ue->timestart == 9999999999) ? 0 : (int)$ue->timestart;
 347  
 348          $meta->enrol_user($instance, $ue->userid, null, $ue->timestart, $ue->timeend, $ue->status);
 349          if ($instance->customint2) {
 350              groups_add_member($instance->customint2, $ue->userid, 'enrol_meta', $instance->id);
 351          }
 352          if ($verbose) {
 353              mtrace("  enrolling: $ue->userid ==> $instance->courseid");
 354          }
 355      }
 356      $rs->close();
 357  
 358  
 359      // unenrol as necessary - ignore enabled flag, we want to get rid of existing enrols in any case
 360      $onecourse = $courseid ? "AND e.courseid = :courseid" : "";
 361      list($enabled, $params) = $DB->get_in_or_equal(explode(',', $CFG->enrol_plugins_enabled), SQL_PARAMS_NAMED, 'e');
 362      $params['courseid'] = $courseid;
 363      $sql = "SELECT ue.*
 364                FROM {user_enrolments} ue
 365                JOIN {enrol} e ON (e.id = ue.enrolid AND e.enrol = 'meta' $onecourse)
 366           LEFT JOIN ({user_enrolments} xpue
 367                        JOIN {enrol} xpe ON (xpe.id = xpue.enrolid AND xpe.enrol <> 'meta' AND xpe.enrol $enabled)
 368                     ) ON (xpe.courseid = e.customint1 AND xpue.userid = ue.userid)
 369               WHERE xpue.userid IS NULL";
 370      $rs = $DB->get_recordset_sql($sql, $params);
 371      foreach($rs as $ue) {
 372          if (!isset($instances[$ue->enrolid])) {
 373              $instances[$ue->enrolid] = $DB->get_record('enrol', array('id'=>$ue->enrolid));
 374          }
 375          $instance = $instances[$ue->enrolid];
 376  
 377          if ($unenrolaction == ENROL_EXT_REMOVED_UNENROL) {
 378              $meta->unenrol_user($instance, $ue->userid);
 379              if ($verbose) {
 380                  mtrace("  unenrolling: $ue->userid ==> $instance->courseid");
 381              }
 382  
 383          } else if ($unenrolaction == ENROL_EXT_REMOVED_SUSPEND) {
 384              if ($ue->status != ENROL_USER_SUSPENDED) {
 385                  $meta->update_user_enrol($instance, $ue->userid, ENROL_USER_SUSPENDED);
 386                  if ($verbose) {
 387                      mtrace("  suspending: $ue->userid ==> $instance->courseid");
 388                  }
 389              }
 390  
 391          } else if ($unenrolaction == ENROL_EXT_REMOVED_SUSPENDNOROLES) {
 392              if ($ue->status != ENROL_USER_SUSPENDED) {
 393                  $meta->update_user_enrol($instance, $ue->userid, ENROL_USER_SUSPENDED);
 394                  $context = context_course::instance($instance->courseid);
 395                  role_unassign_all(array('userid'=>$ue->userid, 'contextid'=>$context->id, 'component'=>'enrol_meta', 'itemid'=>$instance->id));
 396                  if ($verbose) {
 397                      mtrace("  suspending and removing all roles: $ue->userid ==> $instance->courseid");
 398                  }
 399              }
 400          }
 401      }
 402      $rs->close();
 403  
 404  
 405      // Update status - meta enrols are ignored to avoid recursion.
 406      // Note the trick here is that the active enrolment and instance constants have value 0.
 407      $onecourse = $courseid ? "AND e.courseid = :courseid" : "";
 408      list($enabled, $params) = $DB->get_in_or_equal(explode(',', $CFG->enrol_plugins_enabled), SQL_PARAMS_NAMED, 'e');
 409      $params['courseid'] = $courseid;
 410      // The query builds a a list of all the non-meta enrolments that are on courses (the children) that are linked to by a meta
 411      // enrolment, it then groups them by the course that linked to them (the parents).
 412      //
 413      // It will only return results where the there is a difference between the status of the parent and the lowest status
 414      // of the children (remember that 0 is active, any other status is some form of inactive), or the time the earliest non-zero
 415      // start time of a child is different to the parent, or the longest effective end date has changed.
 416      //
 417      // The last two case statements in the HAVING clause are designed to ignore any inactive child records when calculating
 418      // the start and end time.
 419      $sql = "SELECT ue.userid, ue.enrolid,
 420                     MIN(xpue.status + xpe.status) AS pstatus,
 421                     MIN(CASE WHEN (xpue.status + xpe.status = 0) THEN xpue.timestart ELSE 9999999999 END) AS ptimestart,
 422                     MAX(CASE WHEN (xpue.status + xpe.status = 0) THEN
 423                                   (CASE WHEN xpue.timeend = 0 THEN 9999999999 ELSE xpue.timeend END)
 424                              ELSE 0 END) AS ptimeend
 425                FROM {user_enrolments} ue
 426                JOIN {enrol} e ON (e.id = ue.enrolid AND e.enrol = 'meta' $onecourse)
 427                JOIN {user_enrolments} xpue ON (xpue.userid = ue.userid)
 428                JOIN {enrol} xpe ON (xpe.id = xpue.enrolid AND xpe.enrol <> 'meta'
 429                     AND xpe.enrol $enabled AND xpe.courseid = e.customint1)
 430            GROUP BY ue.userid, ue.enrolid
 431              HAVING (MIN(xpue.status + xpe.status) = 0 AND MIN(ue.status) > 0)
 432                     OR (MIN(xpue.status + xpe.status) > 0 AND MIN(ue.status) = 0)
 433                     OR ((CASE WHEN
 434                                    MIN(CASE WHEN (xpue.status + xpe.status = 0) THEN xpue.timestart ELSE 9999999999 END) = 9999999999
 435                               THEN 0
 436                               ELSE
 437                                    MIN(CASE WHEN (xpue.status + xpe.status = 0) THEN xpue.timestart ELSE 9999999999 END)
 438                                END) <> MIN(ue.timestart))
 439                     OR ((CASE
 440                           WHEN MAX(CASE WHEN (xpue.status + xpe.status = 0)
 441                                         THEN (CASE WHEN xpue.timeend = 0 THEN 9999999999 ELSE xpue.timeend END)
 442                                         ELSE 0 END) = 9999999999
 443                           THEN 0 ELSE MAX(CASE WHEN (xpue.status + xpe.status = 0)
 444                                                THEN (CASE WHEN xpue.timeend = 0 THEN 9999999999 ELSE xpue.timeend END)
 445                                                ELSE 0 END)
 446                            END) <> MAX(ue.timeend))";
 447      $rs = $DB->get_recordset_sql($sql, $params);
 448      foreach($rs as $ue) {
 449          if (!isset($instances[$ue->enrolid])) {
 450              $instances[$ue->enrolid] = $DB->get_record('enrol', array('id'=>$ue->enrolid));
 451          }
 452          $instance = $instances[$ue->enrolid];
 453          $ue->pstatus = ($ue->pstatus == ENROL_USER_ACTIVE + ENROL_INSTANCE_ENABLED) ? ENROL_USER_ACTIVE : ENROL_USER_SUSPENDED;
 454          $ue->ptimeend = ($ue->ptimeend == 9999999999) ? 0 : (int)$ue->ptimeend;
 455          $ue->ptimestart = ($ue->ptimestart == 9999999999) ? 0 : (int)$ue->ptimestart;
 456  
 457          if ($ue->pstatus == ENROL_USER_ACTIVE and (!$ue->ptimeend || $ue->ptimeend > time())
 458                  and !$syncall and $unenrolaction != ENROL_EXT_REMOVED_UNENROL) {
 459              // this may be slow if very many users are ignored in sync
 460              $parentcontext = context_course::instance($instance->customint1);
 461              list($ignoreroles, $params) = $DB->get_in_or_equal($skiproles, SQL_PARAMS_NAMED, 'ri', false, -1);
 462              $params['contextid'] = $parentcontext->id;
 463              $params['userid'] = $ue->userid;
 464              $select = "contextid = :contextid AND userid = :userid AND component <> 'enrol_meta' AND roleid $ignoreroles";
 465              if (!$DB->record_exists_select('role_assignments', $select, $params)) {
 466                  // bad luck, this user does not have any role we want in parent course
 467                  if ($verbose) {
 468                      mtrace("  skipping unsuspending: $ue->userid ==> $instance->courseid (user without role)");
 469                  }
 470                  continue;
 471              }
 472          }
 473  
 474          $meta->update_user_enrol($instance, $ue->userid, $ue->pstatus, $ue->ptimestart, $ue->ptimeend);
 475          if ($verbose) {
 476              if ($ue->pstatus == ENROL_USER_ACTIVE) {
 477                  mtrace("  unsuspending: $ue->userid ==> $instance->courseid");
 478              } else {
 479                  mtrace("  suspending: $ue->userid ==> $instance->courseid");
 480              }
 481          }
 482      }
 483      $rs->close();
 484  
 485  
 486      // now assign all necessary roles
 487      $enabled = explode(',', $CFG->enrol_plugins_enabled);
 488      foreach($enabled as $k=>$v) {
 489          if ($v === 'meta') {
 490              continue; // no meta sync of meta roles
 491          }
 492          $enabled[$k] = 'enrol_'.$v;
 493      }
 494      $enabled[] = ''; // manual assignments are replicated too
 495  
 496      $onecourse = $courseid ? "AND e.courseid = :courseid" : "";
 497      list($enabled, $params) = $DB->get_in_or_equal($enabled, SQL_PARAMS_NAMED, 'e');
 498      $params['coursecontext'] = CONTEXT_COURSE;
 499      $params['courseid'] = $courseid;
 500      $params['activeuser'] = ENROL_USER_ACTIVE;
 501      $params['enabledinstance'] = ENROL_INSTANCE_ENABLED;
 502      $sql = "SELECT DISTINCT pra.roleid, pra.userid, c.id AS contextid, e.id AS enrolid, e.courseid
 503                FROM {role_assignments} pra
 504                JOIN {user} u ON (u.id = pra.userid AND u.deleted = 0)
 505                JOIN {context} pc ON (pc.id = pra.contextid AND pc.contextlevel = :coursecontext AND pra.component $enabled)
 506                JOIN {enrol} e ON (e.customint1 = pc.instanceid AND e.enrol = 'meta' $onecourse AND e.status = :enabledinstance)
 507                JOIN {user_enrolments} ue ON (ue.enrolid = e.id AND ue.userid = u.id AND ue.status = :activeuser)
 508                JOIN {context} c ON (c.contextlevel = pc.contextlevel AND c.instanceid = e.courseid)
 509           LEFT JOIN {role_assignments} ra ON (ra.contextid = c.id AND ra.userid = pra.userid AND ra.roleid = pra.roleid AND ra.itemid = e.id AND ra.component = 'enrol_meta')
 510               WHERE ra.id IS NULL";
 511  
 512      if ($ignored = $meta->get_config('nosyncroleids')) {
 513          list($notignored, $xparams) = $DB->get_in_or_equal(explode(',', $ignored), SQL_PARAMS_NAMED, 'ig', false);
 514          $params = array_merge($params, $xparams);
 515          $sql = "$sql AND pra.roleid $notignored";
 516      }
 517  
 518      $rs = $DB->get_recordset_sql($sql, $params);
 519      foreach($rs as $ra) {
 520          role_assign($ra->roleid, $ra->userid, $ra->contextid, 'enrol_meta', $ra->enrolid);
 521          if ($verbose) {
 522              mtrace("  assigning role: $ra->userid ==> $ra->courseid as ".$allroles[$ra->roleid]->shortname);
 523          }
 524      }
 525      $rs->close();
 526  
 527  
 528      // remove unwanted roles - include ignored roles and disabled plugins too
 529      $onecourse = $courseid ? "AND e.courseid = :courseid" : "";
 530      $params = array();
 531      $params['coursecontext'] = CONTEXT_COURSE;
 532      $params['courseid'] = $courseid;
 533      $params['activeuser'] = ENROL_USER_ACTIVE;
 534      $params['enabledinstance'] = ENROL_INSTANCE_ENABLED;
 535      if ($ignored = $meta->get_config('nosyncroleids')) {
 536          list($notignored, $xparams) = $DB->get_in_or_equal(explode(',', $ignored), SQL_PARAMS_NAMED, 'ig', false);
 537          $params = array_merge($params, $xparams);
 538          $notignored = "AND pra.roleid $notignored";
 539      } else {
 540          $notignored = "";
 541      }
 542  
 543      $sql = "SELECT ra.roleid, ra.userid, ra.contextid, ra.itemid, e.courseid
 544                FROM {role_assignments} ra
 545                JOIN {enrol} e ON (e.id = ra.itemid AND ra.component = 'enrol_meta' AND e.enrol = 'meta' $onecourse)
 546                JOIN {context} pc ON (pc.instanceid = e.customint1 AND pc.contextlevel = :coursecontext)
 547           LEFT JOIN {role_assignments} pra ON (pra.contextid = pc.id AND pra.userid = ra.userid AND pra.roleid = ra.roleid AND pra.component <> 'enrol_meta' $notignored)
 548           LEFT JOIN {user_enrolments} ue ON (ue.enrolid = e.id AND ue.userid = ra.userid AND ue.status = :activeuser)
 549               WHERE pra.id IS NULL OR ue.id IS NULL OR e.status <> :enabledinstance";
 550  
 551      if ($unenrolaction != ENROL_EXT_REMOVED_SUSPEND) {
 552          $rs = $DB->get_recordset_sql($sql, $params);
 553          foreach($rs as $ra) {
 554              role_unassign($ra->roleid, $ra->userid, $ra->contextid, 'enrol_meta', $ra->itemid);
 555              if ($verbose) {
 556                  mtrace("  unassigning role: $ra->userid ==> $ra->courseid as ".$allroles[$ra->roleid]->shortname);
 557              }
 558          }
 559          $rs->close();
 560      }
 561  
 562  
 563      // kick out or suspend users without synced roles if syncall disabled
 564      if (!$syncall) {
 565          if ($unenrolaction == ENROL_EXT_REMOVED_UNENROL) {
 566              $onecourse = $courseid ? "AND e.courseid = :courseid" : "";
 567              $params = array();
 568              $params['coursecontext'] = CONTEXT_COURSE;
 569              $params['courseid'] = $courseid;
 570              $sql = "SELECT ue.userid, ue.enrolid
 571                        FROM {user_enrolments} ue
 572                        JOIN {enrol} e ON (e.id = ue.enrolid AND e.enrol = 'meta' $onecourse)
 573                        JOIN {context} c ON (e.courseid = c.instanceid AND c.contextlevel = :coursecontext)
 574                   LEFT JOIN {role_assignments} ra ON (ra.contextid = c.id AND ra.itemid = e.id AND ra.userid = ue.userid)
 575                       WHERE ra.id IS NULL";
 576              $ues = $DB->get_recordset_sql($sql, $params);
 577              foreach($ues as $ue) {
 578                  if (!isset($instances[$ue->enrolid])) {
 579                      $instances[$ue->enrolid] = $DB->get_record('enrol', array('id'=>$ue->enrolid));
 580                  }
 581                  $instance = $instances[$ue->enrolid];
 582                  $meta->unenrol_user($instance, $ue->userid);
 583                  if ($verbose) {
 584                      mtrace("  unenrolling: $ue->userid ==> $instance->courseid (user without role)");
 585                  }
 586              }
 587              $ues->close();
 588  
 589          } else {
 590              // just suspend the users
 591              $onecourse = $courseid ? "AND e.courseid = :courseid" : "";
 592              $params = array();
 593              $params['coursecontext'] = CONTEXT_COURSE;
 594              $params['courseid'] = $courseid;
 595              $params['active'] = ENROL_USER_ACTIVE;
 596              $sql = "SELECT ue.userid, ue.enrolid
 597                        FROM {user_enrolments} ue
 598                        JOIN {enrol} e ON (e.id = ue.enrolid AND e.enrol = 'meta' $onecourse)
 599                        JOIN {context} c ON (e.courseid = c.instanceid AND c.contextlevel = :coursecontext)
 600                   LEFT JOIN {role_assignments} ra ON (ra.contextid = c.id AND ra.itemid = e.id AND ra.userid = ue.userid)
 601                       WHERE ra.id IS NULL AND ue.status = :active";
 602              $ues = $DB->get_recordset_sql($sql, $params);
 603              foreach($ues as $ue) {
 604                  if (!isset($instances[$ue->enrolid])) {
 605                      $instances[$ue->enrolid] = $DB->get_record('enrol', array('id'=>$ue->enrolid));
 606                  }
 607                  $instance = $instances[$ue->enrolid];
 608                  $meta->update_user_enrol($instance, $ue->userid, ENROL_USER_SUSPENDED);
 609                  if ($verbose) {
 610                      mtrace("  suspending: $ue->userid ==> $instance->courseid (user without role)");
 611                  }
 612              }
 613              $ues->close();
 614          }
 615      }
 616  
 617      // Finally sync groups.
 618      $affectedusers = groups_sync_with_enrolment('meta', $courseid);
 619      if ($verbose) {
 620          foreach ($affectedusers['removed'] as $gm) {
 621              mtrace("removing user from group: $gm->userid ==> $gm->courseid - $gm->groupname", 1);
 622          }
 623          foreach ($affectedusers['added'] as $ue) {
 624              mtrace("adding user to group: $ue->userid ==> $ue->courseid - $ue->groupname", 1);
 625          }
 626      }
 627  
 628      if ($verbose) {
 629          mtrace('...user enrolment synchronisation finished.');
 630      }
 631  
 632      return 0;
 633  }
 634  
 635  /**
 636   * Create a new group with the course's name.
 637   *
 638   * @param int $courseid
 639   * @param int $linkedcourseid
 640   * @return int $groupid Group ID for this cohort.
 641   */
 642  function enrol_meta_create_new_group($courseid, $linkedcourseid) {
 643      global $DB, $CFG;
 644  
 645      require_once($CFG->dirroot.'/group/lib.php');
 646  
 647      $coursename = $DB->get_field('course', 'fullname', array('id' => $linkedcourseid), MUST_EXIST);
 648      $a = new stdClass();
 649      $a->name = $coursename;
 650      $a->increment = '';
 651      $inc = 1;
 652      $groupname = trim(get_string('defaultgroupnametext', 'enrol_meta', $a));
 653      // Check to see if the group name already exists in this course. Add an incremented number if it does.
 654      while ($DB->record_exists('groups', array('name' => $groupname, 'courseid' => $courseid))) {
 655          $a->increment = '(' . (++$inc) . ')';
 656          $groupname = trim(get_string('defaultgroupnametext', 'enrol_meta', $a));
 657      }
 658      // Create a new group for the course meta sync.
 659      $groupdata = new stdClass();
 660      $groupdata->courseid = $courseid;
 661      $groupdata->name = $groupname;
 662      $groupid = groups_create_group($groupdata);
 663  
 664      return $groupid;
 665  }