Search moodle.org's
Developer Documentation

See Release Notes
Long Term Support Release

  • Bug fixes for general core bugs in 4.1.x will end 13 November 2023 (12 months).
  • Bug fixes for security issues in 4.1.x will end 10 November 2025 (36 months).
  • PHP version: minimum PHP 7.4.0 Note: minimum PHP version has increased since Moodle 4.0. PHP 8.0.x is supported too.
   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 category enrolment plugin.
  19   *
  20   * @package    enrol_category
  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   * Sync all category enrolments in one course
  29   * @param stdClass $course
  30   * @return void
  31   */
  32  function enrol_category_sync_course($course) {
  33      global $DB;
  34  
  35      if (!enrol_is_enabled('category')) {
  36          return;
  37      }
  38  
  39      $plugin = enrol_get_plugin('category');
  40  
  41      $syscontext = context_system::instance();
  42      $roles = get_roles_with_capability('enrol/category:synchronised', CAP_ALLOW, $syscontext);
  43  
  44      if (!$roles) {
  45          // Nothing to sync, so remove the instance completely if exists.
  46          if ($instances = $DB->get_records('enrol', array('courseid'=>$course->id, 'enrol'=>'category'))) {
  47              foreach ($instances as $instance) {
  48                  $plugin->delete_instance($instance);
  49              }
  50          }
  51          return;
  52      }
  53  
  54      // First find out if any parent category context contains interesting role assignments.
  55      $coursecontext = context_course::instance($course->id);
  56      $contextids = $coursecontext->get_parent_context_ids();
  57      array_pop($contextids); // Remove system context, we are interested in categories only.
  58  
  59      list($roleids, $params) = $DB->get_in_or_equal(array_keys($roles), SQL_PARAMS_NAMED, 'r');
  60      list($contextids, $contextparams) = $DB->get_in_or_equal($contextids, SQL_PARAMS_NAMED, 'c');
  61      $params = array_merge($params, $contextparams);
  62      $params['courseid'] = $course->id;
  63  
  64      $sql = "SELECT 'x'
  65                FROM {role_assignments}
  66               WHERE roleid $roleids AND contextid $contextids";
  67      if (!$DB->record_exists_sql($sql, $params)) {
  68          if ($instances = $DB->get_records('enrol', array('courseid'=>$course->id, 'enrol'=>'category'))) {
  69              // Should be max one instance, but anyway.
  70              foreach ($instances as $instance) {
  71                  $plugin->delete_instance($instance);
  72              }
  73          }
  74          return;
  75      }
  76  
  77      // Make sure the enrol instance exists - there should be always only one instance.
  78      $delinstances = array();
  79      if ($instances = $DB->get_records('enrol', array('courseid'=>$course->id, 'enrol'=>'category'))) {
  80          $instance = array_shift($instances);
  81          $delinstances = $instances;
  82      } else {
  83          $i = $plugin->add_instance($course);
  84          $instance = $DB->get_record('enrol', array('id'=>$i));
  85      }
  86  
  87      // Add new enrolments.
  88      $sql = "SELECT ra.userid, ra.estart
  89                FROM (SELECT xra.userid, MIN(xra.timemodified) AS estart
  90                        FROM {role_assignments} xra
  91                        JOIN {user} xu ON (xu.id = xra.userid AND xu.deleted = 0)
  92                       WHERE xra.roleid $roleids AND xra.contextid $contextids
  93                    GROUP BY xra.userid
  94                     ) ra
  95           LEFT JOIN {user_enrolments} ue ON (ue.enrolid = :instanceid AND ue.userid = ra.userid)
  96               WHERE ue.id IS NULL";
  97      $params['instanceid'] = $instance->id;
  98      $rs = $DB->get_recordset_sql($sql, $params);
  99      foreach ($rs as $ra) {
 100          $plugin->enrol_user($instance, $ra->userid, null, $ra->estart);
 101      }
 102      $rs->close();
 103  
 104      // Remove unwanted enrolments.
 105      $sql = "SELECT DISTINCT ue.userid
 106                FROM {user_enrolments} ue
 107           LEFT JOIN {role_assignments} ra ON (ra.roleid $roleids AND ra.contextid $contextids AND ra.userid = ue.userid)
 108               WHERE ue.enrolid = :instanceid AND ra.id IS NULL";
 109      $rs = $DB->get_recordset_sql($sql, $params);
 110      foreach ($rs as $ra) {
 111          $plugin->unenrol_user($instance, $ra->userid);
 112      }
 113      $rs->close();
 114  
 115      if ($delinstances) {
 116          // We have to do this as the last step in order to prevent temporary unenrolment.
 117          foreach ($delinstances as $delinstance) {
 118              $plugin->delete_instance($delinstance);
 119          }
 120      }
 121  }
 122  
 123  /**
 124   * Synchronise courses in all categories.
 125   *
 126   * It gets out-of-sync if:
 127   * - you move course to different category
 128   * - reorder categories
 129   * - disable enrol_category and enable it again
 130   *
 131   * @param progress_trace $trace
 132   * @return int exit code - 0 is ok, 1 means error, 2 if plugin disabled
 133   */
 134  function enrol_category_sync_full(progress_trace $trace) {
 135      global $DB;
 136  
 137  
 138      if (!enrol_is_enabled('category')) {
 139          $trace->finished();
 140          return 2;
 141      }
 142  
 143      // We may need a lot of time here.
 144      core_php_time_limit::raise();
 145  
 146      $plugin = enrol_get_plugin('category');
 147  
 148      $syscontext = context_system::instance();
 149  
 150      // Any interesting roles worth synchronising?
 151      if (!$roles = get_roles_with_capability('enrol/category:synchronised', CAP_ALLOW, $syscontext)) {
 152          // yay, nothing to do, so let's remove all leftovers
 153          $trace->output("No roles with 'enrol/category:synchronised' capability found.");
 154          if ($instances = $DB->get_records('enrol', array('enrol'=>'category'))) {
 155              $trace->output("Deleting all category enrol instances...");
 156              foreach ($instances as $instance) {
 157                  $trace->output("deleting category enrol instance from course {$instance->courseid}", 1);
 158                  $plugin->delete_instance($instance);
 159              }
 160              $trace->output("...all instances deleted.");
 161          }
 162          $trace->finished();
 163          return 0;
 164      }
 165      $rolenames = role_fix_names($roles, null, ROLENAME_SHORT, true);
 166      $trace->output('Synchronising category enrolments for roles: '.implode(', ', $rolenames).'...');
 167  
 168      list($roleids, $params) = $DB->get_in_or_equal(array_keys($roles), SQL_PARAMS_NAMED, 'r');
 169      $params['courselevel'] = CONTEXT_COURSE;
 170      $params['catlevel'] = CONTEXT_COURSECAT;
 171  
 172      // First of all add necessary enrol instances to all courses.
 173      $parentcat = $DB->sql_concat("cat.path", "'/%'");
 174      $parentcctx = $DB->sql_concat("cctx.path", "'/%'");
 175      // Need whole course records to be used by add_instance(), use inner view (ci) to
 176      // get distinct records only.
 177      // TODO: Moodle 2.1. Improve enrol API to accept courseid / courserec
 178      $sql = "SELECT c.*
 179                FROM {course} c
 180                JOIN (
 181                  SELECT DISTINCT c.id
 182                    FROM {course} c
 183                    JOIN {context} ctx ON (ctx.instanceid = c.id AND ctx.contextlevel = :courselevel)
 184                    JOIN (SELECT DISTINCT cctx.path
 185                            FROM {course_categories} cc
 186                            JOIN {context} cctx ON (cctx.instanceid = cc.id AND cctx.contextlevel = :catlevel)
 187                            JOIN {role_assignments} ra ON (ra.contextid = cctx.id AND ra.roleid $roleids)
 188                         ) cat ON (ctx.path LIKE $parentcat)
 189               LEFT JOIN {enrol} e ON (e.courseid = c.id AND e.enrol = 'category')
 190                   WHERE e.id IS NULL) ci ON (c.id = ci.id)";
 191  
 192      $rs = $DB->get_recordset_sql($sql, $params);
 193      foreach($rs as $course) {
 194          $plugin->add_instance($course);
 195      }
 196      $rs->close();
 197  
 198      // Now look for courses that do not have any interesting roles in parent contexts,
 199      // but still have the instance and delete them.
 200      $sql = "SELECT e.*
 201                FROM {enrol} e
 202                JOIN {context} ctx ON (ctx.instanceid = e.courseid AND ctx.contextlevel = :courselevel)
 203           LEFT JOIN ({course_categories} cc
 204                        JOIN {context} cctx ON (cctx.instanceid = cc.id AND cctx.contextlevel = :catlevel)
 205                        JOIN {role_assignments} ra ON (ra.contextid = cctx.id AND ra.roleid $roleids)
 206                     ) ON (ctx.path LIKE $parentcctx)
 207               WHERE e.enrol = 'category' AND cc.id IS NULL";
 208  
 209      $rs = $DB->get_recordset_sql($sql, $params);
 210      foreach($rs as $instance) {
 211          $plugin->delete_instance($instance);
 212      }
 213      $rs->close();
 214  
 215      // Add missing enrolments.
 216      $sql = "SELECT e.*, cat.userid, cat.estart
 217                FROM {enrol} e
 218                JOIN {context} ctx ON (ctx.instanceid = e.courseid AND ctx.contextlevel = :courselevel)
 219                JOIN (SELECT cctx.path, ra.userid, MIN(ra.timemodified) AS estart
 220                        FROM {course_categories} cc
 221                        JOIN {context} cctx ON (cctx.instanceid = cc.id AND cctx.contextlevel = :catlevel)
 222                        JOIN {role_assignments} ra ON (ra.contextid = cctx.id AND ra.roleid $roleids)
 223                    GROUP BY cctx.path, ra.userid
 224                     ) cat ON (ctx.path LIKE $parentcat)
 225           LEFT JOIN {user_enrolments} ue ON (ue.enrolid = e.id AND ue.userid = cat.userid)
 226               WHERE e.enrol = 'category' AND ue.id IS NULL";
 227      $rs = $DB->get_recordset_sql($sql, $params);
 228      foreach($rs as $instance) {
 229          $userid = $instance->userid;
 230          $estart = $instance->estart;
 231          unset($instance->userid);
 232          unset($instance->estart);
 233          $plugin->enrol_user($instance, $userid, null, $estart);
 234          $trace->output("enrolling: user $userid ==> course $instance->courseid", 1);
 235      }
 236      $rs->close();
 237  
 238      // Remove stale enrolments.
 239      $sql = "SELECT e.*, ue.userid
 240                FROM {enrol} e
 241                JOIN {context} ctx ON (ctx.instanceid = e.courseid AND ctx.contextlevel = :courselevel)
 242                JOIN {user_enrolments} ue ON (ue.enrolid = e.id)
 243           LEFT JOIN ({course_categories} cc
 244                        JOIN {context} cctx ON (cctx.instanceid = cc.id AND cctx.contextlevel = :catlevel)
 245                        JOIN {role_assignments} ra ON (ra.contextid = cctx.id AND ra.roleid $roleids)
 246                     ) ON (ctx.path LIKE $parentcctx AND ra.userid = ue.userid)
 247               WHERE e.enrol = 'category' AND cc.id IS NULL";
 248      $rs = $DB->get_recordset_sql($sql, $params);
 249      foreach($rs as $instance) {
 250          $userid = $instance->userid;
 251          unset($instance->userid);
 252          $plugin->unenrol_user($instance, $userid);
 253          $trace->output("unenrolling: user $userid ==> course $instance->courseid", 1);
 254      }
 255      $rs->close();
 256  
 257      $trace->output('...user enrolment synchronisation finished.');
 258      $trace->finished();
 259  
 260      return 0;
 261  }