Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 4.0.x will end 8 May 2023 (12 months).
  • Bug fixes for security issues in 4.0.x will end 13 November 2023 (18 months).
  • PHP version: minimum PHP 7.3.0 Note: the minimum PHP version has increased since Moodle 3.10. PHP 7.4.x is also supported.

Differences Between: [Versions 310 and 400] [Versions 39 and 400]

   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                          if ($completion->is_enabled($cm)) {
 184                              // Check if the event is completed, then in this case we do not need to complete it.
 185                              // Make sure we're using a cm_info object.
 186                              $completiondata = $completion->get_data($cm);
 187                              return intval($completiondata->completionstate) === COMPLETION_COMPLETE;
 188                          }
 189                          return true;
 190                      }
 191  
 192                      return false;
 193                  },
 194                  self::$coursecache,
 195                  self::$modulecache
 196              );
 197          }
 198  
 199          if (empty(self::$eventvault)) {
 200              self::$eventretrievalstrategy = new raw_event_retrieval_strategy();
 201              self::$eventvault = new event_vault(self::$eventfactory, self::$eventretrievalstrategy);
 202          }
 203      }
 204  
 205      /**
 206       * Reset all static caches, called between tests.
 207       */
 208      public static function reset_caches() {
 209          self::$requestinguserid = null;
 210          self::$eventfactory = null;
 211          self::$eventmapper = null;
 212          self::$eventvault = null;
 213          self::$actionfactory = null;
 214          self::$eventretrievalstrategy = null;
 215          self::$coursecache = [];
 216          self::$modulecache = [];
 217      }
 218  
 219      /**
 220       * Gets the event factory.
 221       *
 222       * @return event_factory
 223       */
 224      public static function get_event_factory() {
 225          self::init();
 226          return self::$eventfactory;
 227      }
 228  
 229      /**
 230       * Gets the event mapper.
 231       *
 232       * @return event_mapper
 233       */
 234      public static function get_event_mapper() {
 235          self::init();
 236          return self::$eventmapper;
 237      }
 238  
 239      /**
 240       * Return an event vault.
 241       *
 242       * @return event_vault
 243       */
 244      public static function get_event_vault() {
 245          self::init();
 246          return self::$eventvault;
 247      }
 248  
 249      /**
 250       * Sets the requesting user so that all capability checks are done against this user.
 251       * Setting the requesting user (hence calling this function) is optional and if you do not so,
 252       * $USER will be used as the requesting user. However, if you wish to set the requesting user yourself,
 253       * you should call this function before any other function of the container class is called.
 254       *
 255       * @param int $userid The user id.
 256       * @throws \coding_exception
 257       */
 258      public static function set_requesting_user($userid) {
 259          self::$requestinguserid = $userid;
 260      }
 261  
 262      /**
 263       * Returns the requesting user id.
 264       * It usually is the current user unless it has been set explicitly using set_requesting_user.
 265       *
 266       * @return int
 267       */
 268      public static function get_requesting_user() {
 269          global $USER;
 270  
 271          return empty(self::$requestinguserid) ? $USER->id : self::$requestinguserid;
 272      }
 273  
 274      /**
 275       * Calls callback 'core_calendar_provide_event_action' from the component responsible for the event
 276       *
 277       * If no callback is present or callback returns null, there is no action on the event
 278       * and it will not be displayed on the dashboard.
 279       *
 280       * @param event_interface $event
 281       * @return action_event|event_interface
 282       */
 283      public static function apply_component_provide_event_action(event_interface $event) {
 284          // Callbacks will get supplied a "legacy" version
 285          // of the event class.
 286          $mapper = self::$eventmapper;
 287          $action = null;
 288          if ($event->get_component()) {
 289              $requestinguserid = self::get_requesting_user();
 290              $legacyevent = $mapper->from_event_to_legacy_event($event);
 291              // We know for a fact that the the requesting user might be different from the logged in user,
 292              // but the event mapper is not aware of that.
 293              if (empty($event->user) && !empty($legacyevent->userid)) {
 294                  $legacyevent->userid = $requestinguserid;
 295              }
 296  
 297              // Any other event will not be displayed on the dashboard.
 298              $action = component_callback(
 299                  $event->get_component(),
 300                  'core_calendar_provide_event_action',
 301                  [
 302                      $legacyevent,
 303                      self::$actionfactory,
 304                      $requestinguserid
 305                  ]
 306              );
 307          }
 308  
 309          // If we get an action back, return an action event, otherwise
 310          // continue piping through the original event.
 311          //
 312          // If a module does not implement the callback, component_callback
 313          // returns null.
 314          return $action ? new action_event($event, $action) : $event;
 315      }
 316  
 317      /**
 318       * Calls callback 'core_calendar_is_event_visible' from the component responsible for the event
 319       *
 320       * The visibility callback is optional, if not present it is assumed as visible.
 321       * If it is an actionable event but the get_item_count() returns 0 the visibility
 322       * is set to false.
 323       *
 324       * @param event_interface $event
 325       * @return bool
 326       */
 327      public static function apply_component_is_event_visible(event_interface $event) {
 328          $mapper = self::$eventmapper;
 329          $eventvisible = null;
 330          if ($event->get_component()) {
 331              $requestinguserid = self::get_requesting_user();
 332              $legacyevent = $mapper->from_event_to_legacy_event($event);
 333              // We know for a fact that the the requesting user might be different from the logged in user,
 334              // but the event mapper is not aware of that.
 335              if (empty($event->user) && !empty($legacyevent->userid)) {
 336                  $legacyevent->userid = $requestinguserid;
 337              }
 338  
 339              $eventvisible = component_callback(
 340                  $event->get_component(),
 341                  'core_calendar_is_event_visible',
 342                  [
 343                      $legacyevent,
 344                      $requestinguserid
 345                  ]
 346              );
 347          }
 348  
 349          // Do not display the event if there is nothing to action.
 350          if ($event instanceof action_event_interface && $event->get_action()->get_item_count() === 0) {
 351              return false;
 352          }
 353  
 354          // Module does not implement the callback, event should be visible.
 355          if (is_null($eventvisible)) {
 356              return true;
 357          }
 358  
 359          return $eventvisible ? true : false;
 360      }
 361  }