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.

Differences Between: [Versions 310 and 311] [Versions 310 and 400] [Versions 310 and 401] [Versions 310 and 402] [Versions 310 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   * Core container for calendar events.
  19   *
  20   * The purpose of this class is simply to wire together the various
  21   * implementations of calendar event components to produce a solution
  22   * to the problems Moodle core wants to solve.
  23   *
  24   * @package    core_calendar
  25   * @copyright  2017 Cameron Ball <cameron@cameron1729.xyz>
  26   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  27   */
  28  
  29  namespace core_calendar\local\event;
  30  
  31  defined('MOODLE_INTERNAL') || die();
  32  
  33  use core_calendar\action_factory;
  34  use core_calendar\local\event\data_access\event_vault;
  35  use core_calendar\local\event\entities\action_event;
  36  use core_calendar\local\event\entities\action_event_interface;
  37  use core_calendar\local\event\entities\event_interface;
  38  use core_calendar\local\event\factories\event_factory;
  39  use core_calendar\local\event\mappers\event_mapper;
  40  use core_calendar\local\event\strategies\raw_event_retrieval_strategy;
  41  
  42  /**
  43   * Core container.
  44   *
  45   * @copyright 2017 Cameron Ball <cameron@cameron1729.xyz>
  46   * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  47   */
  48  class container {
  49      /**
  50       * @var event_factory $eventfactory Event factory.
  51       */
  52      protected static $eventfactory;
  53  
  54      /**
  55       * @var event_mapper $eventmapper Event mapper.
  56       */
  57      protected static $eventmapper;
  58  
  59      /**
  60       * @var action_factory $actionfactory Action factory.
  61       */
  62      protected static $actionfactory;
  63  
  64      /**
  65       * @var event_vault $eventvault Event vault.
  66       */
  67      protected static $eventvault;
  68  
  69      /**
  70       * @var raw_event_retrieval_strategy $eventretrievalstrategy Event retrieval strategy.
  71       */
  72      protected static $eventretrievalstrategy;
  73  
  74      /**
  75       * @var \stdClass[] An array of cached courses to use with the event factory.
  76       */
  77      protected static $coursecache = array();
  78  
  79      /**
  80       * @var \stdClass[] An array of cached modules to use with the event factory.
  81       */
  82      protected static $modulecache = array();
  83  
  84      /**
  85       * @var int The requesting user. All capability checks are done against this user.
  86       */
  87      protected static $requestinguserid;
  88  
  89      /**
  90       * Initialises the dependency graph if it hasn't yet been.
  91       */
  92      private static function init() {
  93          if (empty(self::$eventfactory)) {
  94              self::$actionfactory = new action_factory();
  95              self::$eventmapper = new event_mapper(
  96                  // The event mapper we return from here needs to know how to
  97                  // make events, so it needs an event factory. However we can't
  98                  // give it the same one as we store and return in the container
  99                  // as that one uses all our plumbing to control event visibility.
 100                  //
 101                  // So we make a new even factory that doesn't do anyting other than
 102                  // return the instance.
 103                  new event_factory(
 104                      // Never apply actions, simply return.
 105                      function(event_interface $event) {
 106                          return $event;
 107                      },
 108                      // Never hide an event.
 109                      function() {
 110                          return true;
 111                      },
 112                      // Never bail out early when instantiating an event.
 113                      function() {
 114                          return false;
 115                      },
 116                      self::$coursecache,
 117                      self::$modulecache
 118                  )
 119              );
 120  
 121              self::$eventfactory = new event_factory(
 122                  [self::class, 'apply_component_provide_event_action'],
 123                  [self::class, 'apply_component_is_event_visible'],
 124                  function ($dbrow) {
 125                      $requestinguserid = self::get_requesting_user();
 126  
 127                      if (!empty($dbrow->categoryid)) {
 128                          // This is a category event. Check that the category is visible to this user.
 129                          $category = \core_course_category::get($dbrow->categoryid, IGNORE_MISSING, true, $requestinguserid);
 130  
 131                          if (empty($category) || !$category->is_uservisible($requestinguserid)) {
 132                              return true;
 133                          }
 134                      }
 135  
 136                      // For non-module events we assume that all checks were done in core_calendar_is_event_visible callback.
 137                      // For module events we also check that the course module and course itself are visible to the user.
 138                      if (empty($dbrow->modulename)) {
 139                          return false;
 140                      }
 141  
 142                      $instances = get_fast_modinfo($dbrow->courseid, $requestinguserid)->instances;
 143  
 144                      // If modinfo doesn't know about the module, we should ignore it.
 145                      if (!isset($instances[$dbrow->modulename]) || !isset($instances[$dbrow->modulename][$dbrow->instance])) {
 146                          return true;
 147                      }
 148  
 149                      $cm = $instances[$dbrow->modulename][$dbrow->instance];
 150  
 151                      // If the module is not visible to the current user, we should ignore it.
 152                      // We have to check enrolment here as well because the uservisible check
 153                      // looks for the "view" capability however some activities (such as Lesson)
 154                      // have that capability set on the "Authenticated User" role rather than
 155                      // on "Student" role, which means uservisible returns true even when the user
 156                      // is no longer enrolled in the course.
 157                      // So, with the following we are checking -
 158                      // 1) Only process modules if $cm->uservisible is true.
 159                      // 2) Only process modules for courses a user has the capability to view OR they are enrolled in.
 160                      // 3) Only process modules for courses that are visible OR if the course is not visible, the user
 161                      //    has the capability to view hidden courses.
 162                      if (!$cm->uservisible) {
 163                          return true;
 164                      }
 165  
 166                      $coursecontext = \context_course::instance($dbrow->courseid);
 167                      if (!$cm->get_course()->visible &&
 168                              !has_capability('moodle/course:viewhiddencourses', $coursecontext, $requestinguserid)) {
 169                          return true;
 170                      }
 171  
 172                      if (!has_capability('moodle/course:view', $coursecontext, $requestinguserid) &&
 173                              !is_enrolled($coursecontext, $requestinguserid)) {
 174                          return true;
 175                      }
 176  
 177                      // Ok, now check if we are looking at a completion event.
 178                      if ($dbrow->eventtype === \core_completion\api::COMPLETION_EVENT_TYPE_DATE_COMPLETION_EXPECTED) {
 179                          // Need to have completion enabled before displaying these events.
 180                          $course = new \stdClass();
 181                          $course->id = $dbrow->courseid;
 182                          $completion = new \completion_info($course);
 183  
 184                          return (bool) !$completion->is_enabled($cm);
 185                      }
 186  
 187                      return false;
 188                  },
 189                  self::$coursecache,
 190                  self::$modulecache
 191              );
 192          }
 193  
 194          if (empty(self::$eventvault)) {
 195              self::$eventretrievalstrategy = new raw_event_retrieval_strategy();
 196              self::$eventvault = new event_vault(self::$eventfactory, self::$eventretrievalstrategy);
 197          }
 198      }
 199  
 200      /**
 201       * Reset all static caches, called between tests.
 202       */
 203      public static function reset_caches() {
 204          self::$requestinguserid = null;
 205          self::$eventfactory = null;
 206          self::$eventmapper = null;
 207          self::$eventvault = null;
 208          self::$actionfactory = null;
 209          self::$eventretrievalstrategy = null;
 210          self::$coursecache = [];
 211          self::$modulecache = [];
 212      }
 213  
 214      /**
 215       * Gets the event factory.
 216       *
 217       * @return event_factory
 218       */
 219      public static function get_event_factory() {
 220          self::init();
 221          return self::$eventfactory;
 222      }
 223  
 224      /**
 225       * Gets the event mapper.
 226       *
 227       * @return event_mapper
 228       */
 229      public static function get_event_mapper() {
 230          self::init();
 231          return self::$eventmapper;
 232      }
 233  
 234      /**
 235       * Return an event vault.
 236       *
 237       * @return event_vault
 238       */
 239      public static function get_event_vault() {
 240          self::init();
 241          return self::$eventvault;
 242      }
 243  
 244      /**
 245       * Sets the requesting user so that all capability checks are done against this user.
 246       * Setting the requesting user (hence calling this function) is optional and if you do not so,
 247       * $USER will be used as the requesting user. However, if you wish to set the requesting user yourself,
 248       * you should call this function before any other function of the container class is called.
 249       *
 250       * @param int $userid The user id.
 251       * @throws \coding_exception
 252       */
 253      public static function set_requesting_user($userid) {
 254          self::$requestinguserid = $userid;
 255      }
 256  
 257      /**
 258       * Returns the requesting user id.
 259       * It usually is the current user unless it has been set explicitly using set_requesting_user.
 260       *
 261       * @return int
 262       */
 263      public static function get_requesting_user() {
 264          global $USER;
 265  
 266          return empty(self::$requestinguserid) ? $USER->id : self::$requestinguserid;
 267      }
 268  
 269      /**
 270       * Calls callback 'core_calendar_provide_event_action' from the component responsible for the event
 271       *
 272       * If no callback is present or callback returns null, there is no action on the event
 273       * and it will not be displayed on the dashboard.
 274       *
 275       * @param event_interface $event
 276       * @return action_event|event_interface
 277       */
 278      public static function apply_component_provide_event_action(event_interface $event) {
 279          // Callbacks will get supplied a "legacy" version
 280          // of the event class.
 281          $mapper = self::$eventmapper;
 282          $action = null;
 283          if ($event->get_component()) {
 284              $requestinguserid = self::get_requesting_user();
 285              $legacyevent = $mapper->from_event_to_legacy_event($event);
 286              // We know for a fact that the the requesting user might be different from the logged in user,
 287              // but the event mapper is not aware of that.
 288              if (empty($event->user) && !empty($legacyevent->userid)) {
 289                  $legacyevent->userid = $requestinguserid;
 290              }
 291  
 292              // Any other event will not be displayed on the dashboard.
 293              $action = component_callback(
 294                  $event->get_component(),
 295                  'core_calendar_provide_event_action',
 296                  [
 297                      $legacyevent,
 298                      self::$actionfactory,
 299                      $requestinguserid
 300                  ]
 301              );
 302          }
 303  
 304          // If we get an action back, return an action event, otherwise
 305          // continue piping through the original event.
 306          //
 307          // If a module does not implement the callback, component_callback
 308          // returns null.
 309          return $action ? new action_event($event, $action) : $event;
 310      }
 311  
 312      /**
 313       * Calls callback 'core_calendar_is_event_visible' from the component responsible for the event
 314       *
 315       * The visibility callback is optional, if not present it is assumed as visible.
 316       * If it is an actionable event but the get_item_count() returns 0 the visibility
 317       * is set to false.
 318       *
 319       * @param event_interface $event
 320       * @return bool
 321       */
 322      public static function apply_component_is_event_visible(event_interface $event) {
 323          $mapper = self::$eventmapper;
 324          $eventvisible = null;
 325          if ($event->get_component()) {
 326              $requestinguserid = self::get_requesting_user();
 327              $legacyevent = $mapper->from_event_to_legacy_event($event);
 328              // We know for a fact that the the requesting user might be different from the logged in user,
 329              // but the event mapper is not aware of that.
 330              if (empty($event->user) && !empty($legacyevent->userid)) {
 331                  $legacyevent->userid = $requestinguserid;
 332              }
 333  
 334              $eventvisible = component_callback(
 335                  $event->get_component(),
 336                  'core_calendar_is_event_visible',
 337                  [
 338                      $legacyevent,
 339                      $requestinguserid
 340                  ]
 341              );
 342          }
 343  
 344          // Do not display the event if there is nothing to action.
 345          if ($event instanceof action_event_interface && $event->get_action()->get_item_count() === 0) {
 346              return false;
 347          }
 348  
 349          // Module does not implement the callback, event should be visible.
 350          if (is_null($eventvisible)) {
 351              return true;
 352          }
 353  
 354          return $eventvisible ? true : false;
 355      }
 356  }