. /** * Core container for calendar events. * * The purpose of this class is simply to wire together the various * implementations of calendar event components to produce a solution * to the problems Moodle core wants to solve. * * @package core_calendar * @copyright 2017 Cameron Ball * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace core_calendar\local\event; defined('MOODLE_INTERNAL') || die(); use core_calendar\action_factory; use core_calendar\local\event\data_access\event_vault; use core_calendar\local\event\entities\action_event; use core_calendar\local\event\entities\action_event_interface; use core_calendar\local\event\entities\event_interface; use core_calendar\local\event\factories\event_factory; use core_calendar\local\event\mappers\event_mapper; use core_calendar\local\event\strategies\raw_event_retrieval_strategy; /** * Core container. * * @copyright 2017 Cameron Ball * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class container { /** * @var event_factory $eventfactory Event factory. */ protected static $eventfactory; /** * @var event_mapper $eventmapper Event mapper. */ protected static $eventmapper; /** * @var action_factory $actionfactory Action factory. */ protected static $actionfactory; /** * @var event_vault $eventvault Event vault. */ protected static $eventvault; /** * @var raw_event_retrieval_strategy $eventretrievalstrategy Event retrieval strategy. */ protected static $eventretrievalstrategy; /** * @var \stdClass[] An array of cached courses to use with the event factory. */ protected static $coursecache = array(); /** * @var \stdClass[] An array of cached modules to use with the event factory. */ protected static $modulecache = array(); /** * @var int The requesting user. All capability checks are done against this user. */ protected static $requestinguserid; /** * Initialises the dependency graph if it hasn't yet been. */ private static function init() { if (empty(self::$eventfactory)) { self::$actionfactory = new action_factory(); self::$eventmapper = new event_mapper( // The event mapper we return from here needs to know how to // make events, so it needs an event factory. However we can't // give it the same one as we store and return in the container // as that one uses all our plumbing to control event visibility. // // So we make a new even factory that doesn't do anyting other than // return the instance. new event_factory( // Never apply actions, simply return. function(event_interface $event) { return $event; }, // Never hide an event. function() { return true; }, // Never bail out early when instantiating an event. function() { return false; }, self::$coursecache, self::$modulecache ) ); self::$eventfactory = new event_factory( [self::class, 'apply_component_provide_event_action'], [self::class, 'apply_component_is_event_visible'], function ($dbrow) { $requestinguserid = self::get_requesting_user(); if (!empty($dbrow->categoryid)) { // This is a category event. Check that the category is visible to this user. $category = \core_course_category::get($dbrow->categoryid, IGNORE_MISSING, true, $requestinguserid); if (empty($category) || !$category->is_uservisible($requestinguserid)) { return true; } } // At present we only have a bail-out check for events in course modules. if (empty($dbrow->modulename)) { return false; } $instances = get_fast_modinfo($dbrow->courseid, $requestinguserid)->instances; // If modinfo doesn't know about the module, we should ignore it. if (!isset($instances[$dbrow->modulename]) || !isset($instances[$dbrow->modulename][$dbrow->instance])) { return true; } $cm = $instances[$dbrow->modulename][$dbrow->instance]; // If the module is not visible to the current user, we should ignore it. // We have to check enrolment here as well because the uservisible check // looks for the "view" capability however some activities (such as Lesson) // have that capability set on the "Authenticated User" role rather than // on "Student" role, which means uservisible returns true even when the user // is no longer enrolled in the course. // So, with the following we are checking - // 1) Only process modules if $cm->uservisible is true. // 2) Only process modules for courses a user has the capability to view OR they are enrolled in. // 3) Only process modules for courses that are visible OR if the course is not visible, the user // has the capability to view hidden courses. if (!$cm->uservisible) { return true; } $coursecontext = \context_course::instance($dbrow->courseid); if (!$cm->get_course()->visible && !has_capability('moodle/course:viewhiddencourses', $coursecontext, $requestinguserid)) { return true; } if (!has_capability('moodle/course:view', $coursecontext, $requestinguserid) && !is_enrolled($coursecontext, $requestinguserid)) { return true; } // Ok, now check if we are looking at a completion event. if ($dbrow->eventtype === \core_completion\api::COMPLETION_EVENT_TYPE_DATE_COMPLETION_EXPECTED) { // Need to have completion enabled before displaying these events. $course = new \stdClass(); $course->id = $dbrow->courseid; $completion = new \completion_info($course); return (bool) !$completion->is_enabled($cm); } return false; }, self::$coursecache, self::$modulecache ); } if (empty(self::$eventvault)) { self::$eventretrievalstrategy = new raw_event_retrieval_strategy(); self::$eventvault = new event_vault(self::$eventfactory, self::$eventretrievalstrategy); } } /** * Reset all static caches, called between tests. */ public static function reset_caches() { self::$requestinguserid = null; self::$eventfactory = null; self::$eventmapper = null; self::$eventvault = null; self::$actionfactory = null; self::$eventretrievalstrategy = null; self::$coursecache = []; self::$modulecache = []; } /** * Gets the event factory. * * @return event_factory */ public static function get_event_factory() { self::init(); return self::$eventfactory; } /** * Gets the event mapper. * * @return event_mapper */ public static function get_event_mapper() { self::init(); return self::$eventmapper; } /** * Return an event vault. * * @return event_vault */ public static function get_event_vault() { self::init(); return self::$eventvault; } /** * Sets the requesting user so that all capability checks are done against this user. * Setting the requesting user (hence calling this function) is optional and if you do not so, * $USER will be used as the requesting user. However, if you wish to set the requesting user yourself, * you should call this function before any other function of the container class is called. * * @param int $userid The user id. * @throws \coding_exception */ public static function set_requesting_user($userid) { self::$requestinguserid = $userid; } /** * Returns the requesting user id. * It usually is the current user unless it has been set explicitly using set_requesting_user. * * @return int */ public static function get_requesting_user() { global $USER; return empty(self::$requestinguserid) ? $USER->id : self::$requestinguserid; } /** * Calls callback 'core_calendar_provide_event_action' from the component responsible for the event * * If no callback is present or callback returns null, there is no action on the event * and it will not be displayed on the dashboard. * * @param event_interface $event * @return action_event|event_interface */ public static function apply_component_provide_event_action(event_interface $event) { // Callbacks will get supplied a "legacy" version // of the event class. $mapper = self::$eventmapper; $action = null; if ($event->get_course_module()) { $requestinguserid = self::get_requesting_user(); $legacyevent = $mapper->from_event_to_legacy_event($event); // We know for a fact that the the requesting user might be different from the logged in user, // but the event mapper is not aware of that. if (empty($event->user) && !empty($legacyevent->userid)) { $legacyevent->userid = $requestinguserid; } // TODO MDL-58866 Only activity modules currently support this callback. // Any other event will not be displayed on the dashboard. $action = component_callback( 'mod_' . $event->get_course_module()->get('modname'), 'core_calendar_provide_event_action', [ $legacyevent, self::$actionfactory, $requestinguserid ] ); } // If we get an action back, return an action event, otherwise // continue piping through the original event. // // If a module does not implement the callback, component_callback // returns null. return $action ? new action_event($event, $action) : $event; } /** * Calls callback 'core_calendar_is_event_visible' from the component responsible for the event * * The visibility callback is optional, if not present it is assumed as visible. * If it is an actionable event but the get_item_count() returns 0 the visibility * is set to false. * * @param event_interface $event * @return bool */ public static function apply_component_is_event_visible(event_interface $event) { $mapper = self::$eventmapper; $eventvisible = null; if ($event->get_course_module()) { $requestinguserid = self::get_requesting_user(); $legacyevent = $mapper->from_event_to_legacy_event($event); // We know for a fact that the the requesting user might be different from the logged in user, // but the event mapper is not aware of that. if (empty($event->user) && !empty($legacyevent->userid)) { $legacyevent->userid = $requestinguserid; } // TODO MDL-58866 Only activity modules currently support this callback. $eventvisible = component_callback( 'mod_' . $event->get_course_module()->get('modname'), 'core_calendar_is_event_visible', [ $legacyevent, $requestinguserid ] ); } // Do not display the event if there is nothing to action. if ($event instanceof action_event_interface && $event->get_action()->get_item_count() === 0) { return false; } // Module does not implement the callback, event should be visible. if (is_null($eventvisible)) { return true; } return $eventvisible ? true : false; } }