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.
/calendar/ -> lib.php (source)

Differences Between: [Versions 310 and 401] [Versions 311 and 401] [Versions 39 and 401] [Versions 400 and 401] [Versions 401 and 402] [Versions 401 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   * Calendar extension
  19   *
  20   * @package    core_calendar
  21   * @copyright  2004 Greek School Network (http://www.sch.gr), Jon Papaioannou,
  22   *             Avgoustos Tsinakos
  23   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  24   */
  25  
  26  if (!defined('MOODLE_INTERNAL')) {
  27      die('Direct access to this script is forbidden.');    ///  It must be included from a Moodle page
  28  }
  29  
  30  /**
  31   *  These are read by the administration component to provide default values
  32   */
  33  
  34  /**
  35   * CALENDAR_DEFAULT_UPCOMING_LOOKAHEAD - default value of upcoming event preference
  36   */
  37  define('CALENDAR_DEFAULT_UPCOMING_LOOKAHEAD', 21);
  38  
  39  /**
  40   * CALENDAR_DEFAULT_UPCOMING_MAXEVENTS - default value to display the maximum number of upcoming event
  41   */
  42  define('CALENDAR_DEFAULT_UPCOMING_MAXEVENTS', 10);
  43  
  44  /**
  45   * CALENDAR_DEFAULT_STARTING_WEEKDAY - default value to display the starting weekday
  46   */
  47  define('CALENDAR_DEFAULT_STARTING_WEEKDAY', 1);
  48  
  49  // This is a packed bitfield: day X is "weekend" if $field & (1 << X) is true
  50  // Default value = 65 = 64 + 1 = 2^6 + 2^0 = Saturday & Sunday
  51  
  52  /**
  53   * CALENDAR_DEFAULT_WEEKEND - default value for weekend (Saturday & Sunday)
  54   */
  55  define('CALENDAR_DEFAULT_WEEKEND', 65);
  56  
  57  /**
  58   * CALENDAR_URL - path to calendar's folder
  59   */
  60  define('CALENDAR_URL', $CFG->wwwroot.'/calendar/');
  61  
  62  /**
  63   * CALENDAR_TF_24 - Calendar time in 24 hours format
  64   */
  65  define('CALENDAR_TF_24', '%H:%M');
  66  
  67  /**
  68   * CALENDAR_TF_12 - Calendar time in 12 hours format
  69   */
  70  define('CALENDAR_TF_12', '%I:%M %p');
  71  
  72  /**
  73   * CALENDAR_EVENT_SITE - Site calendar event types
  74   */
  75  define('CALENDAR_EVENT_SITE', 1);
  76  
  77  /**
  78   * CALENDAR_EVENT_COURSE - Course calendar event types
  79   */
  80  define('CALENDAR_EVENT_COURSE', 2);
  81  
  82  /**
  83   * CALENDAR_EVENT_GROUP - group calendar event types
  84   */
  85  define('CALENDAR_EVENT_GROUP', 4);
  86  
  87  /**
  88   * CALENDAR_EVENT_USER - user calendar event types
  89   */
  90  define('CALENDAR_EVENT_USER', 8);
  91  
  92  /**
  93   * CALENDAR_EVENT_COURSECAT - Course category calendar event types
  94   */
  95  define('CALENDAR_EVENT_COURSECAT', 16);
  96  
  97  /**
  98   * CALENDAR_IMPORT_FROM_FILE - import the calendar from a file
  99   */
 100  define('CALENDAR_IMPORT_FROM_FILE', 0);
 101  
 102  /**
 103   * CALENDAR_IMPORT_FROM_URL - import the calendar from a URL
 104   */
 105  define('CALENDAR_IMPORT_FROM_URL',  1);
 106  
 107  /**
 108   * CALENDAR_IMPORT_EVENT_UPDATED_SKIPPED - imported event was skipped
 109   */
 110  define('CALENDAR_IMPORT_EVENT_SKIPPED',  -1);
 111  
 112  /**
 113   * CALENDAR_IMPORT_EVENT_UPDATED - imported event was updated
 114   */
 115  define('CALENDAR_IMPORT_EVENT_UPDATED',  1);
 116  
 117  /**
 118   * CALENDAR_IMPORT_EVENT_INSERTED - imported event was added by insert
 119   */
 120  define('CALENDAR_IMPORT_EVENT_INSERTED', 2);
 121  
 122  /**
 123   * CALENDAR_SUBSCRIPTION_UPDATE - Used to represent update action for subscriptions in various forms.
 124   */
 125  define('CALENDAR_SUBSCRIPTION_UPDATE', 1);
 126  
 127  /**
 128   * CALENDAR_SUBSCRIPTION_REMOVE - Used to represent remove action for subscriptions in various forms.
 129   */
 130  define('CALENDAR_SUBSCRIPTION_REMOVE', 2);
 131  
 132  /**
 133   * CALENDAR_EVENT_USER_OVERRIDE_PRIORITY - Constant for the user override priority.
 134   */
 135  define('CALENDAR_EVENT_USER_OVERRIDE_PRIORITY', 0);
 136  
 137  /**
 138   * CALENDAR_EVENT_TYPE_STANDARD - Standard events.
 139   */
 140  define('CALENDAR_EVENT_TYPE_STANDARD', 0);
 141  
 142  /**
 143   * CALENDAR_EVENT_TYPE_ACTION - Action events.
 144   */
 145  define('CALENDAR_EVENT_TYPE_ACTION', 1);
 146  
 147  /**
 148   * Manage calendar events.
 149   *
 150   * This class provides the required functionality in order to manage calendar events.
 151   * It was introduced as part of Moodle 2.0 and was created in order to provide a
 152   * better framework for dealing with calendar events in particular regard to file
 153   * handling through the new file API.
 154   *
 155   * @package    core_calendar
 156   * @category   calendar
 157   * @copyright  2009 Sam Hemelryk
 158   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 159   *
 160   * @property int $id The id within the event table
 161   * @property string $name The name of the event
 162   * @property string $description The description of the event
 163   * @property int $format The format of the description FORMAT_?
 164   * @property int $courseid The course the event is associated with (0 if none)
 165   * @property int $groupid The group the event is associated with (0 if none)
 166   * @property int $userid The user the event is associated with (0 if none)
 167   * @property int $repeatid If this is a repeated event this will be set to the
 168   *                          id of the original
 169   * @property string $component If created by a plugin/component (other than module), the full frankenstyle name of a component
 170   * @property string $modulename If added by a module this will be the module name
 171   * @property int $instance If added by a module this will be the module instance
 172   * @property string $eventtype The event type
 173   * @property int $timestart The start time as a timestamp
 174   * @property int $timeduration The duration of the event in seconds
 175   * @property int $timeusermidnight User midnight for the event
 176   * @property int $visible 1 if the event is visible
 177   * @property int $uuid ?
 178   * @property int $sequence ?
 179   * @property int $timemodified The time last modified as a timestamp
 180   */
 181  class calendar_event {
 182  
 183      /** @var array An object containing the event properties can be accessed via the magic __get/set methods */
 184      protected $properties = null;
 185  
 186      /** @var string The converted event discription with file paths resolved.
 187       *              This gets populated when someone requests description for the first time */
 188      protected $_description = null;
 189  
 190      /** @var array The options to use with this description editor */
 191      protected $editoroptions = array(
 192          'subdirs' => false,
 193          'forcehttps' => false,
 194          'maxfiles' => -1,
 195          'maxbytes' => null,
 196          'trusttext' => false);
 197  
 198      /** @var object The context to use with the description editor */
 199      protected $editorcontext = null;
 200  
 201      /**
 202       * Instantiates a new event and optionally populates its properties with the data provided.
 203       *
 204       * @param \stdClass $data Optional. An object containing the properties to for
 205       *                  an event
 206       */
 207      public function __construct($data = null) {
 208          global $CFG, $USER;
 209  
 210          // First convert to object if it is not already (should either be object or assoc array).
 211          if (!is_object($data)) {
 212              $data = (object) $data;
 213          }
 214  
 215          $this->editoroptions['maxbytes'] = $CFG->maxbytes;
 216  
 217          $data->eventrepeats = 0;
 218  
 219          if (empty($data->id)) {
 220              $data->id = null;
 221          }
 222  
 223          if (!empty($data->subscriptionid)) {
 224              $data->subscription = calendar_get_subscription($data->subscriptionid);
 225          }
 226  
 227          // Default to a user event.
 228          if (empty($data->eventtype)) {
 229              $data->eventtype = 'user';
 230          }
 231  
 232          // Default to the current user.
 233          if (empty($data->userid)) {
 234              $data->userid = $USER->id;
 235          }
 236  
 237          if (!empty($data->timeduration) && is_array($data->timeduration)) {
 238              $data->timeduration = make_timestamp(
 239                      $data->timeduration['year'], $data->timeduration['month'], $data->timeduration['day'],
 240                      $data->timeduration['hour'], $data->timeduration['minute']) - $data->timestart;
 241          }
 242  
 243          if (!empty($data->description) && is_array($data->description)) {
 244              $data->format = $data->description['format'];
 245              $data->description = $data->description['text'];
 246          } else if (empty($data->description)) {
 247              $data->description = '';
 248              $data->format = editors_get_preferred_format();
 249          }
 250  
 251          // Ensure form is defaulted correctly.
 252          if (empty($data->format)) {
 253              $data->format = editors_get_preferred_format();
 254          }
 255  
 256          if (empty($data->component)) {
 257              $data->component = null;
 258          }
 259  
 260          $this->properties = $data;
 261      }
 262  
 263      /**
 264       * Magic set method.
 265       *
 266       * Attempts to call a set_$key method if one exists otherwise falls back
 267       * to simply set the property.
 268       *
 269       * @param string $key property name
 270       * @param mixed $value value of the property
 271       */
 272      public function __set($key, $value) {
 273          if (method_exists($this, 'set_'.$key)) {
 274              $this->{'set_'.$key}($value);
 275          }
 276          $this->properties->{$key} = $value;
 277      }
 278  
 279      /**
 280       * Magic get method.
 281       *
 282       * Attempts to call a get_$key method to return the property and ralls over
 283       * to return the raw property.
 284       *
 285       * @param string $key property name
 286       * @return mixed property value
 287       * @throws \coding_exception
 288       */
 289      public function __get($key) {
 290          if (method_exists($this, 'get_'.$key)) {
 291              return $this->{'get_'.$key}();
 292          }
 293          if (!property_exists($this->properties, $key)) {
 294              throw new \coding_exception('Undefined property requested');
 295          }
 296          return $this->properties->{$key};
 297      }
 298  
 299      /**
 300       * Magic isset method.
 301       *
 302       * PHP needs an isset magic method if you use the get magic method and
 303       * still want empty calls to work.
 304       *
 305       * @param string $key $key property name
 306       * @return bool|mixed property value, false if property is not exist
 307       */
 308      public function __isset($key) {
 309          return !empty($this->properties->{$key});
 310      }
 311  
 312      /**
 313       * Calculate the context value needed for an event.
 314       *
 315       * Event's type can be determine by the available value store in $data
 316       * It is important to check for the existence of course/courseid to determine
 317       * the course event.
 318       * Default value is set to CONTEXT_USER
 319       *
 320       * @return \stdClass The context object.
 321       */
 322      protected function calculate_context() {
 323          global $USER, $DB;
 324  
 325          $context = null;
 326          if (isset($this->properties->categoryid) && $this->properties->categoryid > 0) {
 327              $context = \context_coursecat::instance($this->properties->categoryid);
 328          } else if (isset($this->properties->courseid) && $this->properties->courseid > 0) {
 329              $context = \context_course::instance($this->properties->courseid);
 330          } else if (isset($this->properties->course) && $this->properties->course > 0) {
 331              $context = \context_course::instance($this->properties->course);
 332          } else if (isset($this->properties->groupid) && $this->properties->groupid > 0) {
 333              $group = $DB->get_record('groups', array('id' => $this->properties->groupid));
 334              $context = \context_course::instance($group->courseid);
 335          } else if (isset($this->properties->userid) && $this->properties->userid > 0
 336              && $this->properties->userid == $USER->id) {
 337              $context = \context_user::instance($this->properties->userid);
 338          } else if (isset($this->properties->userid) && $this->properties->userid > 0
 339              && $this->properties->userid != $USER->id &&
 340              !empty($this->properties->modulename) &&
 341              isset($this->properties->instance) && $this->properties->instance > 0) {
 342              $cm = get_coursemodule_from_instance($this->properties->modulename, $this->properties->instance, 0,
 343                  false, MUST_EXIST);
 344              $context = \context_course::instance($cm->course);
 345          } else {
 346              $context = \context_user::instance($this->properties->userid);
 347          }
 348  
 349          return $context;
 350      }
 351  
 352      /**
 353       * Returns the context for this event. The context is calculated
 354       * the first time is is requested and then stored in a member
 355       * variable to be returned each subsequent time.
 356       *
 357       * This is a magical getter function that will be called when
 358       * ever the context property is accessed, e.g. $event->context.
 359       *
 360       * @return context
 361       */
 362      protected function get_context() {
 363          if (!isset($this->properties->context)) {
 364              $this->properties->context = $this->calculate_context();
 365          }
 366  
 367          return $this->properties->context;
 368      }
 369  
 370      /**
 371       * Returns an array of editoroptions for this event.
 372       *
 373       * @return array event editor options
 374       */
 375      protected function get_editoroptions() {
 376          return $this->editoroptions;
 377      }
 378  
 379      /**
 380       * Returns an event description: Called by __get
 381       * Please use $blah = $event->description;
 382       *
 383       * @return string event description
 384       */
 385      protected function get_description() {
 386          global $CFG;
 387  
 388          require_once($CFG->libdir . '/filelib.php');
 389  
 390          if ($this->_description === null) {
 391              // Check if we have already resolved the context for this event.
 392              if ($this->editorcontext === null) {
 393                  // Switch on the event type to decide upon the appropriate context to use for this event.
 394                  $this->editorcontext = $this->get_context();
 395                  if (!calendar_is_valid_eventtype($this->properties->eventtype)) {
 396                      return clean_text($this->properties->description, $this->properties->format);
 397                  }
 398              }
 399  
 400              // Work out the item id for the editor, if this is a repeated event
 401              // then the files will be associated with the original.
 402              if (!empty($this->properties->repeatid) && $this->properties->repeatid > 0) {
 403                  $itemid = $this->properties->repeatid;
 404              } else {
 405                  $itemid = $this->properties->id;
 406              }
 407  
 408              // Convert file paths in the description so that things display correctly.
 409              $this->_description = file_rewrite_pluginfile_urls($this->properties->description, 'pluginfile.php',
 410                  $this->editorcontext->id, 'calendar', 'event_description', $itemid);
 411              // Clean the text so no nasties get through.
 412              $this->_description = clean_text($this->_description, $this->properties->format);
 413          }
 414  
 415          // Finally return the description.
 416          return $this->_description;
 417      }
 418  
 419      /**
 420       * Return the number of repeat events there are in this events series.
 421       *
 422       * @return int number of event repeated
 423       */
 424      public function count_repeats() {
 425          global $DB;
 426          if (!empty($this->properties->repeatid)) {
 427              $this->properties->eventrepeats = $DB->count_records('event',
 428                  array('repeatid' => $this->properties->repeatid));
 429              // We don't want to count ourselves.
 430              $this->properties->eventrepeats--;
 431          }
 432          return $this->properties->eventrepeats;
 433      }
 434  
 435      /**
 436       * Update or create an event within the database
 437       *
 438       * Pass in a object containing the event properties and this function will
 439       * insert it into the database and deal with any associated files
 440       *
 441       * Capability checking should be performed if the user is directly manipulating the event
 442       * and no other capability has been tested. However if the event is not being manipulated
 443       * directly by the user and another capability has been checked for them to do this then
 444       * capabilites should not be checked.
 445       *
 446       * For example if a user is editing an event in the calendar the check should be true,
 447       * but if you are updating an event in an activities settings are changed then the calendar
 448       * capabilites should not be checked.
 449       *
 450       * @see self::create()
 451       * @see self::update()
 452       *
 453       * @param \stdClass $data object of event
 454       * @param bool $checkcapability If Moodle should check the user can manage the calendar events for this call or not.
 455       * @return bool event updated
 456       */
 457      public function update($data, $checkcapability=true) {
 458          global $DB, $USER;
 459  
 460          foreach ($data as $key => $value) {
 461              $this->properties->$key = $value;
 462          }
 463  
 464          $this->properties->timemodified = time();
 465          $usingeditor = (!empty($this->properties->description) && is_array($this->properties->description));
 466  
 467          // Prepare event data.
 468          $eventargs = array(
 469              'context' => $this->get_context(),
 470              'objectid' => $this->properties->id,
 471              'other' => array(
 472                  'repeatid' => empty($this->properties->repeatid) ? 0 : $this->properties->repeatid,
 473                  'timestart' => $this->properties->timestart,
 474                  'name' => $this->properties->name
 475              )
 476          );
 477  
 478          if (empty($this->properties->id) || $this->properties->id < 1) {
 479              if ($checkcapability) {
 480                  if (!calendar_add_event_allowed($this->properties)) {
 481                      throw new \moodle_exception('nopermissiontoupdatecalendar');
 482                  }
 483              }
 484  
 485              if ($usingeditor) {
 486                  switch ($this->properties->eventtype) {
 487                      case 'user':
 488                          $this->properties->courseid = 0;
 489                          $this->properties->course = 0;
 490                          $this->properties->groupid = 0;
 491                          $this->properties->userid = $USER->id;
 492                          break;
 493                      case 'site':
 494                          $this->properties->courseid = SITEID;
 495                          $this->properties->course = SITEID;
 496                          $this->properties->groupid = 0;
 497                          $this->properties->userid = $USER->id;
 498                          break;
 499                      case 'course':
 500                          $this->properties->groupid = 0;
 501                          $this->properties->userid = $USER->id;
 502                          break;
 503                      case 'category':
 504                          $this->properties->groupid = 0;
 505                          $this->properties->category = 0;
 506                          $this->properties->userid = $USER->id;
 507                          break;
 508                      case 'group':
 509                          $this->properties->userid = $USER->id;
 510                          break;
 511                      default:
 512                          // We should NEVER get here, but just incase we do lets fail gracefully.
 513                          $usingeditor = false;
 514                          break;
 515                  }
 516  
 517                  // If we are actually using the editor, we recalculate the context because some default values
 518                  // were set when calculate_context() was called from the constructor.
 519                  if ($usingeditor) {
 520                      $this->properties->context = $this->calculate_context();
 521                      $this->editorcontext = $this->get_context();
 522                  }
 523  
 524                  $editor = $this->properties->description;
 525                  $this->properties->format = $this->properties->description['format'];
 526                  $this->properties->description = $this->properties->description['text'];
 527              }
 528  
 529              // Insert the event into the database.
 530              $this->properties->id = $DB->insert_record('event', $this->properties);
 531  
 532              if ($usingeditor) {
 533                  $this->properties->description = file_save_draft_area_files(
 534                      $editor['itemid'],
 535                      $this->editorcontext->id,
 536                      'calendar',
 537                      'event_description',
 538                      $this->properties->id,
 539                      $this->editoroptions,
 540                      $editor['text'],
 541                      $this->editoroptions['forcehttps']);
 542                  $DB->set_field('event', 'description', $this->properties->description,
 543                      array('id' => $this->properties->id));
 544              }
 545  
 546              // Log the event entry.
 547              $eventargs['objectid'] = $this->properties->id;
 548              $eventargs['context'] = $this->get_context();
 549              $event = \core\event\calendar_event_created::create($eventargs);
 550              $event->trigger();
 551  
 552              $repeatedids = array();
 553  
 554              if (!empty($this->properties->repeat)) {
 555                  $this->properties->repeatid = $this->properties->id;
 556                  $DB->set_field('event', 'repeatid', $this->properties->repeatid, array('id' => $this->properties->id));
 557  
 558                  $eventcopy = clone($this->properties);
 559                  unset($eventcopy->id);
 560  
 561                  $timestart = new \DateTime('@' . $eventcopy->timestart);
 562                  $timestart->setTimezone(\core_date::get_user_timezone_object());
 563  
 564                  for ($i = 1; $i < $eventcopy->repeats; $i++) {
 565  
 566                      $timestart->add(new \DateInterval('P7D'));
 567                      $eventcopy->timestart = $timestart->getTimestamp();
 568  
 569                      // Get the event id for the log record.
 570                      $eventcopyid = $DB->insert_record('event', $eventcopy);
 571  
 572                      // If the context has been set delete all associated files.
 573                      if ($usingeditor) {
 574                          $fs = get_file_storage();
 575                          $files = $fs->get_area_files($this->editorcontext->id, 'calendar', 'event_description',
 576                              $this->properties->id);
 577                          foreach ($files as $file) {
 578                              $fs->create_file_from_storedfile(array('itemid' => $eventcopyid), $file);
 579                          }
 580                      }
 581  
 582                      $repeatedids[] = $eventcopyid;
 583  
 584                      // Trigger an event.
 585                      $eventargs['objectid'] = $eventcopyid;
 586                      $eventargs['other']['timestart'] = $eventcopy->timestart;
 587                      $event = \core\event\calendar_event_created::create($eventargs);
 588                      $event->trigger();
 589                  }
 590              }
 591  
 592              return true;
 593          } else {
 594  
 595              if ($checkcapability) {
 596                  if (!calendar_edit_event_allowed($this->properties)) {
 597                      throw new \moodle_exception('nopermissiontoupdatecalendar');
 598                  }
 599              }
 600  
 601              if ($usingeditor) {
 602                  if ($this->editorcontext !== null) {
 603                      $this->properties->description = file_save_draft_area_files(
 604                          $this->properties->description['itemid'],
 605                          $this->editorcontext->id,
 606                          'calendar',
 607                          'event_description',
 608                          $this->properties->id,
 609                          $this->editoroptions,
 610                          $this->properties->description['text'],
 611                          $this->editoroptions['forcehttps']);
 612                  } else {
 613                      $this->properties->format = $this->properties->description['format'];
 614                      $this->properties->description = $this->properties->description['text'];
 615                  }
 616              }
 617  
 618              $event = $DB->get_record('event', array('id' => $this->properties->id));
 619  
 620              $updaterepeated = (!empty($this->properties->repeatid) && !empty($this->properties->repeateditall));
 621  
 622              if ($updaterepeated) {
 623  
 624                  $sqlset = 'name = ?,
 625                             description = ?,
 626                             timeduration = ?,
 627                             timemodified = ?,
 628                             groupid = ?,
 629                             courseid = ?';
 630  
 631                  // Note: Group and course id may not be set. If not, keep their current values.
 632                  $params = [
 633                      $this->properties->name,
 634                      $this->properties->description,
 635                      $this->properties->timeduration,
 636                      time(),
 637                      isset($this->properties->groupid) ? $this->properties->groupid : $event->groupid,
 638                      isset($this->properties->courseid) ? $this->properties->courseid : $event->courseid,
 639                  ];
 640  
 641                  // Note: Only update start date, if it was changed by the user.
 642                  if ($this->properties->timestart != $event->timestart) {
 643                      $timestartoffset = $this->properties->timestart - $event->timestart;
 644                      $sqlset .= ', timestart = timestart + ?';
 645                      $params[] = $timestartoffset;
 646                  }
 647  
 648                  // Note: Only update location, if it was changed by the user.
 649                  $updatelocation = (!empty($this->properties->location) && $this->properties->location !== $event->location);
 650                  if ($updatelocation) {
 651                      $sqlset .= ', location = ?';
 652                      $params[] = $this->properties->location;
 653                  }
 654  
 655                  // Update all.
 656                  $sql = "UPDATE {event}
 657                             SET $sqlset
 658                           WHERE repeatid = ?";
 659  
 660                  $params[] = $event->repeatid;
 661                  $DB->execute($sql, $params);
 662  
 663                  // Trigger an update event for each of the calendar event.
 664                  $events = $DB->get_records('event', array('repeatid' => $event->repeatid), '', '*');
 665                  foreach ($events as $calendarevent) {
 666                      $eventargs['objectid'] = $calendarevent->id;
 667                      $eventargs['other']['timestart'] = $calendarevent->timestart;
 668                      $event = \core\event\calendar_event_updated::create($eventargs);
 669                      $event->add_record_snapshot('event', $calendarevent);
 670                      $event->trigger();
 671                  }
 672              } else {
 673                  $DB->update_record('event', $this->properties);
 674                  $event = self::load($this->properties->id);
 675                  $this->properties = $event->properties();
 676  
 677                  // Trigger an update event.
 678                  $event = \core\event\calendar_event_updated::create($eventargs);
 679                  $event->add_record_snapshot('event', $this->properties);
 680                  $event->trigger();
 681              }
 682  
 683              return true;
 684          }
 685      }
 686  
 687      /**
 688       * Deletes an event and if selected an repeated events in the same series
 689       *
 690       * This function deletes an event, any associated events if $deleterepeated=true,
 691       * and cleans up any files associated with the events.
 692       *
 693       * @see self::delete()
 694       *
 695       * @param bool $deleterepeated  delete event repeatedly
 696       * @return bool succession of deleting event
 697       */
 698      public function delete($deleterepeated = false) {
 699          global $DB;
 700  
 701          // If $this->properties->id is not set then something is wrong.
 702          if (empty($this->properties->id)) {
 703              debugging('Attempting to delete an event before it has been loaded', DEBUG_DEVELOPER);
 704              return false;
 705          }
 706          $calevent = $DB->get_record('event',  array('id' => $this->properties->id), '*', MUST_EXIST);
 707          // Delete the event.
 708          $DB->delete_records('event', array('id' => $this->properties->id));
 709  
 710          // Trigger an event for the delete action.
 711          $eventargs = array(
 712              'context' => $this->get_context(),
 713              'objectid' => $this->properties->id,
 714              'other' => array(
 715                  'repeatid' => empty($this->properties->repeatid) ? 0 : $this->properties->repeatid,
 716                  'timestart' => $this->properties->timestart,
 717                  'name' => $this->properties->name
 718              ));
 719          $event = \core\event\calendar_event_deleted::create($eventargs);
 720          $event->add_record_snapshot('event', $calevent);
 721          $event->trigger();
 722  
 723          // If we are deleting parent of a repeated event series, promote the next event in the series as parent.
 724          if (($this->properties->id == $this->properties->repeatid) && !$deleterepeated) {
 725              $newparent = $DB->get_field_sql("SELECT id from {event} where repeatid = ? order by id ASC",
 726                  array($this->properties->id), IGNORE_MULTIPLE);
 727              if (!empty($newparent)) {
 728                  $DB->execute("UPDATE {event} SET repeatid = ? WHERE repeatid = ?",
 729                      array($newparent, $this->properties->id));
 730                  // Get all records where the repeatid is the same as the event being removed.
 731                  $events = $DB->get_records('event', array('repeatid' => $newparent));
 732                  // For each of the returned events trigger an update event.
 733                  foreach ($events as $calendarevent) {
 734                      // Trigger an event for the update.
 735                      $eventargs['objectid'] = $calendarevent->id;
 736                      $eventargs['other']['timestart'] = $calendarevent->timestart;
 737                      $event = \core\event\calendar_event_updated::create($eventargs);
 738                      $event->add_record_snapshot('event', $calendarevent);
 739                      $event->trigger();
 740                  }
 741              }
 742          }
 743  
 744          // If the editor context hasn't already been set then set it now.
 745          if ($this->editorcontext === null) {
 746              $this->editorcontext = $this->get_context();
 747          }
 748  
 749          // If the context has been set delete all associated files.
 750          if ($this->editorcontext !== null) {
 751              $fs = get_file_storage();
 752              $files = $fs->get_area_files($this->editorcontext->id, 'calendar', 'event_description', $this->properties->id);
 753              foreach ($files as $file) {
 754                  $file->delete();
 755              }
 756          }
 757  
 758          // If we need to delete repeated events then we will fetch them all and delete one by one.
 759          if ($deleterepeated && !empty($this->properties->repeatid) && $this->properties->repeatid > 0) {
 760              // Get all records where the repeatid is the same as the event being removed.
 761              $events = $DB->get_records('event', array('repeatid' => $this->properties->repeatid));
 762              // For each of the returned events populate an event object and call delete.
 763              // make sure the arg passed is false as we are already deleting all repeats.
 764              foreach ($events as $event) {
 765                  $event = new calendar_event($event);
 766                  $event->delete(false);
 767              }
 768          }
 769  
 770          return true;
 771      }
 772  
 773      /**
 774       * Fetch all event properties.
 775       *
 776       * This function returns all of the events properties as an object and optionally
 777       * can prepare an editor for the description field at the same time. This is
 778       * designed to work when the properties are going to be used to set the default
 779       * values of a moodle forms form.
 780       *
 781       * @param bool $prepareeditor If set to true a editor is prepared for use with
 782       *              the mforms editor element. (for description)
 783       * @return \stdClass Object containing event properties
 784       */
 785      public function properties($prepareeditor = false) {
 786          global $DB;
 787  
 788          // First take a copy of the properties. We don't want to actually change the
 789          // properties or we'd forever be converting back and forwards between an
 790          // editor formatted description and not.
 791          $properties = clone($this->properties);
 792          // Clean the description here.
 793          $properties->description = clean_text($properties->description, $properties->format);
 794  
 795          // If set to true we need to prepare the properties for use with an editor
 796          // and prepare the file area.
 797          if ($prepareeditor) {
 798  
 799              // We may or may not have a property id. If we do then we need to work
 800              // out the context so we can copy the existing files to the draft area.
 801              if (!empty($properties->id)) {
 802  
 803                  if ($properties->eventtype === 'site') {
 804                      // Site context.
 805                      $this->editorcontext = $this->get_context();
 806                  } else if ($properties->eventtype === 'user') {
 807                      // User context.
 808                      $this->editorcontext = $this->get_context();
 809                  } else if ($properties->eventtype === 'group' || $properties->eventtype === 'course') {
 810                      // First check the course is valid.
 811                      $course = $DB->get_record('course', array('id' => $properties->courseid));
 812                      if (!$course) {
 813                          throw new \moodle_exception('invalidcourse');
 814                      }
 815                      // Course context.
 816                      $this->editorcontext = $this->get_context();
 817                      // We have a course and are within the course context so we had
 818                      // better use the courses max bytes value.
 819                      $this->editoroptions['maxbytes'] = $course->maxbytes;
 820                  } else if ($properties->eventtype === 'category') {
 821                      // First check the course is valid.
 822                      \core_course_category::get($properties->categoryid, MUST_EXIST, true);
 823                      // Course context.
 824                      $this->editorcontext = $this->get_context();
 825                  } else {
 826                      // If we get here we have a custom event type as used by some
 827                      // modules. In this case the event will have been added by
 828                      // code and we won't need the editor.
 829                      $this->editoroptions['maxbytes'] = 0;
 830                      $this->editoroptions['maxfiles'] = 0;
 831                  }
 832  
 833                  if (empty($this->editorcontext) || empty($this->editorcontext->id)) {
 834                      $contextid = false;
 835                  } else {
 836                      // Get the context id that is what we really want.
 837                      $contextid = $this->editorcontext->id;
 838                  }
 839              } else {
 840  
 841                  // If we get here then this is a new event in which case we don't need a
 842                  // context as there is no existing files to copy to the draft area.
 843                  $contextid = null;
 844              }
 845  
 846              // If the contextid === false we don't support files so no preparing
 847              // a draft area.
 848              if ($contextid !== false) {
 849                  // Just encase it has already been submitted.
 850                  $draftiddescription = file_get_submitted_draft_itemid('description');
 851                  // Prepare the draft area, this copies existing files to the draft area as well.
 852                  $properties->description = file_prepare_draft_area($draftiddescription, $contextid, 'calendar',
 853                      'event_description', $properties->id, $this->editoroptions, $properties->description);
 854              } else {
 855                  $draftiddescription = 0;
 856              }
 857  
 858              // Structure the description field as the editor requires.
 859              $properties->description = array('text' => $properties->description, 'format' => $properties->format,
 860                  'itemid' => $draftiddescription);
 861          }
 862  
 863          // Finally return the properties.
 864          return $properties;
 865      }
 866  
 867      /**
 868       * Toggles the visibility of an event
 869       *
 870       * @param null|bool $force If it is left null the events visibility is flipped,
 871       *                   If it is false the event is made hidden, if it is true it
 872       *                   is made visible.
 873       * @return bool if event is successfully updated, toggle will be visible
 874       */
 875      public function toggle_visibility($force = null) {
 876          global $DB;
 877  
 878          // Set visible to the default if it is not already set.
 879          if (empty($this->properties->visible)) {
 880              $this->properties->visible = 1;
 881          }
 882  
 883          if ($force === true || ($force !== false && $this->properties->visible == 0)) {
 884              // Make this event visible.
 885              $this->properties->visible = 1;
 886          } else {
 887              // Make this event hidden.
 888              $this->properties->visible = 0;
 889          }
 890  
 891          // Update the database to reflect this change.
 892          $success = $DB->set_field('event', 'visible', $this->properties->visible, array('id' => $this->properties->id));
 893          $calendarevent = $DB->get_record('event',  array('id' => $this->properties->id), '*', MUST_EXIST);
 894  
 895          // Prepare event data.
 896          $eventargs = array(
 897              'context' => $this->get_context(),
 898              'objectid' => $this->properties->id,
 899              'other' => array(
 900                  'repeatid' => empty($this->properties->repeatid) ? 0 : $this->properties->repeatid,
 901                  'timestart' => $this->properties->timestart,
 902                  'name' => $this->properties->name
 903              )
 904          );
 905          $event = \core\event\calendar_event_updated::create($eventargs);
 906          $event->add_record_snapshot('event', $calendarevent);
 907          $event->trigger();
 908  
 909          return $success;
 910      }
 911  
 912      /**
 913       * Returns an event object when provided with an event id.
 914       *
 915       * This function makes use of MUST_EXIST, if the event id passed in is invalid
 916       * it will result in an exception being thrown.
 917       *
 918       * @param int|object $param event object or event id
 919       * @return calendar_event
 920       */
 921      public static function load($param) {
 922          global $DB;
 923          if (is_object($param)) {
 924              $event = new calendar_event($param);
 925          } else {
 926              $event = $DB->get_record('event', array('id' => (int)$param), '*', MUST_EXIST);
 927              $event = new calendar_event($event);
 928          }
 929          return $event;
 930      }
 931  
 932      /**
 933       * Creates a new event and returns an event object.
 934       *
 935       * Capability checking should be performed if the user is directly creating the event
 936       * and no other capability has been tested. However if the event is not being created
 937       * directly by the user and another capability has been checked for them to do this then
 938       * capabilites should not be checked.
 939       *
 940       * For example if a user is creating an event in the calendar the check should be true,
 941       * but if you are creating an event in an activity when it is created then the calendar
 942       * capabilites should not be checked.
 943       *
 944       * @param \stdClass|array $properties An object containing event properties
 945       * @param bool $checkcapability If Moodle should check the user can manage the calendar events for this call or not.
 946       * @throws \coding_exception
 947       *
 948       * @return calendar_event|bool The event object or false if it failed
 949       */
 950      public static function create($properties, $checkcapability = true) {
 951          if (is_array($properties)) {
 952              $properties = (object)$properties;
 953          }
 954          if (!is_object($properties)) {
 955              throw new \coding_exception('When creating an event properties should be either an object or an assoc array');
 956          }
 957          $event = new calendar_event($properties);
 958          if ($event->update($properties, $checkcapability)) {
 959              return $event;
 960          } else {
 961              return false;
 962          }
 963      }
 964  
 965      /**
 966       * Format the event name using the external API.
 967       *
 968       * This function should we used when text formatting is required in external functions.
 969       *
 970       * @return string Formatted name.
 971       */
 972      public function format_external_name() {
 973          if ($this->editorcontext === null) {
 974              // Switch on the event type to decide upon the appropriate context to use for this event.
 975              $this->editorcontext = $this->get_context();
 976          }
 977  
 978          return external_format_string($this->properties->name, $this->editorcontext->id);
 979      }
 980  
 981      /**
 982       * Format the text using the external API.
 983       *
 984       * This function should we used when text formatting is required in external functions.
 985       *
 986       * @return array an array containing the text formatted and the text format
 987       */
 988      public function format_external_text() {
 989  
 990          if ($this->editorcontext === null) {
 991              // Switch on the event type to decide upon the appropriate context to use for this event.
 992              $this->editorcontext = $this->get_context();
 993  
 994              if (!calendar_is_valid_eventtype($this->properties->eventtype)) {
 995                  // We don't have a context here, do a normal format_text.
 996                  return external_format_text($this->properties->description, $this->properties->format, $this->editorcontext->id);
 997              }
 998          }
 999  
1000          // Work out the item id for the editor, if this is a repeated event then the files will be associated with the original.
1001          if (!empty($this->properties->repeatid) && $this->properties->repeatid > 0) {
1002              $itemid = $this->properties->repeatid;
1003          } else {
1004              $itemid = $this->properties->id;
1005          }
1006  
1007          return external_format_text($this->properties->description, $this->properties->format, $this->editorcontext->id,
1008              'calendar', 'event_description', $itemid);
1009      }
1010  }
1011  
1012  /**
1013   * Calendar information class
1014   *
1015   * This class is used simply to organise the information pertaining to a calendar
1016   * and is used primarily to make information easily available.
1017   *
1018   * @package core_calendar
1019   * @category calendar
1020   * @copyright 2010 Sam Hemelryk
1021   * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
1022   */
1023  class calendar_information {
1024  
1025      /**
1026       * @var int The timestamp
1027       *
1028       * Rather than setting the day, month and year we will set a timestamp which will be able
1029       * to be used by multiple calendars.
1030       */
1031      public $time;
1032  
1033      /** @var int A course id */
1034      public $courseid = null;
1035  
1036      /** @var array An array of categories */
1037      public $categories = array();
1038  
1039      /** @var int The current category */
1040      public $categoryid = null;
1041  
1042      /** @var array An array of courses */
1043      public $courses = array();
1044  
1045      /** @var array An array of groups */
1046      public $groups = array();
1047  
1048      /** @var array An array of users */
1049      public $users = array();
1050  
1051      /** @var context The anticipated context that the calendar is viewed in */
1052      public $context = null;
1053  
1054      /** @var string The calendar's view mode. */
1055      protected $viewmode;
1056  
1057      /**
1058       * Creates a new instance
1059       *
1060       * @param int $day the number of the day
1061       * @param int $month the number of the month
1062       * @param int $year the number of the year
1063       * @param int $time the unixtimestamp representing the date we want to view, this is used instead of $calmonth
1064       *     and $calyear to support multiple calendars
1065       */
1066      public function __construct($day = 0, $month = 0, $year = 0, $time = 0) {
1067          // If a day, month and year were passed then convert it to a timestamp. If these were passed
1068          // then we can assume the day, month and year are passed as Gregorian, as no where in core
1069          // should we be passing these values rather than the time. This is done for BC.
1070          if (!empty($day) || !empty($month) || !empty($year)) {
1071              $date = usergetdate(time());
1072              if (empty($day)) {
1073                  $day = $date['mday'];
1074              }
1075              if (empty($month)) {
1076                  $month = $date['mon'];
1077              }
1078              if (empty($year)) {
1079                  $year =  $date['year'];
1080              }
1081              if (checkdate($month, $day, $year)) {
1082                  $time = make_timestamp($year, $month, $day);
1083              } else {
1084                  $time = time();
1085              }
1086          }
1087  
1088          $this->set_time($time);
1089      }
1090  
1091      /**
1092       * Creates and set up a instance.
1093       *
1094       * @param   int                     $time the unixtimestamp representing the date we want to view.
1095       * @param   int                     $courseid The ID of the course the user wishes to view.
1096       * @param   int                     $categoryid The ID of the category the user wishes to view
1097       *                                  If a courseid is specified, this value is ignored.
1098       * @return  calendar_information
1099       */
1100      public static function create($time, int $courseid, int $categoryid = null) : calendar_information {
1101          $calendar = new static(0, 0, 0, $time);
1102          if ($courseid != SITEID && !empty($courseid)) {
1103              // Course ID must be valid and existing.
1104              $course = get_course($courseid);
1105              $calendar->context = context_course::instance($course->id);
1106  
1107              if (!$course->visible && !is_role_switched($course->id)) {
1108                  require_capability('moodle/course:viewhiddencourses', $calendar->context);
1109              }
1110  
1111              $courses = [$course->id => $course];
1112              $category = (\core_course_category::get($course->category, MUST_EXIST, true))->get_db_record();
1113          } else if (!empty($categoryid)) {
1114              $course = get_site();
1115              $courses = calendar_get_default_courses(null, 'id, category, groupmode, groupmodeforce');
1116  
1117              // Filter available courses to those within this category or it's children.
1118              $ids = [$categoryid];
1119              $category = \core_course_category::get($categoryid);
1120              $ids = array_merge($ids, array_keys($category->get_children()));
1121              $courses = array_filter($courses, function($course) use ($ids) {
1122                  return array_search($course->category, $ids) !== false;
1123              });
1124              $category = $category->get_db_record();
1125  
1126              $calendar->context = context_coursecat::instance($categoryid);
1127          } else {
1128              $course = get_site();
1129              $courses = calendar_get_default_courses(null, 'id, category, groupmode, groupmodeforce');
1130              $category = null;
1131  
1132              $calendar->context = context_system::instance();
1133          }
1134  
1135          $calendar->set_sources($course, $courses, $category);
1136  
1137          return $calendar;
1138      }
1139  
1140      /**
1141       * Set the time period of this instance.
1142       *
1143       * @param   int $time the unixtimestamp representing the date we want to view.
1144       * @return  $this
1145       */
1146      public function set_time($time = null) {
1147          if (empty($time)) {
1148              $this->time = time();
1149          } else {
1150              $this->time = $time;
1151          }
1152  
1153          return $this;
1154      }
1155  
1156      /**
1157       * Initialize calendar information
1158       *
1159       * @deprecated 3.4
1160       * @param stdClass $course object
1161       * @param array $coursestoload An array of courses [$course->id => $course]
1162       * @param bool $ignorefilters options to use filter
1163       */
1164      public function prepare_for_view(stdClass $course, array $coursestoload, $ignorefilters = false) {
1165          debugging('The prepare_for_view() function has been deprecated. Please update your code to use set_sources()',
1166                  DEBUG_DEVELOPER);
1167          $this->set_sources($course, $coursestoload);
1168      }
1169  
1170      /**
1171       * Set the sources for events within the calendar.
1172       *
1173       * If no category is provided, then the category path for the current
1174       * course will be used.
1175       *
1176       * @param   stdClass    $course The current course being viewed.
1177       * @param   stdClass[]  $courses The list of all courses currently accessible.
1178       * @param   stdClass    $category The current category to show.
1179       */
1180      public function set_sources(stdClass $course, array $courses, stdClass $category = null) {
1181          global $USER;
1182  
1183          // A cousre must always be specified.
1184          $this->course = $course;
1185          $this->courseid = $course->id;
1186  
1187          list($courseids, $group, $user) = calendar_set_filters($courses);
1188          $this->courses = $courseids;
1189          $this->groups = $group;
1190          $this->users = $user;
1191  
1192          // Do not show category events by default.
1193          $this->categoryid = null;
1194          $this->categories = null;
1195  
1196          // Determine the correct category information to show.
1197          // When called with a course, the category of that course is usually included too.
1198          // When a category was specifically requested, it should be requested with the site id.
1199          if (SITEID !== $this->courseid) {
1200              // A specific course was requested.
1201              // Fetch the category that this course is in, along with all parents.
1202              // Do not include child categories of this category, as the user many not have enrolments in those siblings or children.
1203              $category = \core_course_category::get($course->category, MUST_EXIST, true);
1204              $this->categoryid = $category->id;
1205  
1206              $this->categories = $category->get_parents();
1207              $this->categories[] = $category->id;
1208          } else if (null !== $category && $category->id > 0) {
1209              // A specific category was requested.
1210              // Fetch all parents of this category, along with all children too.
1211              $category = \core_course_category::get($category->id);
1212              $this->categoryid = $category->id;
1213  
1214              // Build the category list.
1215              // This includes the current category.
1216              $this->categories = $category->get_parents();
1217              $this->categories[] = $category->id;
1218              $this->categories = array_merge($this->categories, $category->get_all_children_ids());
1219          } else if (SITEID === $this->courseid) {
1220              // The site was requested.
1221              // Fetch all categories where this user has any enrolment, and all categories that this user can manage.
1222  
1223              // Grab the list of categories that this user has courses in.
1224              $coursecategories = array_flip(array_map(function($course) {
1225                  return $course->category;
1226              }, $courses));
1227  
1228              $calcatcache = cache::make('core', 'calendar_categories');
1229              $this->categories = $calcatcache->get('site');
1230              if ($this->categories === false) {
1231                  // Use the category id as the key in the following array. That way we do not have to remove duplicates.
1232                  $categories = [];
1233                  foreach (\core_course_category::get_all() as $category) {
1234                      if (isset($coursecategories[$category->id]) ||
1235                              has_capability('moodle/category:manage', $category->get_context(), $USER, false)) {
1236                          // If the user has access to a course in this category or can manage the category,
1237                          // then they can see all parent categories too.
1238                          $categories[$category->id] = true;
1239                          foreach ($category->get_parents() as $catid) {
1240                              $categories[$catid] = true;
1241                          }
1242                      }
1243                  }
1244                  $this->categories = array_keys($categories);
1245                  $calcatcache->set('site', $this->categories);
1246              }
1247          }
1248      }
1249  
1250      /**
1251       * Ensures the date for the calendar is correct and either sets it to now
1252       * or throws a moodle_exception if not
1253       *
1254       * @param bool $defaultonow use current time
1255       * @throws moodle_exception
1256       * @return bool validation of checkdate
1257       */
1258      public function checkdate($defaultonow = true) {
1259          if (!checkdate($this->month, $this->day, $this->year)) {
1260              if ($defaultonow) {
1261                  $now = usergetdate(time());
1262                  $this->day = intval($now['mday']);
1263                  $this->month = intval($now['mon']);
1264                  $this->year = intval($now['year']);
1265                  return true;
1266              } else {
1267                  throw new moodle_exception('invaliddate');
1268              }
1269          }
1270          return true;
1271      }
1272  
1273      /**
1274       * Gets todays timestamp for the calendar
1275       *
1276       * @return int today timestamp
1277       */
1278      public function timestamp_today() {
1279          return $this->time;
1280      }
1281      /**
1282       * Gets tomorrows timestamp for the calendar
1283       *
1284       * @return int tomorrow timestamp
1285       */
1286      public function timestamp_tomorrow() {
1287          return strtotime('+1 day', $this->time);
1288      }
1289      /**
1290       * Adds the pretend blocks for the calendar
1291       *
1292       * @param core_calendar_renderer $renderer
1293       * @param bool $showfilters display filters, false is set as default
1294       * @param string|null $view preference view options (eg: day, month, upcoming)
1295       */
1296      public function add_sidecalendar_blocks(core_calendar_renderer $renderer, $showfilters=false, $view=null) {
1297          global $PAGE;
1298  
1299          if (!has_capability('moodle/block:view', $PAGE->context) ) {
1300              return;
1301          }
1302  
1303          if ($showfilters) {
1304              $filters = new block_contents();
1305              $filters->content = $renderer->event_filter();
1306              $filters->footer = '';
1307              $filters->title = get_string('eventskey', 'calendar');
1308              $renderer->add_pretend_calendar_block($filters, BLOCK_POS_RIGHT);
1309          }
1310      }
1311  
1312      /**
1313       * Getter method for the calendar's view mode.
1314       *
1315       * @return string
1316       */
1317      public function get_viewmode(): string {
1318          return $this->viewmode;
1319      }
1320  
1321      /**
1322       * Setter method for the calendar's view mode.
1323       *
1324       * @param string $viewmode
1325       */
1326      public function set_viewmode(string $viewmode): void {
1327          $this->viewmode = $viewmode;
1328      }
1329  }
1330  
1331  /**
1332   * Get calendar events.
1333   *
1334   * @param int $tstart Start time of time range for events
1335   * @param int $tend End time of time range for events
1336   * @param array|int|boolean $users array of users, user id or boolean for all/no user events
1337   * @param array|int|boolean $groups array of groups, group id or boolean for all/no group events
1338   * @param array|int|boolean $courses array of courses, course id or boolean for all/no course events
1339   * @param boolean $withduration whether only events starting within time range selected
1340   *                              or events in progress/already started selected as well
1341   * @param boolean $ignorehidden whether to select only visible events or all events
1342   * @param array|int|boolean $categories array of categories, category id or boolean for all/no course events
1343   * @return array $events of selected events or an empty array if there aren't any (or there was an error)
1344   */
1345  function calendar_get_events($tstart, $tend, $users, $groups, $courses,
1346          $withduration = true, $ignorehidden = true, $categories = []) {
1347      global $DB;
1348  
1349      $whereclause = '';
1350      $params = array();
1351      // Quick test.
1352      if (empty($users) && empty($groups) && empty($courses) && empty($categories)) {
1353          return array();
1354      }
1355  
1356      if ((is_array($users) && !empty($users)) or is_numeric($users)) {
1357          // Events from a number of users
1358          if(!empty($whereclause)) $whereclause .= ' OR';
1359          list($insqlusers, $inparamsusers) = $DB->get_in_or_equal($users, SQL_PARAMS_NAMED);
1360          $whereclause .= " (e.userid $insqlusers AND e.courseid = 0 AND e.groupid = 0 AND e.categoryid = 0)";
1361          $params = array_merge($params, $inparamsusers);
1362      } else if($users === true) {
1363          // Events from ALL users
1364          if(!empty($whereclause)) $whereclause .= ' OR';
1365          $whereclause .= ' (e.userid != 0 AND e.courseid = 0 AND e.groupid = 0 AND e.categoryid = 0)';
1366      } else if($users === false) {
1367          // No user at all, do nothing
1368      }
1369  
1370      if ((is_array($groups) && !empty($groups)) or is_numeric($groups)) {
1371          // Events from a number of groups
1372          if(!empty($whereclause)) $whereclause .= ' OR';
1373          list($insqlgroups, $inparamsgroups) = $DB->get_in_or_equal($groups, SQL_PARAMS_NAMED);
1374          $whereclause .= " e.groupid $insqlgroups ";
1375          $params = array_merge($params, $inparamsgroups);
1376      } else if($groups === true) {
1377          // Events from ALL groups
1378          if(!empty($whereclause)) $whereclause .= ' OR ';
1379          $whereclause .= ' e.groupid != 0';
1380      }
1381      // boolean false (no groups at all): we don't need to do anything
1382  
1383      if ((is_array($courses) && !empty($courses)) or is_numeric($courses)) {
1384          if(!empty($whereclause)) $whereclause .= ' OR';
1385          list($insqlcourses, $inparamscourses) = $DB->get_in_or_equal($courses, SQL_PARAMS_NAMED);
1386          $whereclause .= " (e.groupid = 0 AND e.courseid $insqlcourses)";
1387          $params = array_merge($params, $inparamscourses);
1388      } else if ($courses === true) {
1389          // Events from ALL courses
1390          if(!empty($whereclause)) $whereclause .= ' OR';
1391          $whereclause .= ' (e.groupid = 0 AND e.courseid != 0)';
1392      }
1393  
1394      if ((is_array($categories) && !empty($categories)) || is_numeric($categories)) {
1395          if (!empty($whereclause)) {
1396              $whereclause .= ' OR';
1397          }
1398          list($insqlcategories, $inparamscategories) = $DB->get_in_or_equal($categories, SQL_PARAMS_NAMED);
1399          $whereclause .= " (e.groupid = 0 AND e.courseid = 0 AND e.categoryid $insqlcategories)";
1400          $params = array_merge($params, $inparamscategories);
1401      } else if ($categories === true) {
1402          // Events from ALL categories.
1403          if (!empty($whereclause)) {
1404              $whereclause .= ' OR';
1405          }
1406          $whereclause .= ' (e.groupid = 0 AND e.courseid = 0 AND e.categoryid != 0)';
1407      }
1408  
1409      // Security check: if, by now, we have NOTHING in $whereclause, then it means
1410      // that NO event-selecting clauses were defined. Thus, we won't be returning ANY
1411      // events no matter what. Allowing the code to proceed might return a completely
1412      // valid query with only time constraints, thus selecting ALL events in that time frame!
1413      if(empty($whereclause)) {
1414          return array();
1415      }
1416  
1417      if($withduration) {
1418          $timeclause = '(e.timestart >= '.$tstart.' OR e.timestart + e.timeduration > '.$tstart.') AND e.timestart <= '.$tend;
1419      }
1420      else {
1421          $timeclause = 'e.timestart >= '.$tstart.' AND e.timestart <= '.$tend;
1422      }
1423      if(!empty($whereclause)) {
1424          // We have additional constraints
1425          $whereclause = $timeclause.' AND ('.$whereclause.')';
1426      }
1427      else {
1428          // Just basic time filtering
1429          $whereclause = $timeclause;
1430      }
1431  
1432      if ($ignorehidden) {
1433          $whereclause .= ' AND e.visible = 1';
1434      }
1435  
1436      $sql = "SELECT e.*
1437                FROM {event} e
1438           LEFT JOIN {modules} m ON e.modulename = m.name
1439                  -- Non visible modules will have a value of 0.
1440               WHERE (m.visible = 1 OR m.visible IS NULL) AND $whereclause
1441            ORDER BY e.timestart";
1442      $events = $DB->get_records_sql($sql, $params);
1443  
1444      if ($events === false) {
1445          $events = array();
1446      }
1447      return $events;
1448  }
1449  
1450  /**
1451   * Return the days of the week.
1452   *
1453   * @return array array of days
1454   */
1455  function calendar_get_days() {
1456      $calendartype = \core_calendar\type_factory::get_calendar_instance();
1457      return $calendartype->get_weekdays();
1458  }
1459  
1460  /**
1461   * Get the subscription from a given id.
1462   *
1463   * @since Moodle 2.5
1464   * @param int $id id of the subscription
1465   * @return stdClass Subscription record from DB
1466   * @throws moodle_exception for an invalid id
1467   */
1468  function calendar_get_subscription($id) {
1469      global $DB;
1470  
1471      $cache = \cache::make('core', 'calendar_subscriptions');
1472      $subscription = $cache->get($id);
1473      if (empty($subscription)) {
1474          $subscription = $DB->get_record('event_subscriptions', array('id' => $id), '*', MUST_EXIST);
1475          $cache->set($id, $subscription);
1476      }
1477  
1478      return $subscription;
1479  }
1480  
1481  /**
1482   * Gets the first day of the week.
1483   *
1484   * Used to be define('CALENDAR_STARTING_WEEKDAY', blah);
1485   *
1486   * @return int
1487   */
1488  function calendar_get_starting_weekday() {
1489      $calendartype = \core_calendar\type_factory::get_calendar_instance();
1490      return $calendartype->get_starting_weekday();
1491  }
1492  
1493  /**
1494   * Get a HTML link to a course.
1495   *
1496   * @param int|stdClass $course the course id or course object
1497   * @return string a link to the course (as HTML); empty if the course id is invalid
1498   */
1499  function calendar_get_courselink($course) {
1500      if (!$course) {
1501          return '';
1502      }
1503  
1504      if (!is_object($course)) {
1505          $course = calendar_get_course_cached($coursecache, $course);
1506      }
1507      $context = \context_course::instance($course->id);
1508      $fullname = format_string($course->fullname, true, array('context' => $context));
1509      $url = new \moodle_url('/course/view.php', array('id' => $course->id));
1510      $link = \html_writer::link($url, $fullname);
1511  
1512      return $link;
1513  }
1514  
1515  /**
1516   * Get current module cache.
1517   *
1518   * Only use this method if you do not know courseid. Otherwise use:
1519   * get_fast_modinfo($courseid)->instances[$modulename][$instance]
1520   *
1521   * @param array $modulecache in memory module cache
1522   * @param string $modulename name of the module
1523   * @param int $instance module instance number
1524   * @return stdClass|bool $module information
1525   */
1526  function calendar_get_module_cached(&$modulecache, $modulename, $instance) {
1527      if (!isset($modulecache[$modulename . '_' . $instance])) {
1528          $modulecache[$modulename . '_' . $instance] = get_coursemodule_from_instance($modulename, $instance);
1529      }
1530  
1531      return $modulecache[$modulename . '_' . $instance];
1532  }
1533  
1534  /**
1535   * Get current course cache.
1536   *
1537   * @param array $coursecache list of course cache
1538   * @param int $courseid id of the course
1539   * @return stdClass $coursecache[$courseid] return the specific course cache
1540   */
1541  function calendar_get_course_cached(&$coursecache, $courseid) {
1542      if (!isset($coursecache[$courseid])) {
1543          $coursecache[$courseid] = get_course($courseid);
1544      }
1545      return $coursecache[$courseid];
1546  }
1547  
1548  /**
1549   * Get group from groupid for calendar display
1550   *
1551   * @param int $groupid
1552   * @return stdClass group object with fields 'id', 'name' and 'courseid'
1553   */
1554  function calendar_get_group_cached($groupid) {
1555      static $groupscache = array();
1556      if (!isset($groupscache[$groupid])) {
1557          $groupscache[$groupid] = groups_get_group($groupid, 'id,name,courseid');
1558      }
1559      return $groupscache[$groupid];
1560  }
1561  
1562  /**
1563   * Add calendar event metadata
1564   *
1565   * @deprecated since 3.9
1566   *
1567   * @param stdClass $event event info
1568   * @return stdClass $event metadata
1569   */
1570  function calendar_add_event_metadata($event) {
1571      debugging('This function is no longer used', DEBUG_DEVELOPER);
1572      global $CFG, $OUTPUT;
1573  
1574      // Support multilang in event->name.
1575      $event->name = format_string($event->name, true);
1576  
1577      if (!empty($event->modulename)) { // Activity event.
1578          // The module name is set. I will assume that it has to be displayed, and
1579          // also that it is an automatically-generated event. And of course that the
1580          // instace id and modulename are set correctly.
1581          $instances = get_fast_modinfo($event->courseid)->get_instances_of($event->modulename);
1582          if (!array_key_exists($event->instance, $instances)) {
1583              return;
1584          }
1585          $module = $instances[$event->instance];
1586  
1587          $modulename = $module->get_module_type_name(false);
1588          if (get_string_manager()->string_exists($event->eventtype, $event->modulename)) {
1589              // Will be used as alt text if the event icon.
1590              $eventtype = get_string($event->eventtype, $event->modulename);
1591          } else {
1592              $eventtype = '';
1593          }
1594  
1595          $event->icon = '<img src="' . s($module->get_icon_url()) . '" alt="' . s($eventtype) .
1596              '" title="' . s($modulename) . '" class="icon" />';
1597          $event->referer = html_writer::link($module->url, $event->name);
1598          $event->courselink = calendar_get_courselink($module->get_course());
1599          $event->cmid = $module->id;
1600      } else if ($event->courseid == SITEID) { // Site event.
1601          $event->icon = '<img src="' . $OUTPUT->image_url('i/siteevent') . '" alt="' .
1602              get_string('siteevent', 'calendar') . '" class="icon" />';
1603          $event->cssclass = 'calendar_event_site';
1604      } else if ($event->courseid != 0 && $event->courseid != SITEID && $event->groupid == 0) { // Course event.
1605          $event->icon = '<img src="' . $OUTPUT->image_url('i/courseevent') . '" alt="' .
1606              get_string('courseevent', 'calendar') . '" class="icon" />';
1607          $event->courselink = calendar_get_courselink($event->courseid);
1608          $event->cssclass = 'calendar_event_course';
1609      } else if ($event->groupid) { // Group event.
1610          if ($group = calendar_get_group_cached($event->groupid)) {
1611              $groupname = format_string($group->name, true, \context_course::instance($group->courseid));
1612          } else {
1613              $groupname = '';
1614          }
1615          $event->icon = \html_writer::empty_tag('image', array('src' => $OUTPUT->image_url('i/groupevent'),
1616              'alt' => get_string('groupevent', 'calendar'), 'title' => $groupname, 'class' => 'icon'));
1617          $event->courselink = calendar_get_courselink($event->courseid) . ', ' . $groupname;
1618          $event->cssclass = 'calendar_event_group';
1619      } else if ($event->userid) { // User event.
1620          $event->icon = '<img src="' . $OUTPUT->image_url('i/userevent') . '" alt="' .
1621              get_string('userevent', 'calendar') . '" class="icon" />';
1622          $event->cssclass = 'calendar_event_user';
1623      }
1624  
1625      return $event;
1626  }
1627  
1628  /**
1629   * Get calendar events by id.
1630   *
1631   * @since Moodle 2.5
1632   * @param array $eventids list of event ids
1633   * @return array Array of event entries, empty array if nothing found
1634   */
1635  function calendar_get_events_by_id($eventids) {
1636      global $DB;
1637  
1638      if (!is_array($eventids) || empty($eventids)) {
1639          return array();
1640      }
1641  
1642      list($wheresql, $params) = $DB->get_in_or_equal($eventids);
1643      $wheresql = "id $wheresql";
1644  
1645      return $DB->get_records_select('event', $wheresql, $params);
1646  }
1647  
1648  /**
1649   * Get control options for calendar.
1650   *
1651   * @param string $type of calendar
1652   * @param array $data calendar information
1653   * @return string $content return available control for the calender in html
1654   */
1655  function calendar_top_controls($type, $data) {
1656      global $PAGE, $OUTPUT;
1657  
1658      // Get the calendar type we are using.
1659      $calendartype = \core_calendar\type_factory::get_calendar_instance();
1660  
1661      $content = '';
1662  
1663      // Ensure course id passed if relevant.
1664      $courseid = '';
1665      if (!empty($data['id'])) {
1666          $courseid = '&amp;course=' . $data['id'];
1667      }
1668  
1669      // If we are passing a month and year then we need to convert this to a timestamp to
1670      // support multiple calendars. No where in core should these be passed, this logic
1671      // here is for third party plugins that may use this function.
1672      if (!empty($data['m']) && !empty($date['y'])) {
1673          if (!isset($data['d'])) {
1674              $data['d'] = 1;
1675          }
1676          if (!checkdate($data['m'], $data['d'], $data['y'])) {
1677              $time = time();
1678          } else {
1679              $time = make_timestamp($data['y'], $data['m'], $data['d']);
1680          }
1681      } else if (!empty($data['time'])) {
1682          $time = $data['time'];
1683      } else {
1684          $time = time();
1685      }
1686  
1687      // Get the date for the calendar type.
1688      $date = $calendartype->timestamp_to_date_array($time);
1689  
1690      $urlbase = $PAGE->url;
1691  
1692      // We need to get the previous and next months in certain cases.
1693      if ($type == 'frontpage' || $type == 'course' || $type == 'month') {
1694          $prevmonth = calendar_sub_month($date['mon'], $date['year']);
1695          $prevmonthtime = $calendartype->convert_to_gregorian($prevmonth[1], $prevmonth[0], 1);
1696          $prevmonthtime = make_timestamp($prevmonthtime['year'], $prevmonthtime['month'], $prevmonthtime['day'],
1697              $prevmonthtime['hour'], $prevmonthtime['minute']);
1698  
1699          $nextmonth = calendar_add_month($date['mon'], $date['year']);
1700          $nextmonthtime = $calendartype->convert_to_gregorian($nextmonth[1], $nextmonth[0], 1);
1701          $nextmonthtime = make_timestamp($nextmonthtime['year'], $nextmonthtime['month'], $nextmonthtime['day'],
1702              $nextmonthtime['hour'], $nextmonthtime['minute']);
1703      }
1704  
1705      switch ($type) {
1706          case 'frontpage':
1707              $prevlink = calendar_get_link_previous(get_string('monthprev', 'calendar'), $urlbase, false, false, false,
1708                  true, $prevmonthtime);
1709              $nextlink = calendar_get_link_next(get_string('monthnext', 'calendar'), $urlbase, false, false, false, true,
1710                  $nextmonthtime);
1711              $calendarlink = calendar_get_link_href(new \moodle_url(CALENDAR_URL . 'view.php', array('view' => 'month')),
1712                  false, false, false, $time);
1713  
1714              if (!empty($data['id'])) {
1715                  $calendarlink->param('course', $data['id']);
1716              }
1717  
1718              $right = $nextlink;
1719  
1720              $content .= \html_writer::start_tag('div', array('class' => 'calendar-controls'));
1721              $content .= $prevlink . '<span class="hide"> | </span>';
1722              $content .= \html_writer::tag('span', \html_writer::link($calendarlink,
1723                  userdate($time, get_string('strftimemonthyear')), array('title' => get_string('monththis', 'calendar'))
1724              ), array('class' => 'current'));
1725              $content .= '<span class="hide"> | </span>' . $right;
1726              $content .= "<span class=\"clearer\"><!-- --></span>\n";
1727              $content .= \html_writer::end_tag('div');
1728  
1729              break;
1730          case 'course':
1731              $prevlink = calendar_get_link_previous(get_string('monthprev', 'calendar'), $urlbase, false, false, false,
1732                  true, $prevmonthtime);
1733              $nextlink = calendar_get_link_next(get_string('monthnext', 'calendar'), $urlbase, false, false, false,
1734                  true, $nextmonthtime);
1735              $calendarlink = calendar_get_link_href(new \moodle_url(CALENDAR_URL . 'view.php', array('view' => 'month')),
1736                  false, false, false, $time);
1737  
1738              if (!empty($data['id'])) {
1739                  $calendarlink->param('course', $data['id']);
1740              }
1741  
1742              $content .= \html_writer::start_tag('div', array('class' => 'calendar-controls'));
1743              $content .= $prevlink . '<span class="hide"> | </span>';
1744              $content .= \html_writer::tag('span', \html_writer::link($calendarlink,
1745                  userdate($time, get_string('strftimemonthyear')), array('title' => get_string('monththis', 'calendar'))
1746              ), array('class' => 'current'));
1747              $content .= '<span class="hide"> | </span>' . $nextlink;
1748              $content .= "<span class=\"clearer\"><!-- --></span>";
1749              $content .= \html_writer::end_tag('div');
1750              break;
1751          case 'upcoming':
1752              $calendarlink = calendar_get_link_href(new \moodle_url(CALENDAR_URL . 'view.php', array('view' => 'upcoming')),
1753                  false, false, false, $time);
1754              if (!empty($data['id'])) {
1755                  $calendarlink->param('course', $data['id']);
1756              }
1757              $calendarlink = \html_writer::link($calendarlink, userdate($time, get_string('strftimemonthyear')));
1758              $content .= \html_writer::tag('div', $calendarlink, array('class' => 'centered'));
1759              break;
1760          case 'display':
1761              $calendarlink = calendar_get_link_href(new \moodle_url(CALENDAR_URL . 'view.php', array('view' => 'month')),
1762                  false, false, false, $time);
1763              if (!empty($data['id'])) {
1764                  $calendarlink->param('course', $data['id']);
1765              }
1766              $calendarlink = \html_writer::link($calendarlink, userdate($time, get_string('strftimemonthyear')));
1767              $content .= \html_writer::tag('h3', $calendarlink);
1768              break;
1769          case 'month':
1770              $prevlink = calendar_get_link_previous(userdate($prevmonthtime, get_string('strftimemonthyear')),
1771                  'view.php?view=month' . $courseid . '&amp;', false, false, false, false, $prevmonthtime);
1772              $nextlink = calendar_get_link_next(userdate($nextmonthtime, get_string('strftimemonthyear')),
1773                  'view.php?view=month' . $courseid . '&amp;', false, false, false, false, $nextmonthtime);
1774  
1775              $content .= \html_writer::start_tag('div', array('class' => 'calendar-controls'));
1776              $content .= $prevlink . '<span class="hide"> | </span>';
1777              $content .= $OUTPUT->heading(userdate($time, get_string('strftimemonthyear')), 2, 'current');
1778              $content .= '<span class="hide"> | </span>' . $nextlink;
1779              $content .= '<span class="clearer"><!-- --></span>';
1780              $content .= \html_writer::end_tag('div')."\n";
1781              break;
1782          case 'day':
1783              $days = calendar_get_days();
1784  
1785              $prevtimestamp = strtotime('-1 day', $time);
1786              $nexttimestamp = strtotime('+1 day', $time);
1787  
1788              $prevdate = $calendartype->timestamp_to_date_array($prevtimestamp);
1789              $nextdate = $calendartype->timestamp_to_date_array($nexttimestamp);
1790  
1791              $prevname = $days[$prevdate['wday']]['fullname'];
1792              $nextname = $days[$nextdate['wday']]['fullname'];
1793              $prevlink = calendar_get_link_previous($prevname, 'view.php?view=day' . $courseid . '&amp;', false, false,
1794                  false, false, $prevtimestamp);
1795              $nextlink = calendar_get_link_next($nextname, 'view.php?view=day' . $courseid . '&amp;', false, false, false,
1796                  false, $nexttimestamp);
1797  
1798              $content .= \html_writer::start_tag('div', array('class' => 'calendar-controls'));
1799              $content .= $prevlink;
1800              $content .= '<span class="hide"> | </span><span class="current">' .userdate($time,
1801                      get_string('strftimedaydate')) . '</span>';
1802              $content .= '<span class="hide"> | </span>' . $nextlink;
1803              $content .= "<span class=\"clearer\"><!-- --></span>";
1804              $content .= \html_writer::end_tag('div') . "\n";
1805  
1806              break;
1807      }
1808  
1809      return $content;
1810  }
1811  
1812  /**
1813   * Return the representation day.
1814   *
1815   * @param int $tstamp Timestamp in GMT
1816   * @param int|bool $now current Unix timestamp
1817   * @param bool $usecommonwords
1818   * @return string the formatted date/time
1819   */
1820  function calendar_day_representation($tstamp, $now = false, $usecommonwords = true) {
1821      static $shortformat;
1822  
1823      if (empty($shortformat)) {
1824          $shortformat = get_string('strftimedayshort');
1825      }
1826  
1827      if ($now === false) {
1828          $now = time();
1829      }
1830  
1831      // To have it in one place, if a change is needed.
1832      $formal = userdate($tstamp, $shortformat);
1833  
1834      $datestamp = usergetdate($tstamp);
1835      $datenow = usergetdate($now);
1836  
1837      if ($usecommonwords == false) {
1838          // We don't want words, just a date.
1839          return $formal;
1840      } else if ($datestamp['year'] == $datenow['year'] && $datestamp['yday'] == $datenow['yday']) {
1841          return get_string('today', 'calendar');
1842      } else if (($datestamp['year'] == $datenow['year'] && $datestamp['yday'] == $datenow['yday'] - 1 ) ||
1843          ($datestamp['year'] == $datenow['year'] - 1 && $datestamp['mday'] == 31 && $datestamp['mon'] == 12
1844              && $datenow['yday'] == 1)) {
1845          return get_string('yesterday', 'calendar');
1846      } else if (($datestamp['year'] == $datenow['year'] && $datestamp['yday'] == $datenow['yday'] + 1 ) ||
1847          ($datestamp['year'] == $datenow['year'] + 1 && $datenow['mday'] == 31 && $datenow['mon'] == 12
1848              && $datestamp['yday'] == 1)) {
1849          return get_string('tomorrow', 'calendar');
1850      } else {
1851          return $formal;
1852      }
1853  }
1854  
1855  /**
1856   * return the formatted representation time.
1857   *
1858  
1859   * @param int $time the timestamp in UTC, as obtained from the database
1860   * @return string the formatted date/time
1861   */
1862  function calendar_time_representation($time) {
1863      static $langtimeformat = null;
1864  
1865      if ($langtimeformat === null) {
1866          $langtimeformat = get_string('strftimetime');
1867      }
1868  
1869      $timeformat = get_user_preferences('calendar_timeformat');
1870      if (empty($timeformat)) {
1871          $timeformat = get_config(null, 'calendar_site_timeformat');
1872      }
1873  
1874      // Allow language customization of selected time format.
1875      if ($timeformat === CALENDAR_TF_12) {
1876          $timeformat = get_string('strftimetime12', 'langconfig');
1877      } else if ($timeformat === CALENDAR_TF_24) {
1878          $timeformat = get_string('strftimetime24', 'langconfig');
1879      }
1880  
1881      return userdate($time, empty($timeformat) ? $langtimeformat : $timeformat);
1882  }
1883  
1884  /**
1885   * Adds day, month, year arguments to a URL and returns a moodle_url object.
1886   *
1887   * @param string|moodle_url $linkbase
1888   * @param int $d The number of the day.
1889   * @param int $m The number of the month.
1890   * @param int $y The number of the year.
1891   * @param int $time the unixtime, used for multiple calendar support. The values $d,
1892   *     $m and $y are kept for backwards compatibility.
1893   * @return moodle_url|null $linkbase
1894   */
1895  function calendar_get_link_href($linkbase, $d, $m, $y, $time = 0) {
1896      if (empty($linkbase)) {
1897          return null;
1898      }
1899  
1900      if (!($linkbase instanceof \moodle_url)) {
1901          $linkbase = new \moodle_url($linkbase);
1902      }
1903  
1904      $linkbase->param('time', calendar_get_timestamp($d, $m, $y, $time));
1905  
1906      return $linkbase;
1907  }
1908  
1909  /**
1910   * Build and return a previous month HTML link, with an arrow.
1911   *
1912   * @param string $text The text label.
1913   * @param string|moodle_url $linkbase The URL stub.
1914   * @param int $d The number of the date.
1915   * @param int $m The number of the month.
1916   * @param int $y year The number of the year.
1917   * @param bool $accesshide Default visible, or hide from all except screenreaders.
1918   * @param int $time the unixtime, used for multiple calendar support. The values $d,
1919   *     $m and $y are kept for backwards compatibility.
1920   * @return string HTML string.
1921   */
1922  function calendar_get_link_previous($text, $linkbase, $d, $m, $y, $accesshide = false, $time = 0) {
1923      $href = calendar_get_link_href(new \moodle_url($linkbase), $d, $m, $y, $time);
1924  
1925      if (empty($href)) {
1926          return $text;
1927      }
1928  
1929      $attrs = [
1930          'data-time' => calendar_get_timestamp($d, $m, $y, $time),
1931          'data-drop-zone' => 'nav-link',
1932      ];
1933  
1934      return link_arrow_left($text, $href->out(false), $accesshide, 'previous', $attrs);
1935  }
1936  
1937  /**
1938   * Build and return a next month HTML link, with an arrow.
1939   *
1940   * @param string $text The text label.
1941   * @param string|moodle_url $linkbase The URL stub.
1942   * @param int $d the number of the Day
1943   * @param int $m The number of the month.
1944   * @param int $y The number of the year.
1945   * @param bool $accesshide Default visible, or hide from all except screenreaders.
1946   * @param int $time the unixtime, used for multiple calendar support. The values $d,
1947   *     $m and $y are kept for backwards compatibility.
1948   * @return string HTML string.
1949   */
1950  function calendar_get_link_next($text, $linkbase, $d, $m, $y, $accesshide = false, $time = 0) {
1951      $href = calendar_get_link_href(new \moodle_url($linkbase), $d, $m, $y, $time);
1952  
1953      if (empty($href)) {
1954          return $text;
1955      }
1956  
1957      $attrs = [
1958          'data-time' => calendar_get_timestamp($d, $m, $y, $time),
1959          'data-drop-zone' => 'nav-link',
1960      ];
1961  
1962      return link_arrow_right($text, $href->out(false), $accesshide, 'next', $attrs);
1963  }
1964  
1965  /**
1966   * Return the number of days in month.
1967   *
1968   * @param int $month the number of the month.
1969   * @param int $year the number of the year
1970   * @return int
1971   */
1972  function calendar_days_in_month($month, $year) {
1973      $calendartype = \core_calendar\type_factory::get_calendar_instance();
1974      return $calendartype->get_num_days_in_month($year, $month);
1975  }
1976  
1977  /**
1978   * Get the next following month.
1979   *
1980   * @param int $month the number of the month.
1981   * @param int $year the number of the year.
1982   * @return array the following month
1983   */
1984  function calendar_add_month($month, $year) {
1985      $calendartype = \core_calendar\type_factory::get_calendar_instance();
1986      return $calendartype->get_next_month($year, $month);
1987  }
1988  
1989  /**
1990   * Get the previous month.
1991   *
1992   * @param int $month the number of the month.
1993   * @param int $year the number of the year.
1994   * @return array previous month
1995   */
1996  function calendar_sub_month($month, $year) {
1997      $calendartype = \core_calendar\type_factory::get_calendar_instance();
1998      return $calendartype->get_prev_month($year, $month);
1999  }
2000  
2001  /**
2002   * Get per-day basis events
2003   *
2004   * @param array $events list of events
2005   * @param int $month the number of the month
2006   * @param int $year the number of the year
2007   * @param array $eventsbyday event on specific day
2008   * @param array $durationbyday duration of the event in days
2009   * @param array $typesbyday event type (eg: site, course, user, or group)
2010   * @param array $courses list of courses
2011   * @return void
2012   */
2013  function calendar_events_by_day($events, $month, $year, &$eventsbyday, &$durationbyday, &$typesbyday, &$courses) {
2014      $calendartype = \core_calendar\type_factory::get_calendar_instance();
2015  
2016      $eventsbyday = array();
2017      $typesbyday = array();
2018      $durationbyday = array();
2019  
2020      if ($events === false) {
2021          return;
2022      }
2023  
2024      foreach ($events as $event) {
2025          $startdate = $calendartype->timestamp_to_date_array($event->timestart);
2026          if ($event->timeduration) {
2027              $enddate = $calendartype->timestamp_to_date_array($event->timestart + $event->timeduration - 1);
2028          } else {
2029              $enddate = $startdate;
2030          }
2031  
2032          // Simple arithmetic: $year * 13 + $month is a distinct integer for each distinct ($year, $month) pair.
2033          if (!($startdate['year'] * 13 + $startdate['mon'] <= $year * 13 + $month) &&
2034              ($enddate['year'] * 13 + $enddate['mon'] >= $year * 13 + $month)) {
2035              continue;
2036          }
2037  
2038          $eventdaystart = intval($startdate['mday']);
2039  
2040          if ($startdate['mon'] == $month && $startdate['year'] == $year) {
2041              // Give the event to its day.
2042              $eventsbyday[$eventdaystart][] = $event->id;
2043  
2044              // Mark the day as having such an event.
2045              if ($event->courseid == SITEID && $event->groupid == 0) {
2046                  $typesbyday[$eventdaystart]['startsite'] = true;
2047                  // Set event class for site event.
2048                  $events[$event->id]->class = 'calendar_event_site';
2049              } else if ($event->courseid != 0 && $event->courseid != SITEID && $event->groupid == 0) {
2050                  $typesbyday[$eventdaystart]['startcourse'] = true;
2051                  // Set event class for course event.
2052                  $events[$event->id]->class = 'calendar_event_course';
2053              } else if ($event->groupid) {
2054                  $typesbyday[$eventdaystart]['startgroup'] = true;
2055                  // Set event class for group event.
2056                  $events[$event->id]->class = 'calendar_event_group';
2057              } else if ($event->userid) {
2058                  $typesbyday[$eventdaystart]['startuser'] = true;
2059                  // Set event class for user event.
2060                  $events[$event->id]->class = 'calendar_event_user';
2061              }
2062          }
2063  
2064          if ($event->timeduration == 0) {
2065              // Proceed with the next.
2066              continue;
2067          }
2068  
2069          // The event starts on $month $year or before.
2070          if ($startdate['mon'] == $month && $startdate['year'] == $year) {
2071              $lowerbound = intval($startdate['mday']);
2072          } else {
2073              $lowerbound = 0;
2074          }
2075  
2076          // Also, it ends on $month $year or later.
2077          if ($enddate['mon'] == $month && $enddate['year'] == $year) {
2078              $upperbound = intval($enddate['mday']);
2079          } else {
2080              $upperbound = calendar_days_in_month($month, $year);
2081          }
2082  
2083          // Mark all days between $lowerbound and $upperbound (inclusive) as duration.
2084          for ($i = $lowerbound + 1; $i <= $upperbound; ++$i) {
2085              $durationbyday[$i][] = $event->id;
2086              if ($event->courseid == SITEID && $event->groupid == 0) {
2087                  $typesbyday[$i]['durationsite'] = true;
2088              } else if ($event->courseid != 0 && $event->courseid != SITEID && $event->groupid == 0) {
2089                  $typesbyday[$i]['durationcourse'] = true;
2090              } else if ($event->groupid) {
2091                  $typesbyday[$i]['durationgroup'] = true;
2092              } else if ($event->userid) {
2093                  $typesbyday[$i]['durationuser'] = true;
2094              }
2095          }
2096  
2097      }
2098      return;
2099  }
2100  
2101  /**
2102   * Returns the courses to load events for.
2103   *
2104   * @param array $courseeventsfrom An array of courses to load calendar events for
2105   * @param bool $ignorefilters specify the use of filters, false is set as default
2106   * @param stdClass $user The user object. This defaults to the global $USER object.
2107   * @return array An array of courses, groups, and user to load calendar events for based upon filters
2108   */
2109  function calendar_set_filters(array $courseeventsfrom, $ignorefilters = false, stdClass $user = null) {
2110      global $CFG, $USER;
2111  
2112      if (is_null($user)) {
2113          $user = $USER;
2114      }
2115  
2116      $courses = array();
2117      $userid = false;
2118      $group = false;
2119  
2120      // Get the capabilities that allow seeing group events from all groups.
2121      $allgroupscaps = array('moodle/site:accessallgroups', 'moodle/calendar:manageentries');
2122  
2123      $isvaliduser = !empty($user->id);
2124  
2125      if ($ignorefilters || calendar_show_event_type(CALENDAR_EVENT_COURSE, $user)) {
2126          $courses = array_keys($courseeventsfrom);
2127      }
2128      if ($ignorefilters || calendar_show_event_type(CALENDAR_EVENT_SITE, $user)) {
2129          $courses[] = SITEID;
2130      }
2131      $courses = array_unique($courses);
2132      sort($courses);
2133  
2134      if (!empty($courses) && in_array(SITEID, $courses)) {
2135          // Sort courses for consistent colour highlighting.
2136          // Effectively ignoring SITEID as setting as last course id.
2137          $key = array_search(SITEID, $courses);
2138          unset($courses[$key]);
2139          $courses[] = SITEID;
2140      }
2141  
2142      if ($ignorefilters || ($isvaliduser && calendar_show_event_type(CALENDAR_EVENT_USER, $user))) {
2143          $userid = $user->id;
2144      }
2145  
2146      if (!empty($courseeventsfrom) && (calendar_show_event_type(CALENDAR_EVENT_GROUP, $user) || $ignorefilters)) {
2147  
2148          if (count($courseeventsfrom) == 1) {
2149              $course = reset($courseeventsfrom);
2150              if (has_any_capability($allgroupscaps, \context_course::instance($course->id))) {
2151                  $coursegroups = groups_get_all_groups($course->id, 0, 0, 'g.id');
2152                  $group = array_keys($coursegroups);
2153              }
2154          }
2155          if ($group === false) {
2156              if (!empty($CFG->calendar_adminseesall) && has_any_capability($allgroupscaps, \context_system::instance())) {
2157                  $group = true;
2158              } else if ($isvaliduser) {
2159                  $groupids = array();
2160                  foreach ($courseeventsfrom as $courseid => $course) {
2161                      if ($course->groupmode != NOGROUPS || !$course->groupmodeforce) {
2162                          // If this course has groups, show events from all of those related to the current user.
2163                          $coursegroups = groups_get_user_groups($course->id, $user->id);
2164                          $groupids = array_merge($groupids, $coursegroups['0']);
2165                      }
2166                  }
2167                  if (!empty($groupids)) {
2168                      $group = $groupids;
2169                  }
2170              }
2171          }
2172      }
2173      if (empty($courses)) {
2174          $courses = false;
2175      }
2176  
2177      return array($courses, $group, $userid);
2178  }
2179  
2180  /**
2181   * Can current user manage a non user event in system context.
2182   *
2183   * @param calendar_event|stdClass $event event object
2184   * @return boolean
2185   */
2186  function calendar_can_manage_non_user_event_in_system($event) {
2187      $sitecontext = \context_system::instance();
2188      $isuserevent = $event->eventtype == 'user';
2189      $canmanageentries = has_capability('moodle/calendar:manageentries', $sitecontext);
2190      // If user has manageentries at site level and it's not user event, return true.
2191      if ($canmanageentries && !$isuserevent) {
2192          return true;
2193      }
2194  
2195      return false;
2196  }
2197  
2198  /**
2199   * Return the capability for viewing a calendar event.
2200   *
2201   * @param calendar_event $event event object
2202   * @return boolean
2203   */
2204  function calendar_view_event_allowed(calendar_event $event) {
2205      global $USER;
2206  
2207      // Anyone can see site events.
2208      if ($event->courseid && $event->courseid == SITEID) {
2209          return true;
2210      }
2211  
2212      if (calendar_can_manage_non_user_event_in_system($event)) {
2213          return true;
2214      }
2215  
2216      if (!empty($event->groupid)) {
2217          // If it is a group event we need to be able to manage events in the course, or be in the group.
2218          if (has_capability('moodle/calendar:manageentries', $event->context) ||
2219                  has_capability('moodle/calendar:managegroupentries', $event->context)) {
2220              return true;
2221          }
2222  
2223          $mycourses = enrol_get_my_courses('id');
2224          return isset($mycourses[$event->courseid]) && groups_is_member($event->groupid);
2225      } else if ($event->modulename) {
2226          // If this is a module event we need to be able to see the module.
2227          $coursemodules = [];
2228          $courseid = 0;
2229          // Override events do not have the courseid set.
2230          if ($event->courseid) {
2231              $courseid = $event->courseid;
2232              $coursemodules = get_fast_modinfo($event->courseid)->instances;
2233          } else {
2234              $cmraw = get_coursemodule_from_instance($event->modulename, $event->instance, 0, false, MUST_EXIST);
2235              $courseid = $cmraw->course;
2236              $coursemodules = get_fast_modinfo($cmraw->course)->instances;
2237          }
2238          $hasmodule = isset($coursemodules[$event->modulename]);
2239          $hasinstance = isset($coursemodules[$event->modulename][$event->instance]);
2240  
2241          // If modinfo doesn't know about the module, return false to be safe.
2242          if (!$hasmodule || !$hasinstance) {
2243              return false;
2244          }
2245  
2246          // Must be able to see the course and the module - MDL-59304.
2247          $cm = $coursemodules[$event->modulename][$event->instance];
2248          if (!$cm->uservisible) {
2249              return false;
2250          }
2251          $mycourses = enrol_get_my_courses('id');
2252          return isset($mycourses[$courseid]);
2253      } else if ($event->categoryid) {
2254          // If this is a category we need to be able to see the category.
2255          $cat = \core_course_category::get($event->categoryid, IGNORE_MISSING);
2256          if (!$cat) {
2257              return false;
2258          }
2259          return true;
2260      } else if (!empty($event->courseid)) {
2261          // If it is a course event we need to be able to manage events in the course, or be in the course.
2262          if (has_capability('moodle/calendar:manageentries', $event->context)) {
2263              return true;
2264          }
2265  
2266          return can_access_course(get_course($event->courseid));
2267      } else if ($event->userid) {
2268          return calendar_can_manage_user_event($event);
2269      } else {
2270          throw new moodle_exception('unknown event type');
2271      }
2272  
2273      return false;
2274  }
2275  
2276  /**
2277   * Return the capability for editing calendar event.
2278   *
2279   * @param calendar_event $event event object
2280   * @param bool $manualedit is the event being edited manually by the user
2281   * @return bool capability to edit event
2282   */
2283  function calendar_edit_event_allowed($event, $manualedit = false) {
2284      global $USER, $DB;
2285  
2286      // Must be logged in.
2287      if (!isloggedin()) {
2288          return false;
2289      }
2290  
2291      // Can not be using guest account.
2292      if (isguestuser()) {
2293          return false;
2294      }
2295  
2296      if ($manualedit && !empty($event->modulename)) {
2297          $hascallback = component_callback_exists(
2298              'mod_' . $event->modulename,
2299              'core_calendar_event_timestart_updated'
2300          );
2301  
2302          if (!$hascallback) {
2303              // If the activity hasn't implemented the correct callback
2304              // to handle changes to it's events then don't allow any
2305              // manual changes to them.
2306              return false;
2307          }
2308  
2309          $coursemodules = get_fast_modinfo($event->courseid)->instances;
2310          $hasmodule = isset($coursemodules[$event->modulename]);
2311          $hasinstance = isset($coursemodules[$event->modulename][$event->instance]);
2312  
2313          // If modinfo doesn't know about the module, return false to be safe.
2314          if (!$hasmodule || !$hasinstance) {
2315              return false;
2316          }
2317  
2318          $coursemodule = $coursemodules[$event->modulename][$event->instance];
2319          $context = context_module::instance($coursemodule->id);
2320          // This is the capability that allows a user to modify the activity
2321          // settings. Since the activity generated this event we need to check
2322          // that the current user has the same capability before allowing them
2323          // to update the event because the changes to the event will be
2324          // reflected within the activity.
2325          return has_capability('moodle/course:manageactivities', $context);
2326      }
2327  
2328      if ($manualedit && !empty($event->component)) {
2329          // TODO possibly we can later add a callback similar to core_calendar_event_timestart_updated in the modules.
2330          return false;
2331      }
2332  
2333      // You cannot edit URL based calendar subscription events presently.
2334      if (!empty($event->subscriptionid)) {
2335          if (!empty($event->subscription->url)) {
2336              // This event can be updated externally, so it cannot be edited.
2337              return false;
2338          }
2339      }
2340  
2341      if (calendar_can_manage_non_user_event_in_system($event)) {
2342          return true;
2343      }
2344  
2345      // If groupid is set, it's definitely a group event.
2346      if (!empty($event->groupid)) {
2347          // Allow users to add/edit group events if -
2348          // 1) They have manageentries for the course OR
2349          // 2) They have managegroupentries AND are in the group.
2350          $group = $DB->get_record('groups', array('id' => $event->groupid));
2351          return $group && (
2352                  has_capability('moodle/calendar:manageentries', $event->context) ||
2353                  (has_capability('moodle/calendar:managegroupentries', $event->context)
2354                      && groups_is_member($event->groupid)));
2355      } else if (!empty($event->courseid)) {
2356          // If groupid is not set, but course is set, it's definitely a course event.
2357          return has_capability('moodle/calendar:manageentries', $event->context);
2358      } else if (!empty($event->categoryid)) {
2359          // If groupid is not set, but category is set, it's definitely a category event.
2360          return has_capability('moodle/calendar:manageentries', $event->context);
2361      } else if (!empty($event->userid) && $event->userid == $USER->id) {
2362          // If course is not set, but userid id set, it's a user event.
2363          return (has_capability('moodle/calendar:manageownentries',
2364              context_user::instance($event->userid)));
2365      } else if (!empty($event->userid)) {
2366          return calendar_can_manage_user_event($event);
2367      }
2368  
2369      return false;
2370  }
2371  
2372  /**
2373   * Can current user edit/delete/add an user event?
2374   *
2375   * @param calendar_event|stdClass $event event object
2376   * @return bool
2377   */
2378  function calendar_can_manage_user_event($event): bool {
2379      global $USER;
2380  
2381      if (!($event instanceof \calendar_event)) {
2382          $event = new \calendar_event(clone($event));
2383      }
2384  
2385      $canmanage = has_capability('moodle/calendar:manageentries', $event->context);
2386      $canmanageown = has_capability('moodle/calendar:manageownentries', $event->context);
2387      $ismyevent = $event->userid == $USER->id;
2388      $isadminevent = is_siteadmin($event->userid);
2389  
2390      if ($canmanageown && $ismyevent) {
2391          return true;
2392      }
2393  
2394      // In site context, user must have login and calendar:manageentries permissions
2395      // ... to manage other user's events except admin users.
2396      if ($canmanage && !$isadminevent) {
2397          return true;
2398      }
2399  
2400      return false;
2401  }
2402  
2403  /**
2404   * Return the capability for deleting a calendar event.
2405   *
2406   * @param calendar_event $event The event object
2407   * @return bool Whether the user has permission to delete the event or not.
2408   */
2409  function calendar_delete_event_allowed($event) {
2410      // Only allow delete if you have capabilities and it is not an module or component event.
2411      return (calendar_edit_event_allowed($event) && empty($event->modulename) && empty($event->component));
2412  }
2413  
2414  /**
2415   * Returns the default courses to display on the calendar when there isn't a specific
2416   * course to display.
2417   *
2418   * @param int $courseid (optional) If passed, an additional course can be returned for admins (the current course).
2419   * @param string $fields Comma separated list of course fields to return.
2420   * @param bool $canmanage If true, this will return the list of courses the user can create events in, rather
2421   *                        than the list of courses they see events from (an admin can always add events in a course
2422   *                        calendar, even if they are not enrolled in the course).
2423   * @param int $userid (optional) The user which this function returns the default courses for.
2424   *                        By default the current user.
2425   * @return array $courses Array of courses to display
2426   */
2427  function calendar_get_default_courses($courseid = null, $fields = '*', $canmanage = false, int $userid = null) {
2428      global $CFG, $USER;
2429  
2430      if (!$userid) {
2431          if (!isloggedin()) {
2432              return array();
2433          }
2434          $userid = $USER->id;
2435      }
2436  
2437      if ((!empty($CFG->calendar_adminseesall) || $canmanage) &&
2438              has_capability('moodle/calendar:manageentries', context_system::instance(), $userid)) {
2439  
2440          // Add a c. prefix to every field as expected by get_courses function.
2441          $fieldlist = explode(',', $fields);
2442  
2443          $prefixedfields = array_map(function($value) {
2444              return 'c.' . trim(strtolower($value));
2445          }, $fieldlist);
2446  
2447          $courses = get_courses('all', 'c.shortname', implode(',', $prefixedfields));
2448      } else {
2449          $courses = enrol_get_users_courses($userid, true, $fields, 'c.shortname');
2450      }
2451  
2452      if ($courseid && $courseid != SITEID) {
2453          if (empty($courses[$courseid]) && has_capability('moodle/calendar:manageentries', context_system::instance(), $userid)) {
2454              // Allow a site admin to see calendars from courses he is not enrolled in.
2455              // This will come from $COURSE.
2456              $courses[$courseid] = get_course($courseid);
2457          }
2458      }
2459  
2460      return $courses;
2461  }
2462  
2463  /**
2464   * Get event format time.
2465   *
2466   * @param calendar_event $event event object
2467   * @param int $now current time in gmt
2468   * @param array $linkparams list of params for event link
2469   * @param bool $usecommonwords the words as formatted date/time.
2470   * @param int $showtime determine the show time GMT timestamp
2471   * @return string $eventtime link/string for event time
2472   */
2473  function calendar_format_event_time($event, $now, $linkparams = null, $usecommonwords = true, $showtime = 0) {
2474      $starttime = $event->timestart;
2475      $endtime = $event->timestart + $event->timeduration;
2476  
2477      if (empty($linkparams) || !is_array($linkparams)) {
2478          $linkparams = array();
2479      }
2480  
2481      $linkparams['view'] = 'day';
2482  
2483      // OK, now to get a meaningful display.
2484      // Check if there is a duration for this event.
2485      if ($event->timeduration) {
2486          // Get the midnight of the day the event will start.
2487          $usermidnightstart = usergetmidnight($starttime);
2488          // Get the midnight of the day the event will end.
2489          $usermidnightend = usergetmidnight($endtime);
2490          // Check if we will still be on the same day.
2491          if ($usermidnightstart == $usermidnightend) {
2492              // Check if we are running all day.
2493              if ($event->timeduration == DAYSECS) {
2494                  $time = get_string('allday', 'calendar');
2495              } else { // Specify the time we will be running this from.
2496                  $datestart = calendar_time_representation($starttime);
2497                  $dateend = calendar_time_representation($endtime);
2498                  $time = $datestart . ' <strong>&raquo;</strong> ' . $dateend;
2499              }
2500  
2501              // Set printable representation.
2502              if (!$showtime) {
2503                  $day = calendar_day_representation($event->timestart, $now, $usecommonwords);
2504                  $url = calendar_get_link_href(new \moodle_url(CALENDAR_URL . 'view.php', $linkparams), 0, 0, 0, $endtime);
2505                  $eventtime = \html_writer::link($url, $day) . ', ' . $time;
2506              } else {
2507                  $eventtime = $time;
2508              }
2509          } else { // It must spans two or more days.
2510              $daystart = calendar_day_representation($event->timestart, $now, $usecommonwords) . ', ';
2511              if ($showtime == $usermidnightstart) {
2512                  $daystart = '';
2513              }
2514              $timestart = calendar_time_representation($event->timestart);
2515              $dayend = calendar_day_representation($event->timestart + $event->timeduration, $now, $usecommonwords) . ', ';
2516              if ($showtime == $usermidnightend) {
2517                  $dayend = '';
2518              }
2519              $timeend = calendar_time_representation($event->timestart + $event->timeduration);
2520  
2521              // Set printable representation.
2522              if ($now >= $usermidnightstart && $now < strtotime('+1 day', $usermidnightstart)) {
2523                  $url = calendar_get_link_href(new \moodle_url(CALENDAR_URL . 'view.php', $linkparams), 0, 0, 0, $endtime);
2524                  $eventtime = $timestart . ' <strong>&raquo;</strong> ' . \html_writer::link($url, $dayend) . $timeend;
2525              } else {
2526                  // The event is in the future, print start and end links.
2527                  $url = calendar_get_link_href(new \moodle_url(CALENDAR_URL . 'view.php', $linkparams), 0, 0, 0, $starttime);
2528                  $eventtime = \html_writer::link($url, $daystart) . $timestart . ' <strong>&raquo;</strong> ';
2529  
2530                  $url = calendar_get_link_href(new \moodle_url(CALENDAR_URL . 'view.php', $linkparams),  0, 0, 0, $endtime);
2531                  $eventtime .= \html_writer::link($url, $dayend) . $timeend;
2532              }
2533          }
2534      } else { // There is no time duration.
2535          $time = calendar_time_representation($event->timestart);
2536          // Set printable representation.
2537          if (!$showtime) {
2538              $day = calendar_day_representation($event->timestart, $now, $usecommonwords);
2539              $url = calendar_get_link_href(new \moodle_url(CALENDAR_URL . 'view.php', $linkparams),  0, 0, 0, $starttime);
2540              $eventtime = \html_writer::link($url, $day) . ', ' . trim($time);
2541          } else {
2542              $eventtime = $time;
2543          }
2544      }
2545  
2546      // Check if It has expired.
2547      if ($event->timestart + $event->timeduration < $now) {
2548          $eventtime = '<span class="dimmed_text">' . str_replace(' href=', ' class="dimmed" href=', $eventtime) . '</span>';
2549      }
2550  
2551      return $eventtime;
2552  }
2553  
2554  /**
2555   * Format event location property
2556   *
2557   * @param calendar_event $event
2558   * @return string
2559   */
2560  function calendar_format_event_location(calendar_event $event): string {
2561      $location = format_text($event->location, FORMAT_PLAIN, ['context' => $event->context]);
2562  
2563      // If it looks like a link, convert it to one.
2564      if (preg_match('/^https?:\/\//i', $location) && clean_param($location, PARAM_URL)) {
2565          $location = \html_writer::link($location, $location, [
2566              'title' => get_string('eventnamelocation', 'core_calendar', ['name' => $event->name, 'location' => $location]),
2567          ]);
2568      }
2569  
2570      return $location;
2571  }
2572  
2573  /**
2574   * Checks to see if the requested type of event should be shown for the given user.
2575   *
2576   * @param int $type The type to check the display for (default is to display all)
2577   * @param stdClass|int|null $user The user to check for - by default the current user
2578   * @return bool True if the tyep should be displayed false otherwise
2579   */
2580  function calendar_show_event_type($type, $user = null) {
2581      $default = CALENDAR_EVENT_SITE + CALENDAR_EVENT_COURSE + CALENDAR_EVENT_GROUP + CALENDAR_EVENT_USER;
2582  
2583      if ((int)get_user_preferences('calendar_persistflt', 0, $user) === 0) {
2584          global $SESSION;
2585          if (!isset($SESSION->calendarshoweventtype)) {
2586              $SESSION->calendarshoweventtype = $default;
2587          }
2588          return $SESSION->calendarshoweventtype & $type;
2589      } else {
2590          return get_user_preferences('calendar_savedflt', $default, $user) & $type;
2591      }
2592  }
2593  
2594  /**
2595   * Sets the display of the event type given $display.
2596   *
2597   * If $display = true the event type will be shown.
2598   * If $display = false the event type will NOT be shown.
2599   * If $display = null the current value will be toggled and saved.
2600   *
2601   * @param int $type object of CALENDAR_EVENT_XXX
2602   * @param bool $display option to display event type
2603   * @param stdClass|int $user moodle user object or id, null means current user
2604   */
2605  function calendar_set_event_type_display($type, $display = null, $user = null) {
2606      $persist = (int)get_user_preferences('calendar_persistflt', 0, $user);
2607      $default = CALENDAR_EVENT_SITE + CALENDAR_EVENT_COURSE + CALENDAR_EVENT_GROUP
2608              + CALENDAR_EVENT_USER + CALENDAR_EVENT_COURSECAT;
2609      if ($persist === 0) {
2610          global $SESSION;
2611          if (!isset($SESSION->calendarshoweventtype)) {
2612              $SESSION->calendarshoweventtype = $default;
2613          }
2614          $preference = $SESSION->calendarshoweventtype;
2615      } else {
2616          $preference = get_user_preferences('calendar_savedflt', $default, $user);
2617      }
2618      $current = $preference & $type;
2619      if ($display === null) {
2620          $display = !$current;
2621      }
2622      if ($display && !$current) {
2623          $preference += $type;
2624      } else if (!$display && $current) {
2625          $preference -= $type;
2626      }
2627      if ($persist === 0) {
2628          $SESSION->calendarshoweventtype = $preference;
2629      } else {
2630          if ($preference == $default) {
2631              unset_user_preference('calendar_savedflt', $user);
2632          } else {
2633              set_user_preference('calendar_savedflt', $preference, $user);
2634          }
2635      }
2636  }
2637  
2638  /**
2639   * Get calendar's allowed types.
2640   *
2641   * @param stdClass $allowed list of allowed edit for event  type
2642   * @param stdClass|int $course object of a course or course id
2643   * @param array $groups array of groups for the given course
2644   * @param stdClass|int $category object of a category
2645   */
2646  function calendar_get_allowed_types(&$allowed, $course = null, $groups = null, $category = null) {
2647      global $USER, $DB;
2648  
2649      $allowed = new \stdClass();
2650      $allowed->user = has_capability('moodle/calendar:manageownentries', \context_system::instance());
2651      $allowed->groups = false;
2652      $allowed->courses = false;
2653      $allowed->categories = false;
2654      $allowed->site = has_capability('moodle/calendar:manageentries', \context_course::instance(SITEID));
2655      $getgroupsfunc = function($course, $context, $user) use ($groups) {
2656          if ($course->groupmode != NOGROUPS || !$course->groupmodeforce) {
2657              if (has_capability('moodle/site:accessallgroups', $context)) {
2658                  return is_null($groups) ? groups_get_all_groups($course->id) : $groups;
2659              } else {
2660                  if (is_null($groups)) {
2661                      return groups_get_all_groups($course->id, $user->id);
2662                  } else {
2663                      return array_filter($groups, function($group) use ($user) {
2664                          return isset($group->members[$user->id]);
2665                      });
2666                  }
2667              }
2668          }
2669  
2670          return false;
2671      };
2672  
2673      if (!empty($course)) {
2674          if (!is_object($course)) {
2675              $course = $DB->get_record('course', array('id' => $course), 'id, groupmode, groupmodeforce', MUST_EXIST);
2676          }
2677          if ($course->id != SITEID) {
2678              $coursecontext = \context_course::instance($course->id);
2679              $allowed->user = has_capability('moodle/calendar:manageownentries', $coursecontext);
2680  
2681              if (has_capability('moodle/calendar:manageentries', $coursecontext)) {
2682                  $allowed->courses = array($course->id => 1);
2683                  $allowed->groups = $getgroupsfunc($course, $coursecontext, $USER);
2684              } else if (has_capability('moodle/calendar:managegroupentries', $coursecontext)) {
2685                  $allowed->groups = $getgroupsfunc($course, $coursecontext, $USER);
2686              }
2687          }
2688      }
2689  
2690      if (!empty($category)) {
2691          $catcontext = \context_coursecat::instance($category->id);
2692          if (has_capability('moodle/category:manage', $catcontext)) {
2693              $allowed->categories = [$category->id => 1];
2694          }
2695      }
2696  }
2697  
2698  /**
2699   * See if user can add calendar entries at all used to print the "New Event" button.
2700   *
2701   * @param stdClass $course object of a course or course id
2702   * @return bool has the capability to add at least one event type
2703   */
2704  function calendar_user_can_add_event($course) {
2705      if (!isloggedin() || isguestuser()) {
2706          return false;
2707      }
2708  
2709      calendar_get_allowed_types($allowed, $course);
2710  
2711      return (bool)($allowed->user || $allowed->groups || $allowed->courses || $allowed->categories || $allowed->site);
2712  }
2713  
2714  /**
2715   * Check wether the current user is permitted to add events.
2716   *
2717   * @param stdClass $event object of event
2718   * @return bool has the capability to add event
2719   */
2720  function calendar_add_event_allowed($event) {
2721      global $USER, $DB;
2722  
2723      // Can not be using guest account.
2724      if (!isloggedin() or isguestuser()) {
2725          return false;
2726      }
2727  
2728      if (calendar_can_manage_non_user_event_in_system($event)) {
2729          return true;
2730      }
2731  
2732      switch ($event->eventtype) {
2733          case 'category':
2734              return has_capability('moodle/category:manage', $event->context);
2735          case 'course':
2736              return has_capability('moodle/calendar:manageentries', $event->context);
2737          case 'group':
2738              // Allow users to add/edit group events if -
2739              // 1) They have manageentries (= entries for whole course).
2740              // 2) They have managegroupentries AND are in the group.
2741              $group = $DB->get_record('groups', array('id' => $event->groupid));
2742              return $group && (
2743                      has_capability('moodle/calendar:manageentries', $event->context) ||
2744                      (has_capability('moodle/calendar:managegroupentries', $event->context)
2745                          && groups_is_member($event->groupid)));
2746          case 'user':
2747              return calendar_can_manage_user_event($event);
2748          case 'site':
2749              return has_capability('moodle/calendar:manageentries', $event->context);
2750          default:
2751              return has_capability('moodle/calendar:manageentries', $event->context);
2752      }
2753  }
2754  
2755  /**
2756   * Returns option list for the poll interval setting.
2757   *
2758   * @return array An array of poll interval options. Interval => description.
2759   */
2760  function calendar_get_pollinterval_choices() {
2761      return array(
2762          '0' => get_string('never', 'calendar'),
2763          HOURSECS => get_string('hourly', 'calendar'),
2764          DAYSECS => get_string('daily', 'calendar'),
2765          WEEKSECS => get_string('weekly', 'calendar'),
2766          '2628000' => get_string('monthly', 'calendar'),
2767          YEARSECS => get_string('annually', 'calendar')
2768      );
2769  }
2770  
2771  /**
2772   * Returns option list of available options for the calendar event type, given the current user and course.
2773   *
2774   * @param int $courseid The id of the course
2775   * @return array An array containing the event types the user can create.
2776   */
2777  function calendar_get_eventtype_choices($courseid) {
2778      $choices = array();
2779      $allowed = new \stdClass;
2780      calendar_get_allowed_types($allowed, $courseid);
2781  
2782      if ($allowed->user) {
2783          $choices['user'] = get_string('userevents', 'calendar');
2784      }
2785      if ($allowed->site) {
2786          $choices['site'] = get_string('siteevents', 'calendar');
2787      }
2788      if (!empty($allowed->courses)) {
2789          $choices['course'] = get_string('courseevents', 'calendar');
2790      }
2791      if (!empty($allowed->categories)) {
2792          $choices['category'] = get_string('categoryevents', 'calendar');
2793      }
2794      if (!empty($allowed->groups) and is_array($allowed->groups)) {
2795          $choices['group'] = get_string('group');
2796      }
2797  
2798      return array($choices, $allowed->groups);
2799  }
2800  
2801  /**
2802   * Add an iCalendar subscription to the database.
2803   *
2804   * @param stdClass $sub The subscription object (e.g. from the form)
2805   * @return int The insert ID, if any.
2806   */
2807  function calendar_add_subscription($sub) {
2808      global $DB, $USER, $SITE;
2809  
2810      // Undo the form definition work around to allow us to have two different
2811      // course selectors present depending on which event type the user selects.
2812      if (!empty($sub->groupcourseid)) {
2813          $sub->courseid = $sub->groupcourseid;
2814          unset($sub->groupcourseid);
2815      }
2816  
2817      // Default course id if none is set.
2818      if (empty($sub->courseid)) {
2819          if ($sub->eventtype === 'site') {
2820              $sub->courseid = SITEID;
2821          } else {
2822              $sub->courseid = 0;
2823          }
2824      }
2825  
2826      if ($sub->eventtype === 'site') {
2827          $sub->courseid = $SITE->id;
2828      } else if ($sub->eventtype === 'group' || $sub->eventtype === 'course') {
2829          $sub->courseid = $sub->courseid;
2830      } else if ($sub->eventtype === 'category') {
2831          $sub->categoryid = $sub->categoryid;
2832      } else {
2833          // User events.
2834          $sub->courseid = 0;
2835      }
2836      $sub->userid = $USER->id;
2837  
2838      // File subscriptions never update.
2839      if (empty($sub->url)) {
2840          $sub->pollinterval = 0;
2841      }
2842  
2843      if (!empty($sub->name)) {
2844          if (empty($sub->id)) {
2845              $id = $DB->insert_record('event_subscriptions', $sub);
2846              // We cannot cache the data here because $sub is not complete.
2847              $sub->id = $id;
2848              // Trigger event, calendar subscription added.
2849              $eventparams = array('objectid' => $sub->id,
2850                  'context' => calendar_get_calendar_context($sub),
2851                  'other' => array(
2852                      'eventtype' => $sub->eventtype,
2853                  )
2854              );
2855              switch ($sub->eventtype) {
2856                  case 'category':
2857                      $eventparams['other']['categoryid'] = $sub->categoryid;
2858                      break;
2859                  case 'course':
2860                      $eventparams['other']['courseid'] = $sub->courseid;
2861                      break;
2862                  case 'group':
2863                      $eventparams['other']['courseid'] = $sub->courseid;
2864                      $eventparams['other']['groupid'] = $sub->groupid;
2865                      break;
2866                  default:
2867                      $eventparams['other']['courseid'] = $sub->courseid;
2868              }
2869  
2870              $event = \core\event\calendar_subscription_created::create($eventparams);
2871              $event->trigger();
2872              return $id;
2873          } else {
2874              // Why are we doing an update here?
2875              calendar_update_subscription($sub);
2876              return $sub->id;
2877          }
2878      } else {
2879          throw new \moodle_exception('errorbadsubscription', 'importcalendar');
2880      }
2881  }
2882  
2883  /**
2884   * Add an iCalendar event to the Moodle calendar.
2885   *
2886   * @param stdClass $event The RFC-2445 iCalendar event
2887   * @param int $unused Deprecated
2888   * @param int $subscriptionid The iCalendar subscription ID
2889   * @param string $timezone The X-WR-TIMEZONE iCalendar property if provided
2890   * @throws dml_exception A DML specific exception is thrown for invalid subscriptionids.
2891   * @return int Code: CALENDAR_IMPORT_EVENT_UPDATED = updated,  CALENDAR_IMPORT_EVENT_INSERTED = inserted, 0 = error
2892   */
2893  function calendar_add_icalendar_event($event, $unused, $subscriptionid, $timezone='UTC') {
2894      global $DB;
2895  
2896      // Probably an unsupported X-MICROSOFT-CDO-BUSYSTATUS event.
2897      if (empty($event->properties['SUMMARY'])) {
2898          return 0;
2899      }
2900  
2901      $name = $event->properties['SUMMARY'][0]->value;
2902      $name = str_replace('\n', '<br />', $name);
2903      $name = str_replace('\\', '', $name);
2904      $name = preg_replace('/\s+/u', ' ', $name);
2905  
2906      $eventrecord = new \stdClass;
2907      $eventrecord->name = clean_param($name, PARAM_NOTAGS);
2908  
2909      if (empty($event->properties['DESCRIPTION'][0]->value)) {
2910          $description = '';
2911      } else {
2912          $description = $event->properties['DESCRIPTION'][0]->value;
2913          $description = clean_param($description, PARAM_NOTAGS);
2914          $description = str_replace('\n', '<br />', $description);
2915          $description = str_replace('\\', '', $description);
2916          $description = preg_replace('/\s+/u', ' ', $description);
2917      }
2918      $eventrecord->description = $description;
2919  
2920      // Probably a repeating event with RRULE etc. TODO: skip for now.
2921      if (empty($event->properties['DTSTART'][0]->value)) {
2922          return 0;
2923      }
2924  
2925      if (isset($event->properties['DTSTART'][0]->parameters['TZID'])) {
2926          $tz = $event->properties['DTSTART'][0]->parameters['TZID'];
2927      } else {
2928          $tz = $timezone;
2929      }
2930      $tz = \core_date::normalise_timezone($tz);
2931      $eventrecord->timestart = strtotime($event->properties['DTSTART'][0]->value . ' ' . $tz);
2932      if (empty($event->properties['DTEND'])) {
2933          $eventrecord->timeduration = 0; // No duration if no end time specified.
2934      } else {
2935          if (isset($event->properties['DTEND'][0]->parameters['TZID'])) {
2936              $endtz = $event->properties['DTEND'][0]->parameters['TZID'];
2937          } else {
2938              $endtz = $timezone;
2939          }
2940          $endtz = \core_date::normalise_timezone($endtz);
2941          $eventrecord->timeduration = strtotime($event->properties['DTEND'][0]->value . ' ' . $endtz) - $eventrecord->timestart;
2942      }
2943  
2944      // Check to see if it should be treated as an all day event.
2945      if ($eventrecord->timeduration == DAYSECS) {
2946          // Check to see if the event started at Midnight on the imported calendar.
2947          date_default_timezone_set($timezone);
2948          if (date('H:i:s', $eventrecord->timestart) === "00:00:00") {
2949              // This event should be an all day event. This is not correct, we don't do anything differently for all day events.
2950              // See MDL-56227.
2951              $eventrecord->timeduration = 0;
2952          }
2953          \core_date::set_default_server_timezone();
2954      }
2955  
2956      $eventrecord->location = empty($event->properties['LOCATION'][0]->value) ? '' :
2957              trim(str_replace('\\', '', $event->properties['LOCATION'][0]->value));
2958      $eventrecord->uuid = $event->properties['UID'][0]->value;
2959      $eventrecord->timemodified = time();
2960  
2961      // Add the iCal subscription details if required.
2962      // We should never do anything with an event without a subscription reference.
2963      $sub = calendar_get_subscription($subscriptionid);
2964      $eventrecord->subscriptionid = $subscriptionid;
2965      $eventrecord->userid = $sub->userid;
2966      $eventrecord->groupid = $sub->groupid;
2967      $eventrecord->courseid = $sub->courseid;
2968      $eventrecord->categoryid = $sub->categoryid;
2969      $eventrecord->eventtype = $sub->eventtype;
2970  
2971      if ($updaterecord = $DB->get_record('event', array('uuid' => $eventrecord->uuid,
2972          'subscriptionid' => $eventrecord->subscriptionid))) {
2973  
2974          // Compare iCal event data against the moodle event to see if something has changed.
2975          $result = array_diff((array) $eventrecord, (array) $updaterecord);
2976  
2977          // Unset timemodified field because it's always going to be different.
2978          unset($result['timemodified']);
2979  
2980          if (count($result)) {
2981              $eventrecord->id = $updaterecord->id;
2982              $return = CALENDAR_IMPORT_EVENT_UPDATED; // Update.
2983          } else {
2984              return CALENDAR_IMPORT_EVENT_SKIPPED;
2985          }
2986      } else {
2987          $return = CALENDAR_IMPORT_EVENT_INSERTED; // Insert.
2988      }
2989  
2990      if ($createdevent = \calendar_event::create($eventrecord, false)) {
2991          if (!empty($event->properties['RRULE'])) {
2992              // Repeating events.
2993              date_default_timezone_set($tz); // Change time zone to parse all events.
2994              $rrule = new \core_calendar\rrule_manager($event->properties['RRULE'][0]->value);
2995              $rrule->parse_rrule();
2996              $rrule->create_events($createdevent);
2997              \core_date::set_default_server_timezone(); // Change time zone back to what it was.
2998          }
2999          return $return;
3000      } else {
3001          return 0;
3002      }
3003  }
3004  
3005  /**
3006   * Delete subscription and all related events.
3007   *
3008   * @param int|stdClass $subscription subscription or it's id, which needs to be deleted.
3009   */
3010  function calendar_delete_subscription($subscription) {
3011      global $DB;
3012  
3013      if (!is_object($subscription)) {
3014          $subscription = $DB->get_record('event_subscriptions', array('id' => $subscription), '*', MUST_EXIST);
3015      }
3016  
3017      // Delete subscription and related events.
3018      $DB->delete_records('event', array('subscriptionid' => $subscription->id));
3019      $DB->delete_records('event_subscriptions', array('id' => $subscription->id));
3020      \cache_helper::invalidate_by_definition('core', 'calendar_subscriptions', array(), array($subscription->id));
3021  
3022      // Trigger event, calendar subscription deleted.
3023      $eventparams = array('objectid' => $subscription->id,
3024          'context' => calendar_get_calendar_context($subscription),
3025          'other' => array(
3026              'eventtype' => $subscription->eventtype,
3027          )
3028      );
3029      switch ($subscription->eventtype) {
3030          case 'category':
3031              $eventparams['other']['categoryid'] = $subscription->categoryid;
3032              break;
3033          case 'course':
3034              $eventparams['other']['courseid'] = $subscription->courseid;
3035              break;
3036          case 'group':
3037              $eventparams['other']['courseid'] = $subscription->courseid;
3038              $eventparams['other']['groupid'] = $subscription->groupid;
3039              break;
3040          default:
3041              $eventparams['other']['courseid'] = $subscription->courseid;
3042      }
3043      $event = \core\event\calendar_subscription_deleted::create($eventparams);
3044      $event->trigger();
3045  }
3046  
3047  /**
3048   * From a URL, fetch the calendar and return an iCalendar object.
3049   *
3050   * @param string $url The iCalendar URL
3051   * @return iCalendar The iCalendar object
3052   */
3053  function calendar_get_icalendar($url) {
3054      global $CFG;
3055  
3056      require_once($CFG->libdir . '/filelib.php');
3057      require_once($CFG->libdir . '/bennu/bennu.inc.php');
3058  
3059      $curl = new \curl();
3060      $curl->setopt(array('CURLOPT_FOLLOWLOCATION' => 1, 'CURLOPT_MAXREDIRS' => 5));
3061      $calendar = $curl->get($url);
3062  
3063      // Http code validation should actually be the job of curl class.
3064      if (!$calendar || $curl->info['http_code'] != 200 || !empty($curl->errorno)) {
3065          throw new \moodle_exception('errorinvalidicalurl', 'calendar');
3066      }
3067  
3068      $ical = new \iCalendar();
3069      $ical->unserialize($calendar);
3070  
3071      return $ical;
3072  }
3073  
3074  /**
3075   * Import events from an iCalendar object into a course calendar.
3076   *
3077   * @param iCalendar $ical The iCalendar object.
3078   * @param int|null $subscriptionid The subscription ID.
3079   * @return array A log of the import progress, including errors.
3080   */
3081  function calendar_import_events_from_ical(iCalendar $ical, int $subscriptionid = null): array {
3082      global $DB;
3083  
3084      $errors = [];
3085      $eventcount = 0;
3086      $updatecount = 0;
3087      $skippedcount = 0;
3088      $deletedcount = 0;
3089  
3090      // Large calendars take a while...
3091      if (!CLI_SCRIPT) {
3092          \core_php_time_limit::raise(300);
3093      }
3094  
3095      // Start with a safe default timezone.
3096      $timezone = 'UTC';
3097  
3098      // Grab the timezone from the iCalendar file to be used later.
3099      if (isset($ical->properties['X-WR-TIMEZONE'][0]->value)) {
3100          $timezone = $ical->properties['X-WR-TIMEZONE'][0]->value;
3101  
3102      } else if (isset($ical->properties['PRODID'][0]->value)) {
3103          // If the timezone was not found, check to se if this is MS exchange / Office 365 which uses Windows timezones.
3104          if (strncmp($ical->properties['PRODID'][0]->value, 'Microsoft', 9) == 0) {
3105              if (isset($ical->components['VTIMEZONE'][0]->properties['TZID'][0]->value)) {
3106                  $tzname = $ical->components['VTIMEZONE'][0]->properties['TZID'][0]->value;
3107                  $timezone = IntlTimeZone::getIDForWindowsID($tzname);
3108              }
3109          }
3110      }
3111  
3112      $icaluuids = [];
3113      foreach ($ical->components['VEVENT'] as $event) {
3114          $icaluuids[] = $event->properties['UID'][0]->value;
3115          $res = calendar_add_icalendar_event($event, null, $subscriptionid, $timezone);
3116          switch ($res) {
3117              case CALENDAR_IMPORT_EVENT_UPDATED:
3118                  $updatecount++;
3119                  break;
3120              case CALENDAR_IMPORT_EVENT_INSERTED:
3121                  $eventcount++;
3122                  break;
3123              case CALENDAR_IMPORT_EVENT_SKIPPED:
3124                  $skippedcount++;
3125                  break;
3126              case 0:
3127                  if (empty($event->properties['SUMMARY'])) {
3128                      $errors[] = '(' . get_string('notitle', 'calendar') . ')';
3129                  } else {
3130                      $errors[] = $event->properties['SUMMARY'][0]->value;
3131                  }
3132                  break;
3133          }
3134      }
3135  
3136      $existing = $DB->get_field('event_subscriptions', 'lastupdated', ['id' => $subscriptionid]);
3137      if (!empty($existing)) {
3138          $eventsuuids = $DB->get_records_menu('event', ['subscriptionid' => $subscriptionid], '', 'id, uuid');
3139  
3140          $icaleventscount = count($icaluuids);
3141          $tobedeleted = [];
3142          if (count($eventsuuids) > $icaleventscount) {
3143              foreach ($eventsuuids as $eventid => $eventuuid) {
3144                  if (!in_array($eventuuid, $icaluuids)) {
3145                      $tobedeleted[] = $eventid;
3146                  }
3147              }
3148              if (!empty($tobedeleted)) {
3149                  $DB->delete_records_list('event', 'id', $tobedeleted);
3150                  $deletedcount = count($tobedeleted);
3151              }
3152          }
3153      }
3154  
3155      $result = [
3156          'eventsimported' => $eventcount,
3157          'eventsskipped' => $skippedcount,
3158          'eventsupdated' => $updatecount,
3159          'eventsdeleted' => $deletedcount,
3160          'haserror' => !empty($errors),
3161          'errors' => $errors,
3162      ];
3163  
3164      return $result;
3165  }
3166  
3167  /**
3168   * Fetch a calendar subscription and update the events in the calendar.
3169   *
3170   * @param int $subscriptionid The course ID for the calendar.
3171   * @return string A log of the import progress, including errors.
3172   */
3173  function calendar_update_subscription_events($subscriptionid) {
3174      $sub = calendar_get_subscription($subscriptionid);
3175  
3176      // Don't update a file subscription.
3177      if (empty($sub->url)) {
3178          return 'File subscription not updated.';
3179      }
3180  
3181      $ical = calendar_get_icalendar($sub->url);
3182      $return = calendar_import_events_from_ical($ical, $subscriptionid);
3183      $sub->lastupdated = time();
3184  
3185      calendar_update_subscription($sub);
3186  
3187      return $return;
3188  }
3189  
3190  /**
3191   * Update a calendar subscription. Also updates the associated cache.
3192   *
3193   * @param stdClass|array $subscription Subscription record.
3194   * @throws coding_exception If something goes wrong
3195   * @since Moodle 2.5
3196   */
3197  function calendar_update_subscription($subscription) {
3198      global $DB;
3199  
3200      if (is_array($subscription)) {
3201          $subscription = (object)$subscription;
3202      }
3203      if (empty($subscription->id) || !$DB->record_exists('event_subscriptions', array('id' => $subscription->id))) {
3204          throw new \coding_exception('Cannot update a subscription without a valid id');
3205      }
3206  
3207      $DB->update_record('event_subscriptions', $subscription);
3208  
3209      // Update cache.
3210      $cache = \cache::make('core', 'calendar_subscriptions');
3211      $cache->set($subscription->id, $subscription);
3212  
3213      // Trigger event, calendar subscription updated.
3214      $eventparams = array('userid' => $subscription->userid,
3215          'objectid' => $subscription->id,
3216          'context' => calendar_get_calendar_context($subscription),
3217          'other' => array(
3218              'eventtype' => $subscription->eventtype,
3219          )
3220      );
3221      switch ($subscription->eventtype) {
3222          case 'category':
3223              $eventparams['other']['categoryid'] = $subscription->categoryid;
3224              break;
3225          case 'course':
3226              $eventparams['other']['courseid'] = $subscription->courseid;
3227              break;
3228          case 'group':
3229              $eventparams['other']['courseid'] = $subscription->courseid;
3230              $eventparams['other']['groupid'] = $subscription->groupid;
3231              break;
3232          default:
3233              $eventparams['other']['courseid'] = $subscription->courseid;
3234      }
3235      $event = \core\event\calendar_subscription_updated::create($eventparams);
3236      $event->trigger();
3237  }
3238  
3239  /**
3240   * Checks to see if the user can edit a given subscription feed.
3241   *
3242   * @param mixed $subscriptionorid Subscription object or id
3243   * @return bool true if current user can edit the subscription else false
3244   */
3245  function calendar_can_edit_subscription($subscriptionorid) {
3246      global $USER;
3247      if (is_array($subscriptionorid)) {
3248          $subscription = (object)$subscriptionorid;
3249      } else if (is_object($subscriptionorid)) {
3250          $subscription = $subscriptionorid;
3251      } else {
3252          $subscription = calendar_get_subscription($subscriptionorid);
3253      }
3254  
3255      $allowed = new \stdClass;
3256      $courseid = $subscription->courseid;
3257      $categoryid = $subscription->categoryid;
3258      $groupid = $subscription->groupid;
3259      $category = null;
3260  
3261      if (!empty($categoryid)) {
3262          $category = \core_course_category::get($categoryid);
3263      }
3264      calendar_get_allowed_types($allowed, $courseid, null, $category);
3265      switch ($subscription->eventtype) {
3266          case 'user':
3267              return ($USER->id == $subscription->userid && $allowed->user);
3268          case 'course':
3269              if (isset($allowed->courses[$courseid])) {
3270                  return $allowed->courses[$courseid];
3271              } else {
3272                  return false;
3273              }
3274          case 'category':
3275              if (isset($allowed->categories[$categoryid])) {
3276                  return $allowed->categories[$categoryid];
3277              } else {
3278                  return false;
3279              }
3280          case 'site':
3281              return $allowed->site;
3282          case 'group':
3283              if (isset($allowed->groups[$groupid])) {
3284                  return $allowed->groups[$groupid];
3285              } else {
3286                  return false;
3287              }
3288          default:
3289              return false;
3290      }
3291  }
3292  
3293  /**
3294   * Helper function to determine the context of a calendar subscription.
3295   * Subscriptions can be created in two contexts COURSE, or USER.
3296   *
3297   * @param stdClass $subscription
3298   * @return context instance
3299   */
3300  function calendar_get_calendar_context($subscription) {
3301      // Determine context based on calendar type.
3302      if ($subscription->eventtype === 'site') {
3303          $context = \context_course::instance(SITEID);
3304      } else if ($subscription->eventtype === 'group' || $subscription->eventtype === 'course') {
3305          $context = \context_course::instance($subscription->courseid);
3306      } else {
3307          $context = \context_user::instance($subscription->userid);
3308      }
3309      return $context;
3310  }
3311  
3312  /**
3313   * Implements callback user_preferences, lists preferences that users are allowed to update directly
3314   *
3315   * Used in {@see core_user::fill_preferences_cache()}, see also {@see useredit_update_user_preference()}
3316   *
3317   * @return array
3318   */
3319  function core_calendar_user_preferences() {
3320      $preferences = [];
3321      $preferences['calendar_timeformat'] = array('type' => PARAM_NOTAGS, 'null' => NULL_NOT_ALLOWED, 'default' => '0',
3322          'choices' => array('0', CALENDAR_TF_12, CALENDAR_TF_24)
3323      );
3324      $preferences['calendar_startwday'] = array('type' => PARAM_INT, 'null' => NULL_NOT_ALLOWED, 'default' => 0,
3325          'choices' => array(0, 1, 2, 3, 4, 5, 6));
3326      $preferences['calendar_maxevents'] = array('type' => PARAM_INT, 'choices' => range(1, 20));
3327      $preferences['calendar_lookahead'] = array('type' => PARAM_INT, 'null' => NULL_NOT_ALLOWED, 'default' => 365,
3328          'choices' => array(365, 270, 180, 150, 120, 90, 60, 30, 21, 14, 7, 6, 5, 4, 3, 2, 1));
3329      $preferences['calendar_persistflt'] = array('type' => PARAM_INT, 'null' => NULL_NOT_ALLOWED, 'default' => 0,
3330          'choices' => array(0, 1));
3331      return $preferences;
3332  }
3333  
3334  /**
3335   * Get legacy calendar events
3336   *
3337   * @param int $tstart Start time of time range for events
3338   * @param int $tend End time of time range for events
3339   * @param array|int|boolean $users array of users, user id or boolean for all/no user events
3340   * @param array|int|boolean $groups array of groups, group id or boolean for all/no group events
3341   * @param array|int|boolean $courses array of courses, course id or boolean for all/no course events
3342   * @param boolean $withduration whether only events starting within time range selected
3343   *                              or events in progress/already started selected as well
3344   * @param boolean $ignorehidden whether to select only visible events or all events
3345   * @param array $categories array of category ids and/or objects.
3346   * @param int $limitnum Number of events to fetch or zero to fetch all.
3347   *
3348   * @return array $events of selected events or an empty array if there aren't any (or there was an error)
3349   */
3350  function calendar_get_legacy_events($tstart, $tend, $users, $groups, $courses,
3351          $withduration = true, $ignorehidden = true, $categories = [], $limitnum = 0) {
3352      // Normalise the users, groups and courses parameters so that they are compliant with \core_calendar\local\api::get_events().
3353      // Existing functions that were using the old calendar_get_events() were passing a mixture of array, int, boolean for these
3354      // parameters, but with the new API method, only null and arrays are accepted.
3355      list($userparam, $groupparam, $courseparam, $categoryparam) = array_map(function($param) {
3356          // If parameter is true, return null.
3357          if ($param === true) {
3358              return null;
3359          }
3360  
3361          // If parameter is false, return an empty array.
3362          if ($param === false) {
3363              return [];
3364          }
3365  
3366          // If the parameter is a scalar value, enclose it in an array.
3367          if (!is_array($param)) {
3368              return [$param];
3369          }
3370  
3371          // No normalisation required.
3372          return $param;
3373      }, [$users, $groups, $courses, $categories]);
3374  
3375      // If a single user is provided, we can use that for capability checks.
3376      // Otherwise current logged in user is used - See MDL-58768.
3377      if (is_array($userparam) && count($userparam) == 1) {
3378          \core_calendar\local\event\container::set_requesting_user($userparam[0]);
3379      }
3380      $mapper = \core_calendar\local\event\container::get_event_mapper();
3381      $events = \core_calendar\local\api::get_events(
3382          $tstart,
3383          $tend,
3384          null,
3385          null,
3386          null,
3387          null,
3388          $limitnum,
3389          null,
3390          $userparam,
3391          $groupparam,
3392          $courseparam,
3393          $categoryparam,
3394          $withduration,
3395          $ignorehidden
3396      );
3397  
3398      return array_reduce($events, function($carry, $event) use ($mapper) {
3399          return $carry + [$event->get_id() => $mapper->from_event_to_stdclass($event)];
3400      }, []);
3401  }
3402  
3403  
3404  /**
3405   * Get the calendar view output.
3406   *
3407   * @param   \calendar_information $calendar The calendar being represented
3408   * @param   string  $view The type of calendar to have displayed
3409   * @param   bool    $includenavigation Whether to include navigation
3410   * @param   bool    $skipevents Whether to load the events or not
3411   * @param   int     $lookahead Overwrites site and users's lookahead setting.
3412   * @return  array[array, string]
3413   */
3414  function calendar_get_view(\calendar_information $calendar, $view, $includenavigation = true, bool $skipevents = false,
3415          ?int $lookahead = null) {
3416      global $PAGE, $CFG;
3417  
3418      $renderer = $PAGE->get_renderer('core_calendar');
3419      $type = \core_calendar\type_factory::get_calendar_instance();
3420  
3421      // Calculate the bounds of the month.
3422      $calendardate = $type->timestamp_to_date_array($calendar->time);
3423  
3424      $date = new \DateTime('now', core_date::get_user_timezone_object(99));
3425      $eventlimit = 0;
3426  
3427      if ($view === 'day') {
3428          $tstart = $type->convert_to_timestamp($calendardate['year'], $calendardate['mon'], $calendardate['mday']);
3429          $date->setTimestamp($tstart);
3430          $date->modify('+1 day');
3431      } else if ($view === 'upcoming' || $view === 'upcoming_mini') {
3432          // Number of days in the future that will be used to fetch events.
3433          if (!$lookahead) {
3434              if (isset($CFG->calendar_lookahead)) {
3435                  $defaultlookahead = intval($CFG->calendar_lookahead);
3436              } else {
3437                  $defaultlookahead = CALENDAR_DEFAULT_UPCOMING_LOOKAHEAD;
3438              }
3439              $lookahead = get_user_preferences('calendar_lookahead', $defaultlookahead);
3440          }
3441  
3442          // Maximum number of events to be displayed on upcoming view.
3443          $defaultmaxevents = CALENDAR_DEFAULT_UPCOMING_MAXEVENTS;
3444          if (isset($CFG->calendar_maxevents)) {
3445              $defaultmaxevents = intval($CFG->calendar_maxevents);
3446          }
3447          $eventlimit = get_user_preferences('calendar_maxevents', $defaultmaxevents);
3448  
3449          $tstart = $type->convert_to_timestamp($calendardate['year'], $calendardate['mon'], $calendardate['mday'],
3450                  $calendardate['hours']);
3451          $date->setTimestamp($tstart);
3452          $date->modify('+' . $lookahead . ' days');
3453      } else {
3454          $tstart = $type->convert_to_timestamp($calendardate['year'], $calendardate['mon'], 1);
3455          $monthdays = $type->get_num_days_in_month($calendardate['year'], $calendardate['mon']);
3456          $date->setTimestamp($tstart);
3457          $date->modify('+' . $monthdays . ' days');
3458  
3459          if ($view === 'mini' || $view === 'minithree') {
3460              $template = 'core_calendar/calendar_mini';
3461          } else {
3462              $template = 'core_calendar/calendar_month';
3463          }
3464      }
3465  
3466      // We need to extract 1 second to ensure that we don't get into the next day.
3467      $date->modify('-1 second');
3468      $tend = $date->getTimestamp();
3469  
3470      list($userparam, $groupparam, $courseparam, $categoryparam) = array_map(function($param) {
3471          // If parameter is true, return null.
3472          if ($param === true) {
3473              return null;
3474          }
3475  
3476          // If parameter is false, return an empty array.
3477          if ($param === false) {
3478              return [];
3479          }
3480  
3481          // If the parameter is a scalar value, enclose it in an array.
3482          if (!is_array($param)) {
3483              return [$param];
3484          }
3485  
3486          // No normalisation required.
3487          return $param;
3488      }, [$calendar->users, $calendar->groups, $calendar->courses, $calendar->categories]);
3489  
3490      if ($skipevents) {
3491          $events = [];
3492      } else {
3493          $events = \core_calendar\local\api::get_events(
3494              $tstart,
3495              $tend,
3496              null,
3497              null,
3498              null,
3499              null,
3500              $eventlimit,
3501              null,
3502              $userparam,
3503              $groupparam,
3504              $courseparam,
3505              $categoryparam,
3506              true,
3507              true,
3508              function ($event) {
3509                  if ($proxy = $event->get_course_module()) {
3510                      $cminfo = $proxy->get_proxied_instance();
3511                      return $cminfo->uservisible;
3512                  }
3513  
3514                  if ($proxy = $event->get_category()) {
3515                      $category = $proxy->get_proxied_instance();
3516  
3517                      return $category->is_uservisible();
3518                  }
3519  
3520                  return true;
3521              }
3522          );
3523      }
3524  
3525      $related = [
3526          'events' => $events,
3527          'cache' => new \core_calendar\external\events_related_objects_cache($events),
3528          'type' => $type,
3529      ];
3530  
3531      $data = [];
3532      $calendar->set_viewmode($view);
3533      if ($view == "month" || $view == "monthblock" || $view == "mini" || $view == "minithree" ) {
3534          $month = new \core_calendar\external\month_exporter($calendar, $type, $related);
3535          $month->set_includenavigation($includenavigation);
3536          $month->set_initialeventsloaded(!$skipevents);
3537          $month->set_showcoursefilter(($view == "month" || $view == "monthblock"));
3538          $data = $month->export($renderer);
3539      } else if ($view == "day") {
3540          $day = new \core_calendar\external\calendar_day_exporter($calendar, $related);
3541          $data = $day->export($renderer);
3542          $data->viewingday = true;
3543          $data->showviewselector = true;
3544          $template = 'core_calendar/calendar_day';
3545      } else if ($view == "upcoming" || $view == "upcoming_mini") {
3546          $upcoming = new \core_calendar\external\calendar_upcoming_exporter($calendar, $related);
3547          $data = $upcoming->export($renderer);
3548  
3549          if ($view == "upcoming") {
3550              $template = 'core_calendar/calendar_upcoming';
3551              $data->viewingupcoming = true;
3552              $data->showviewselector = true;
3553          } else if ($view == "upcoming_mini") {
3554              $template = 'core_calendar/calendar_upcoming_mini';
3555          }
3556      }
3557  
3558      return [$data, $template];
3559  }
3560  
3561  /**
3562   * Request and render event form fragment.
3563   *
3564   * @param array $args The fragment arguments.
3565   * @return string The rendered mform fragment.
3566   */
3567  function calendar_output_fragment_event_form($args) {
3568      global $CFG, $OUTPUT, $USER;
3569      require_once($CFG->libdir . '/grouplib.php');
3570      $html = '';
3571      $data = [];
3572      $eventid = isset($args['eventid']) ? clean_param($args['eventid'], PARAM_INT) : null;
3573      $starttime = isset($args['starttime']) ? clean_param($args['starttime'], PARAM_INT) : null;
3574      $courseid = (isset($args['courseid']) && $args['courseid'] != SITEID) ? clean_param($args['courseid'], PARAM_INT) : null;
3575      $categoryid = isset($args['categoryid']) ? clean_param($args['categoryid'], PARAM_INT) : null;
3576      $event = null;
3577      $hasformdata = isset($args['formdata']) && !empty($args['formdata']);
3578      $context = \context_user::instance($USER->id);
3579      $editoroptions = \core_calendar\local\event\forms\create::build_editor_options($context);
3580      $formoptions = ['editoroptions' => $editoroptions, 'courseid' => $courseid];
3581      $draftitemid = 0;
3582  
3583      if ($hasformdata) {
3584          parse_str(clean_param($args['formdata'], PARAM_TEXT), $data);
3585          if (isset($data['description']['itemid'])) {
3586              $draftitemid = $data['description']['itemid'];
3587          }
3588      }
3589  
3590      if ($starttime) {
3591          $formoptions['starttime'] = $starttime;
3592      }
3593      // Let's check first which event types user can add.
3594      $eventtypes = calendar_get_allowed_event_types($courseid);
3595      $formoptions['eventtypes'] = $eventtypes;
3596  
3597      if (is_null($eventid)) {
3598          if (!empty($courseid)) {
3599              $groupcoursedata = groups_get_course_data($courseid);
3600              $formoptions['groups'] = [];
3601              foreach ($groupcoursedata->groups as $groupid => $groupdata) {
3602                  $formoptions['groups'][$groupid] = $groupdata->name;
3603              }
3604          }
3605  
3606          $mform = new \core_calendar\local\event\forms\create(
3607              null,
3608              $formoptions,
3609              'post',
3610              '',
3611              null,
3612              true,
3613              $data
3614          );
3615  
3616          // If the user is on course context and is allowed to add course events set the event type default to course.
3617          if (!empty($courseid) && !empty($eventtypes['course'])) {
3618              $data['eventtype'] = 'course';
3619              $data['courseid'] = $courseid;
3620              $data['groupcourseid'] = $courseid;
3621          } else if (!empty($categoryid) && !empty($eventtypes['category'])) {
3622              $data['eventtype'] = 'category';
3623              $data['categoryid'] = $categoryid;
3624          } else if (!empty($groupcoursedata) && !empty($eventtypes['group'])) {
3625              $data['groupcourseid'] = $courseid;
3626              $data['groups'] = $groupcoursedata->groups;
3627          }
3628          $mform->set_data($data);
3629      } else {
3630          $event = calendar_event::load($eventid);
3631  
3632          if (!calendar_edit_event_allowed($event)) {
3633              throw new \moodle_exception('nopermissiontoupdatecalendar');
3634          }
3635  
3636          $mapper = new \core_calendar\local\event\mappers\create_update_form_mapper();
3637          $eventdata = $mapper->from_legacy_event_to_data($event);
3638          $data = array_merge((array) $eventdata, $data);
3639          $event->count_repeats();
3640          $formoptions['event'] = $event;
3641  
3642          if (!empty($event->courseid)) {
3643              $groupcoursedata = groups_get_course_data($event->courseid);
3644              $formoptions['groups'] = [];
3645              foreach ($groupcoursedata->groups as $groupid => $groupdata) {
3646                  $formoptions['groups'][$groupid] = $groupdata->name;
3647              }
3648          }
3649  
3650          $data['description']['text'] = file_prepare_draft_area(
3651              $draftitemid,
3652              $event->context->id,
3653              'calendar',
3654              'event_description',
3655              $event->id,
3656              null,
3657              $data['description']['text']
3658          );
3659          $data['description']['itemid'] = $draftitemid;
3660  
3661          $mform = new \core_calendar\local\event\forms\update(
3662              null,
3663              $formoptions,
3664              'post',
3665              '',
3666              null,
3667              true,
3668              $data
3669          );
3670          $mform->set_data($data);
3671  
3672          // Check to see if this event is part of a subscription or import.
3673          // If so display a warning on edit.
3674          if (isset($event->subscriptionid) && ($event->subscriptionid != null)) {
3675              $renderable = new \core\output\notification(
3676                  get_string('eventsubscriptioneditwarning', 'calendar'),
3677                  \core\output\notification::NOTIFY_INFO
3678              );
3679  
3680              $html .= $OUTPUT->render($renderable);
3681          }
3682      }
3683  
3684      if ($hasformdata) {
3685          $mform->is_validated();
3686      }
3687  
3688      $html .= $mform->render();
3689      return $html;
3690  }
3691  
3692  /**
3693   * Calculate the timestamp from the supplied Gregorian Year, Month, and Day.
3694   *
3695   * @param   int     $d The day
3696   * @param   int     $m The month
3697   * @param   int     $y The year
3698   * @param   int     $time The timestamp to use instead of a separate y/m/d.
3699   * @return  int     The timestamp
3700   */
3701  function calendar_get_timestamp($d, $m, $y, $time = 0) {
3702      // If a day, month and year were passed then convert it to a timestamp. If these were passed
3703      // then we can assume the day, month and year are passed as Gregorian, as no where in core
3704      // should we be passing these values rather than the time.
3705      if (!empty($d) && !empty($m) && !empty($y)) {
3706          if (checkdate($m, $d, $y)) {
3707              $time = make_timestamp($y, $m, $d);
3708          } else {
3709              $time = time();
3710          }
3711      } else if (empty($time)) {
3712          $time = time();
3713      }
3714  
3715      return $time;
3716  }
3717  
3718  /**
3719   * Get the calendar footer options.
3720   *
3721   * @param calendar_information $calendar The calendar information object.
3722   * @param array $options Display options for the footer. If an option is not set, a default value will be provided.
3723   *                      It consists of:
3724   *                      - showfullcalendarlink - bool - Whether to show the full calendar link or not. Defaults to false.
3725   *
3726   * @return array The data for template and template name.
3727   */
3728  function calendar_get_footer_options($calendar, array $options = []) {
3729      global $CFG, $USER, $PAGE;
3730  
3731      // Generate hash for iCal link.
3732      $authtoken = calendar_get_export_token($USER);
3733  
3734      $renderer = $PAGE->get_renderer('core_calendar');
3735      $footer = new \core_calendar\external\footer_options_exporter($calendar, $USER->id, $authtoken, $options);
3736      $data = $footer->export($renderer);
3737      $template = 'core_calendar/footer_options';
3738  
3739      return [$data, $template];
3740  }
3741  
3742  /**
3743   * Get the list of potential calendar filter types as a type => name
3744   * combination.
3745   *
3746   * @return array
3747   */
3748  function calendar_get_filter_types() {
3749      $types = [
3750          'site',
3751          'category',
3752          'course',
3753          'group',
3754          'user',
3755          'other'
3756      ];
3757  
3758      return array_map(function($type) {
3759          return [
3760              'eventtype' => $type,
3761              'name' => get_string("eventtype{$type}", "calendar"),
3762              'icon' => true,
3763              'key' => 'i/' . $type . 'event',
3764              'component' => 'core'
3765          ];
3766      }, $types);
3767  }
3768  
3769  /**
3770   * Check whether the specified event type is valid.
3771   *
3772   * @param string $type
3773   * @return bool
3774   */
3775  function calendar_is_valid_eventtype($type) {
3776      $validtypes = [
3777          'user',
3778          'group',
3779          'course',
3780          'category',
3781          'site',
3782      ];
3783      return in_array($type, $validtypes);
3784  }
3785  
3786  /**
3787   * Get event types the user can create event based on categories, courses and groups
3788   * the logged in user belongs to.
3789   *
3790   * @param int|null $courseid The course id.
3791   * @return array The array of allowed types.
3792   */
3793  function calendar_get_allowed_event_types(int $courseid = null) {
3794      global $DB, $CFG, $USER;
3795  
3796      $types = [
3797          'user' => false,
3798          'site' => false,
3799          'course' => false,
3800          'group' => false,
3801          'category' => false
3802      ];
3803  
3804      if (!empty($courseid) && $courseid != SITEID) {
3805          $context = \context_course::instance($courseid);
3806          $types['user'] = has_capability('moodle/calendar:manageownentries', $context);
3807          calendar_internal_update_course_and_group_permission($courseid, $context, $types);
3808      }
3809  
3810      if (has_capability('moodle/calendar:manageentries', \context_course::instance(SITEID))) {
3811          $types['site'] = true;
3812      }
3813  
3814      if (has_capability('moodle/calendar:manageownentries', \context_system::instance())) {
3815          $types['user'] = true;
3816      }
3817      if (core_course_category::has_manage_capability_on_any()) {
3818          $types['category'] = true;
3819      }
3820  
3821      // We still don't know if the user can create group and course events, so iterate over the courses to find out
3822      // if the user has capabilities in one of the courses.
3823      if ($types['course'] == false || $types['group'] == false) {
3824          if ($CFG->calendar_adminseesall && has_capability('moodle/calendar:manageentries', context_system::instance())) {
3825              $sql = "SELECT c.id, " . context_helper::get_preload_record_columns_sql('ctx') . "
3826                        FROM {course} c
3827                        JOIN {context} ctx ON ctx.contextlevel = ? AND ctx.instanceid = c.id
3828                       WHERE c.id IN (
3829                              SELECT DISTINCT courseid FROM {groups}
3830                          )";
3831              $courseswithgroups = $DB->get_recordset_sql($sql, [CONTEXT_COURSE]);
3832              foreach ($courseswithgroups as $course) {
3833                  context_helper::preload_from_record($course);
3834                  $context = context_course::instance($course->id);
3835  
3836                  if (has_capability('moodle/calendar:manageentries', $context)) {
3837                      if (has_any_capability(['moodle/site:accessallgroups', 'moodle/calendar:managegroupentries'], $context)) {
3838                          // The user can manage group entries or access any group.
3839                          $types['group'] = true;
3840                          $types['course'] = true;
3841                          break;
3842                      }
3843                  }
3844              }
3845              $courseswithgroups->close();
3846  
3847              if (false === $types['course']) {
3848                  // Course is still not confirmed. There may have been no courses with a group in them.
3849                  $ctxfields = context_helper::get_preload_record_columns_sql('ctx');
3850                  $sql = "SELECT
3851                              c.id, c.visible, {$ctxfields}
3852                          FROM {course} c
3853                          JOIN {context} ctx ON (ctx.instanceid = c.id AND ctx.contextlevel = :contextlevel)";
3854                  $params = [
3855                      'contextlevel' => CONTEXT_COURSE,
3856                  ];
3857                  $courses = $DB->get_recordset_sql($sql, $params);
3858                  foreach ($courses as $course) {
3859                      context_helper::preload_from_record($course);
3860                      $context = context_course::instance($course->id);
3861                      if (has_capability('moodle/calendar:manageentries', $context)) {
3862                          $types['course'] = true;
3863                          break;
3864                      }
3865                  }
3866                  $courses->close();
3867              }
3868  
3869          } else {
3870              $courses = calendar_get_default_courses(null, 'id');
3871              if (empty($courses)) {
3872                  return $types;
3873              }
3874  
3875              $courseids = array_map(function($c) {
3876                  return $c->id;
3877              }, $courses);
3878  
3879              // Check whether the user has access to create events within courses which have groups.
3880              list($insql, $params) = $DB->get_in_or_equal($courseids, SQL_PARAMS_NAMED);
3881              $sql = "SELECT c.id, " . context_helper::get_preload_record_columns_sql('ctx') . "
3882                        FROM {course} c
3883                        JOIN {context} ctx ON ctx.contextlevel = :contextlevel AND ctx.instanceid = c.id
3884                       WHERE c.id $insql
3885                         AND c.id IN (SELECT DISTINCT courseid FROM {groups})";
3886              $params['contextlevel'] = CONTEXT_COURSE;
3887              $courseswithgroups = $DB->get_recordset_sql($sql, $params);
3888              foreach ($courseswithgroups as $coursewithgroup) {
3889                  context_helper::preload_from_record($coursewithgroup);
3890                  $context = context_course::instance($coursewithgroup->id);
3891  
3892                  calendar_internal_update_course_and_group_permission($coursewithgroup->id, $context, $types);
3893  
3894                  // Okay, course and group event types are allowed, no need to keep the loop iteration.
3895                  if ($types['course'] == true && $types['group'] == true) {
3896                      break;
3897                  }
3898              }
3899              $courseswithgroups->close();
3900  
3901              if (false === $types['course']) {
3902                  list($insql, $params) = $DB->get_in_or_equal($courseids, SQL_PARAMS_NAMED);
3903                  $contextsql = "SELECT c.id, " . context_helper::get_preload_record_columns_sql('ctx') . "
3904                                  FROM {course} c
3905                                  JOIN {context} ctx ON ctx.contextlevel = :contextlevel AND ctx.instanceid = c.id
3906                                  WHERE c.id $insql";
3907                  $params['contextlevel'] = CONTEXT_COURSE;
3908                  $contextrecords = $DB->get_recordset_sql($contextsql, $params);
3909                  foreach ($contextrecords as $course) {
3910                      context_helper::preload_from_record($course);
3911                      $coursecontext = context_course::instance($course->id);
3912                      if (has_capability('moodle/calendar:manageentries', $coursecontext)
3913                              && ($courseid == $course->id || empty($courseid))) {
3914                          $types['course'] = true;
3915                          break;
3916                      }
3917                  }
3918                  $contextrecords->close();
3919              }
3920  
3921          }
3922      }
3923  
3924      return $types;
3925  }
3926  
3927  /**
3928   * Given a course id, and context, updates the permission types array to add the 'course' or 'group'
3929   * permission if it is relevant for that course.
3930   *
3931   * For efficiency, if they already have 'course' or 'group' then it skips checks.
3932   *
3933   * Do not call this function directly, it is only for use by calendar_get_allowed_event_types().
3934   *
3935   * @param int $courseid Course id
3936   * @param context $context Context for that course
3937   * @param array $types Current permissions
3938   */
3939  function calendar_internal_update_course_and_group_permission(int $courseid, context $context, array &$types) {
3940      if (!$types['course']) {
3941          // If they have manageentries permission on the course, then they can update this course.
3942          if (has_capability('moodle/calendar:manageentries', $context)) {
3943              $types['course'] = true;
3944          }
3945      }
3946      // To update group events they must have EITHER manageentries OR managegroupentries.
3947      if (!$types['group'] && (has_capability('moodle/calendar:manageentries', $context) ||
3948              has_capability('moodle/calendar:managegroupentries', $context))) {
3949          // And they also need for a group to exist on the course.
3950          $groups = groups_get_all_groups($courseid);
3951          if (!empty($groups)) {
3952              // And either accessallgroups, or belong to one of the groups.
3953              if (has_capability('moodle/site:accessallgroups', $context)) {
3954                  $types['group'] = true;
3955              } else {
3956                  foreach ($groups as $group) {
3957                      if (groups_is_member($group->id)) {
3958                          $types['group'] = true;
3959                          break;
3960                      }
3961                  }
3962              }
3963          }
3964      }
3965  }
3966  
3967  /**
3968   * Get the auth token for exporting the given user calendar.
3969   * @param stdClass $user The user to export the calendar for
3970   *
3971   * @return string The export token.
3972   */
3973  function calendar_get_export_token(stdClass $user): string {
3974      global $CFG, $DB;
3975  
3976      return sha1($user->id . $DB->get_field('user', 'password', ['id' => $user->id]) . $CFG->calendar_exportsalt);
3977  }
3978  
3979  /**
3980   * Get the list of URL parameters for calendar expport and import links.
3981   *
3982   * @return array
3983   */
3984  function calendar_get_export_import_link_params(): array {
3985      global $PAGE;
3986  
3987      $params = [];
3988      if ($courseid = $PAGE->url->get_param('course')) {
3989          $params['course'] = $courseid;
3990      }
3991      if ($categoryid = $PAGE->url->get_param('category')) {
3992          $params['category'] = $categoryid;
3993      }
3994  
3995      return $params;
3996  }
3997  
3998  /**
3999   * Implements the inplace editable feature.
4000   *
4001   * @param string $itemtype Type of the inplace editable element
4002   * @param int $itemid Id of the item to edit
4003   * @param int $newvalue New value of the item
4004   * @return \core\output\inplace_editable
4005   */
4006  function calendar_inplace_editable(string $itemtype, int $itemid, int $newvalue): \core\output\inplace_editable {
4007      global $OUTPUT;
4008  
4009      if ($itemtype === 'refreshinterval') {
4010  
4011          $subscription = calendar_get_subscription($itemid);
4012          $context = calendar_get_calendar_context($subscription);
4013          \external_api::validate_context($context);
4014  
4015          $updateresult = \core_calendar\output\refreshintervalcollection::update($itemid, $newvalue);
4016  
4017          $refreshresults = calendar_update_subscription_events($itemid);
4018          \core\notification::add($OUTPUT->render_from_template(
4019              'core_calendar/subscription_update_result',
4020              array_merge($refreshresults, [
4021                  'subscriptionname' => s($subscription->name),
4022              ])
4023          ), \core\notification::INFO);
4024  
4025          return $updateresult;
4026      }
4027  
4028      \external_api::validate_context(context_system::instance());
4029  }