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.
   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  namespace tool_monitor\task;
  17  use tool_monitor\subscription;
  18  use tool_monitor\subscription_manager;
  19  
  20  /**
  21   * Simple task class responsible for activating, deactivating and removing subscriptions.
  22   *
  23   * Activation/deactivation is managed by looking at the same access rules used to determine whether a user can
  24   * subscribe to the rule in the first place.
  25   *
  26   * Removal occurs when a subscription has been inactive for a period of time exceeding the lifespan, as set by
  27   * subscription_manager::get_inactive_subscription_lifespan().
  28   *
  29   * I.e.
  30   *  - Activation:   If a user can subscribe currently, then an existing subscription should be made active.
  31   *  - Deactivation: If a user cannot subscribe currently, then an existing subscription should be made inactive.
  32   *  - Removal:      If a user has a subscription that has been inactive for longer than the prescribed period, then
  33   *                  delete the subscription entirely.
  34   *
  35   * @since      3.2.0
  36   * @package    tool_monitor
  37   * @copyright  2016 Jake Dallimore <jrhdallimore@gmail.com>
  38   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  39   */
  40  class check_subscriptions extends \core\task\scheduled_task {
  41  
  42      /** @var array 1d static cache, indexed by userid, storing whether or not the user has been fully set up.*/
  43      protected $userssetupcache = array();
  44  
  45      /** @var array 2d static cache, indexed by courseid and userid, storing whether a user can access the course with
  46       *  the 'tool/monitor:subscribe' capability.
  47       */
  48      protected $courseaccesscache = array();
  49  
  50      /**
  51       * Get a descriptive name for this task.
  52       *
  53       * @since 3.2.0
  54       * @return string name of the task.
  55       */
  56      public function get_name() {
  57          return get_string('taskchecksubscriptions', 'tool_monitor');
  58      }
  59  
  60      /**
  61       * Checks all course-level rule subscriptions and activates/deactivates based on current course access.
  62       *
  63       * The ordering of checks within the task is important for optimisation purposes. The aim is to be able to make a decision
  64       * about whether to activate/deactivate each subscription without making unnecessary checks. The ordering roughly follows the
  65       * context model, starting with system and user checks and moving down to course and course-module only when necessary.
  66       *
  67       * For example, if the user is suspended, then any active subscription is made inactive right away. I.e. there is no need to
  68       * check site-level, course-level or course-module-level permissions. Likewise, if a subscriptions is site-level, there is no
  69       * need to check course-level and course-module-level permissions.
  70       *
  71       * The task performs the following checks, in this order:
  72       * 1. Check for a suspended user, breaking if suspended.
  73       * 2. Check for an incomplete (not set up) user, breaking if not fully set up.
  74       * 3. Check for the required capability in the relevant context, breaking if the capability is not found.
  75       * 4. Check whether the subscription is site-context, breaking if true.
  76       * 5. Check whether the user has course access, breaking only if the subscription is not also course-module-level.
  77       * 6. Check whether the user has course-module access.
  78       *
  79       * @since 3.2.0
  80       */
  81      public function execute() {
  82          global $DB;
  83  
  84          if (!get_config('tool_monitor', 'enablemonitor')) {
  85              return; // The tool is disabled. Nothing to do.
  86          }
  87  
  88          $toactivate   = array(); // Store the ids of subscriptions to be activated upon completion.
  89          $todeactivate = array(); // Store the ids of subscriptions to be deactivated upon completion.
  90  
  91          // Resultset rows are ordered by userid and courseid to work nicely with get_fast_modinfo() caching.
  92          $sql = "SELECT u.id AS userid, u.firstname AS userfirstname, u.lastname AS userlastname, u.suspended AS usersuspended,
  93                         u.email AS useremail, c.visible as coursevisible, c.cacherev as coursecacherev, s.courseid AS subcourseid,
  94                         s.userid AS subuserid, s.cmid AS subcmid, s.inactivedate AS subinactivedate, s.id AS subid
  95                    FROM {user} u
  96                    JOIN {tool_monitor_subscriptions} s ON (s.userid = u.id)
  97               LEFT JOIN {course} c ON (c.id = s.courseid)
  98                   WHERE u.id = s.userid
  99                ORDER BY s.userid, s.courseid";
 100          $rs = $DB->get_recordset_sql($sql);
 101  
 102          foreach ($rs as $row) {
 103              // Create skeleton records from the result. This should be enough to use in subsequent access calls and avoids DB hits.
 104              $sub = $this->get_subscription_from_rowdata($row);
 105              $sub = new subscription($sub);
 106              if (!isset($user) || $user->id != $sub->userid) {
 107                  $user= $this->get_user_from_rowdata($row);
 108              }
 109              if ((!isset($course) || $course->id != $sub->courseid) && !empty($sub->courseid)) {
 110                  $course = $this->get_course_from_rowdata($row);
 111              }
 112  
 113              // The user is suspended at site level, so deactivate any active subscriptions.
 114              if ($user->suspended) {
 115                  if (subscription_manager::subscription_is_active($sub)) {
 116                      $todeactivate[] = $sub->id;
 117                  }
 118                  continue;
 119              }
 120  
 121              // Is the user fully set up? As per require_login on the subscriptions page.
 122              if (!$this->is_user_setup($user)) {
 123                  if (subscription_manager::subscription_is_active($sub)) {
 124                      $todeactivate[] = $sub->id;
 125                  }
 126                  continue;
 127              }
 128  
 129              // Determine the context, based on the subscription course id.
 130              $sitelevelsubscription = false;
 131              if (empty($sub->courseid)) {
 132                  $context = \context_system::instance();
 133                  $sitelevelsubscription = true;
 134              } else {
 135                  $context = \context_course::instance($sub->courseid);
 136              }
 137  
 138              // Check capability in the context.
 139              if (!has_capability('tool/monitor:subscribe', $context, $user)) {
 140                  if (subscription_manager::subscription_is_active($sub)) {
 141                      $todeactivate[] = $sub->id;
 142                  }
 143                  continue;
 144              }
 145  
 146              // If the subscription is site-level, then we've run all the checks required to make an access decision.
 147              if ($sitelevelsubscription) {
 148                  if (!subscription_manager::subscription_is_active($sub)) {
 149                      $toactivate[] = $sub->id;
 150                  }
 151                  continue;
 152              }
 153  
 154              // Check course access.
 155              if (!$this->user_can_access_course($user, $course, 'tool/monitor:subscribe')) {
 156                  if (subscription_manager::subscription_is_active($sub)) {
 157                      $todeactivate[] = $sub->id;
 158                  }
 159                  continue;
 160              }
 161  
 162              // If the subscription has no course module relationship.
 163              if (empty($sub->cmid)) {
 164                  if (!subscription_manager::subscription_is_active($sub)) {
 165                      $toactivate[] = $sub->id;
 166                  }
 167                  continue;
 168              }
 169  
 170              // Otherwise, check the course module info. We use the same checks as on the subscription page.
 171              $modinfo = get_fast_modinfo($course, $sub->userid);
 172              $cm = $modinfo->get_cm($sub->cmid);
 173              if (!$cm || !$cm->uservisible || !$cm->available) {
 174                  if (subscription_manager::subscription_is_active($sub)) {
 175                      $todeactivate[] = $sub->id;
 176                  }
 177                  continue;
 178              }
 179  
 180              // The course module is available and visible, so make a decision.
 181              if (!subscription_manager::subscription_is_active($sub)) {
 182                  $toactivate[] = $sub->id;
 183              }
 184          }
 185          $rs->close();
 186  
 187          // Activate/deactivate/delete relevant subscriptions.
 188          subscription_manager::activate_subscriptions($toactivate);
 189          subscription_manager::deactivate_subscriptions($todeactivate);
 190          subscription_manager::delete_stale_subscriptions();
 191      }
 192  
 193      /**
 194       * Determines whether a user is fully set up, using cached results where possible.
 195       *
 196       * @since 3.2.0
 197       * @param \stdClass $user the user record.
 198       * @return bool true if the user is fully set up, false otherwise.
 199       */
 200      protected function is_user_setup($user) {
 201          if (!isset($this->userssetupcache[$user->id])) {
 202              $this->userssetupcache[$user->id] = !user_not_fully_set_up($user, true);
 203          }
 204          return $this->userssetupcache[$user->id];
 205      }
 206  
 207      /**
 208       * Determines a user's access to a course with a given capability, using cached results where possible.
 209       *
 210       * @since 3.2.0
 211       * @param \stdClass $user the user record.
 212       * @param \stdClass $course the course record.
 213       * @param string $capability the capability to check.
 214       * @return bool true if the user can access the course with the specified capability, false otherwise.
 215       */
 216      protected function user_can_access_course($user, $course, $capability) {
 217          if (!isset($this->courseaccesscache[$course->id][$user->id][$capability])) {
 218              $this->courseaccesscache[$course->id][$user->id][$capability] = can_access_course($course, $user, $capability, true);
 219          }
 220          return $this->courseaccesscache[$course->id][$user->id][$capability];
 221      }
 222  
 223      /**
 224       * Returns a partial subscription record, created from properties of the supplied recordset row object.
 225       * Intended to return a minimal record for specific use within this class and in subsequent access control calls only.
 226       *
 227       * @since 3.2.0
 228       * @param \stdClass $rowdata the row object.
 229       * @return \stdClass a partial subscription record.
 230       */
 231      protected function get_subscription_from_rowdata($rowdata) {
 232          $sub = new \stdClass();
 233          $sub->id = $rowdata->subid;
 234          $sub->userid = $rowdata->subuserid;
 235          $sub->courseid = $rowdata->subcourseid;
 236          $sub->cmid = $rowdata->subcmid;
 237          $sub->inactivedate = $rowdata->subinactivedate;
 238          return $sub;
 239      }
 240  
 241      /**
 242       * Returns a partial course record, created from properties of the supplied recordset row object.
 243       * Intended to return a minimal record for specific use within this class and in subsequent access control calls only.
 244       *
 245       * @since 3.2.0
 246       * @param \stdClass $rowdata the row object.
 247       * @return \stdClass a partial course record.
 248       */
 249      protected function get_course_from_rowdata($rowdata) {
 250          $course = new \stdClass();
 251          $course->id = $rowdata->subcourseid;
 252          $course->visible = $rowdata->coursevisible;
 253          $course->cacherev = $rowdata->coursecacherev;
 254          return $course;
 255      }
 256  
 257      /**
 258       * Returns a partial user record, created from properties of the supplied recordset row object.
 259       * Intended to return a minimal record for specific use within this class and in subsequent access control calls only.
 260       *
 261       * @since 3.2.0
 262       * @param \stdClass $rowdata the row object.
 263       * @return \stdClass a partial user record.
 264       */
 265      protected function get_user_from_rowdata($rowdata) {
 266          $user = new \stdClass();
 267          $user->id = $rowdata->userid;
 268          $user->firstname = $rowdata->userfirstname;
 269          $user->lastname = $rowdata->userlastname;
 270          $user->email = $rowdata->useremail;
 271          $user->suspended = $rowdata->usersuspended;
 272          return $user;
 273      }
 274  }