Search moodle.org's
Developer Documentation

See Release Notes

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

Differences Between: [Versions 310 and 400] [Versions 311 and 400] [Versions 39 and 400] [Versions 400 and 401] [Versions 400 and 402] [Versions 400 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                      print_error('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                      print_error('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                          print_error('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);
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   * Checks to see if the requested type of event should be shown for the given user.
2556   *
2557   * @param int $type The type to check the display for (default is to display all)
2558   * @param stdClass|int|null $user The user to check for - by default the current user
2559   * @return bool True if the tyep should be displayed false otherwise
2560   */
2561  function calendar_show_event_type($type, $user = null) {
2562      $default = CALENDAR_EVENT_SITE + CALENDAR_EVENT_COURSE + CALENDAR_EVENT_GROUP + CALENDAR_EVENT_USER;
2563  
2564      if ((int)get_user_preferences('calendar_persistflt', 0, $user) === 0) {
2565          global $SESSION;
2566          if (!isset($SESSION->calendarshoweventtype)) {
2567              $SESSION->calendarshoweventtype = $default;
2568          }
2569          return $SESSION->calendarshoweventtype & $type;
2570      } else {
2571          return get_user_preferences('calendar_savedflt', $default, $user) & $type;
2572      }
2573  }
2574  
2575  /**
2576   * Sets the display of the event type given $display.
2577   *
2578   * If $display = true the event type will be shown.
2579   * If $display = false the event type will NOT be shown.
2580   * If $display = null the current value will be toggled and saved.
2581   *
2582   * @param int $type object of CALENDAR_EVENT_XXX
2583   * @param bool $display option to display event type
2584   * @param stdClass|int $user moodle user object or id, null means current user
2585   */
2586  function calendar_set_event_type_display($type, $display = null, $user = null) {
2587      $persist = (int)get_user_preferences('calendar_persistflt', 0, $user);
2588      $default = CALENDAR_EVENT_SITE + CALENDAR_EVENT_COURSE + CALENDAR_EVENT_GROUP
2589              + CALENDAR_EVENT_USER + CALENDAR_EVENT_COURSECAT;
2590      if ($persist === 0) {
2591          global $SESSION;
2592          if (!isset($SESSION->calendarshoweventtype)) {
2593              $SESSION->calendarshoweventtype = $default;
2594          }
2595          $preference = $SESSION->calendarshoweventtype;
2596      } else {
2597          $preference = get_user_preferences('calendar_savedflt', $default, $user);
2598      }
2599      $current = $preference & $type;
2600      if ($display === null) {
2601          $display = !$current;
2602      }
2603      if ($display && !$current) {
2604          $preference += $type;
2605      } else if (!$display && $current) {
2606          $preference -= $type;
2607      }
2608      if ($persist === 0) {
2609          $SESSION->calendarshoweventtype = $preference;
2610      } else {
2611          if ($preference == $default) {
2612              unset_user_preference('calendar_savedflt', $user);
2613          } else {
2614              set_user_preference('calendar_savedflt', $preference, $user);
2615          }
2616      }
2617  }
2618  
2619  /**
2620   * Get calendar's allowed types.
2621   *
2622   * @param stdClass $allowed list of allowed edit for event  type
2623   * @param stdClass|int $course object of a course or course id
2624   * @param array $groups array of groups for the given course
2625   * @param stdClass|int $category object of a category
2626   */
2627  function calendar_get_allowed_types(&$allowed, $course = null, $groups = null, $category = null) {
2628      global $USER, $DB;
2629  
2630      $allowed = new \stdClass();
2631      $allowed->user = has_capability('moodle/calendar:manageownentries', \context_system::instance());
2632      $allowed->groups = false;
2633      $allowed->courses = false;
2634      $allowed->categories = false;
2635      $allowed->site = has_capability('moodle/calendar:manageentries', \context_course::instance(SITEID));
2636      $getgroupsfunc = function($course, $context, $user) use ($groups) {
2637          if ($course->groupmode != NOGROUPS || !$course->groupmodeforce) {
2638              if (has_capability('moodle/site:accessallgroups', $context)) {
2639                  return is_null($groups) ? groups_get_all_groups($course->id) : $groups;
2640              } else {
2641                  if (is_null($groups)) {
2642                      return groups_get_all_groups($course->id, $user->id);
2643                  } else {
2644                      return array_filter($groups, function($group) use ($user) {
2645                          return isset($group->members[$user->id]);
2646                      });
2647                  }
2648              }
2649          }
2650  
2651          return false;
2652      };
2653  
2654      if (!empty($course)) {
2655          if (!is_object($course)) {
2656              $course = $DB->get_record('course', array('id' => $course), 'id, groupmode, groupmodeforce', MUST_EXIST);
2657          }
2658          if ($course->id != SITEID) {
2659              $coursecontext = \context_course::instance($course->id);
2660              $allowed->user = has_capability('moodle/calendar:manageownentries', $coursecontext);
2661  
2662              if (has_capability('moodle/calendar:manageentries', $coursecontext)) {
2663                  $allowed->courses = array($course->id => 1);
2664                  $allowed->groups = $getgroupsfunc($course, $coursecontext, $USER);
2665              } else if (has_capability('moodle/calendar:managegroupentries', $coursecontext)) {
2666                  $allowed->groups = $getgroupsfunc($course, $coursecontext, $USER);
2667              }
2668          }
2669      }
2670  
2671      if (!empty($category)) {
2672          $catcontext = \context_coursecat::instance($category->id);
2673          if (has_capability('moodle/category:manage', $catcontext)) {
2674              $allowed->categories = [$category->id => 1];
2675          }
2676      }
2677  }
2678  
2679  /**
2680   * See if user can add calendar entries at all used to print the "New Event" button.
2681   *
2682   * @param stdClass $course object of a course or course id
2683   * @return bool has the capability to add at least one event type
2684   */
2685  function calendar_user_can_add_event($course) {
2686      if (!isloggedin() || isguestuser()) {
2687          return false;
2688      }
2689  
2690      calendar_get_allowed_types($allowed, $course);
2691  
2692      return (bool)($allowed->user || $allowed->groups || $allowed->courses || $allowed->categories || $allowed->site);
2693  }
2694  
2695  /**
2696   * Check wether the current user is permitted to add events.
2697   *
2698   * @param stdClass $event object of event
2699   * @return bool has the capability to add event
2700   */
2701  function calendar_add_event_allowed($event) {
2702      global $USER, $DB;
2703  
2704      // Can not be using guest account.
2705      if (!isloggedin() or isguestuser()) {
2706          return false;
2707      }
2708  
2709      if (calendar_can_manage_non_user_event_in_system($event)) {
2710          return true;
2711      }
2712  
2713      switch ($event->eventtype) {
2714          case 'category':
2715              return has_capability('moodle/category:manage', $event->context);
2716          case 'course':
2717              return has_capability('moodle/calendar:manageentries', $event->context);
2718          case 'group':
2719              // Allow users to add/edit group events if -
2720              // 1) They have manageentries (= entries for whole course).
2721              // 2) They have managegroupentries AND are in the group.
2722              $group = $DB->get_record('groups', array('id' => $event->groupid));
2723              return $group && (
2724                      has_capability('moodle/calendar:manageentries', $event->context) ||
2725                      (has_capability('moodle/calendar:managegroupentries', $event->context)
2726                          && groups_is_member($event->groupid)));
2727          case 'user':
2728              return calendar_can_manage_user_event($event);
2729          case 'site':
2730              return has_capability('moodle/calendar:manageentries', $event->context);
2731          default:
2732              return has_capability('moodle/calendar:manageentries', $event->context);
2733      }
2734  }
2735  
2736  /**
2737   * Returns option list for the poll interval setting.
2738   *
2739   * @return array An array of poll interval options. Interval => description.
2740   */
2741  function calendar_get_pollinterval_choices() {
2742      return array(
2743          '0' => get_string('never', 'calendar'),
2744          HOURSECS => get_string('hourly', 'calendar'),
2745          DAYSECS => get_string('daily', 'calendar'),
2746          WEEKSECS => get_string('weekly', 'calendar'),
2747          '2628000' => get_string('monthly', 'calendar'),
2748          YEARSECS => get_string('annually', 'calendar')
2749      );
2750  }
2751  
2752  /**
2753   * Returns option list of available options for the calendar event type, given the current user and course.
2754   *
2755   * @param int $courseid The id of the course
2756   * @return array An array containing the event types the user can create.
2757   */
2758  function calendar_get_eventtype_choices($courseid) {
2759      $choices = array();
2760      $allowed = new \stdClass;
2761      calendar_get_allowed_types($allowed, $courseid);
2762  
2763      if ($allowed->user) {
2764          $choices['user'] = get_string('userevents', 'calendar');
2765      }
2766      if ($allowed->site) {
2767          $choices['site'] = get_string('siteevents', 'calendar');
2768      }
2769      if (!empty($allowed->courses)) {
2770          $choices['course'] = get_string('courseevents', 'calendar');
2771      }
2772      if (!empty($allowed->categories)) {
2773          $choices['category'] = get_string('categoryevents', 'calendar');
2774      }
2775      if (!empty($allowed->groups) and is_array($allowed->groups)) {
2776          $choices['group'] = get_string('group');
2777      }
2778  
2779      return array($choices, $allowed->groups);
2780  }
2781  
2782  /**
2783   * Add an iCalendar subscription to the database.
2784   *
2785   * @param stdClass $sub The subscription object (e.g. from the form)
2786   * @return int The insert ID, if any.
2787   */
2788  function calendar_add_subscription($sub) {
2789      global $DB, $USER, $SITE;
2790  
2791      // Undo the form definition work around to allow us to have two different
2792      // course selectors present depending on which event type the user selects.
2793      if (!empty($sub->groupcourseid)) {
2794          $sub->courseid = $sub->groupcourseid;
2795          unset($sub->groupcourseid);
2796      }
2797  
2798      // Default course id if none is set.
2799      if (empty($sub->courseid)) {
2800          if ($sub->eventtype === 'site') {
2801              $sub->courseid = SITEID;
2802          } else {
2803              $sub->courseid = 0;
2804          }
2805      }
2806  
2807      if ($sub->eventtype === 'site') {
2808          $sub->courseid = $SITE->id;
2809      } else if ($sub->eventtype === 'group' || $sub->eventtype === 'course') {
2810          $sub->courseid = $sub->courseid;
2811      } else if ($sub->eventtype === 'category') {
2812          $sub->categoryid = $sub->categoryid;
2813      } else {
2814          // User events.
2815          $sub->courseid = 0;
2816      }
2817      $sub->userid = $USER->id;
2818  
2819      // File subscriptions never update.
2820      if (empty($sub->url)) {
2821          $sub->pollinterval = 0;
2822      }
2823  
2824      if (!empty($sub->name)) {
2825          if (empty($sub->id)) {
2826              $id = $DB->insert_record('event_subscriptions', $sub);
2827              // We cannot cache the data here because $sub is not complete.
2828              $sub->id = $id;
2829              // Trigger event, calendar subscription added.
2830              $eventparams = array('objectid' => $sub->id,
2831                  'context' => calendar_get_calendar_context($sub),
2832                  'other' => array(
2833                      'eventtype' => $sub->eventtype,
2834                  )
2835              );
2836              switch ($sub->eventtype) {
2837                  case 'category':
2838                      $eventparams['other']['categoryid'] = $sub->categoryid;
2839                      break;
2840                  case 'course':
2841                      $eventparams['other']['courseid'] = $sub->courseid;
2842                      break;
2843                  case 'group':
2844                      $eventparams['other']['courseid'] = $sub->courseid;
2845                      $eventparams['other']['groupid'] = $sub->groupid;
2846                      break;
2847                  default:
2848                      $eventparams['other']['courseid'] = $sub->courseid;
2849              }
2850  
2851              $event = \core\event\calendar_subscription_created::create($eventparams);
2852              $event->trigger();
2853              return $id;
2854          } else {
2855              // Why are we doing an update here?
2856              calendar_update_subscription($sub);
2857              return $sub->id;
2858          }
2859      } else {
2860          print_error('errorbadsubscription', 'importcalendar');
2861      }
2862  }
2863  
2864  /**
2865   * Add an iCalendar event to the Moodle calendar.
2866   *
2867   * @param stdClass $event The RFC-2445 iCalendar event
2868   * @param int $unused Deprecated
2869   * @param int $subscriptionid The iCalendar subscription ID
2870   * @param string $timezone The X-WR-TIMEZONE iCalendar property if provided
2871   * @throws dml_exception A DML specific exception is thrown for invalid subscriptionids.
2872   * @return int Code: CALENDAR_IMPORT_EVENT_UPDATED = updated,  CALENDAR_IMPORT_EVENT_INSERTED = inserted, 0 = error
2873   */
2874  function calendar_add_icalendar_event($event, $unused, $subscriptionid, $timezone='UTC') {
2875      global $DB;
2876  
2877      // Probably an unsupported X-MICROSOFT-CDO-BUSYSTATUS event.
2878      if (empty($event->properties['SUMMARY'])) {
2879          return 0;
2880      }
2881  
2882      $name = $event->properties['SUMMARY'][0]->value;
2883      $name = str_replace('\n', '<br />', $name);
2884      $name = str_replace('\\', '', $name);
2885      $name = preg_replace('/\s+/u', ' ', $name);
2886  
2887      $eventrecord = new \stdClass;
2888      $eventrecord->name = clean_param($name, PARAM_NOTAGS);
2889  
2890      if (empty($event->properties['DESCRIPTION'][0]->value)) {
2891          $description = '';
2892      } else {
2893          $description = $event->properties['DESCRIPTION'][0]->value;
2894          $description = clean_param($description, PARAM_NOTAGS);
2895          $description = str_replace('\n', '<br />', $description);
2896          $description = str_replace('\\', '', $description);
2897          $description = preg_replace('/\s+/u', ' ', $description);
2898      }
2899      $eventrecord->description = $description;
2900  
2901      // Probably a repeating event with RRULE etc. TODO: skip for now.
2902      if (empty($event->properties['DTSTART'][0]->value)) {
2903          return 0;
2904      }
2905  
2906      if (isset($event->properties['DTSTART'][0]->parameters['TZID'])) {
2907          $tz = $event->properties['DTSTART'][0]->parameters['TZID'];
2908      } else {
2909          $tz = $timezone;
2910      }
2911      $tz = \core_date::normalise_timezone($tz);
2912      $eventrecord->timestart = strtotime($event->properties['DTSTART'][0]->value . ' ' . $tz);
2913      if (empty($event->properties['DTEND'])) {
2914          $eventrecord->timeduration = 0; // No duration if no end time specified.
2915      } else {
2916          if (isset($event->properties['DTEND'][0]->parameters['TZID'])) {
2917              $endtz = $event->properties['DTEND'][0]->parameters['TZID'];
2918          } else {
2919              $endtz = $timezone;
2920          }
2921          $endtz = \core_date::normalise_timezone($endtz);
2922          $eventrecord->timeduration = strtotime($event->properties['DTEND'][0]->value . ' ' . $endtz) - $eventrecord->timestart;
2923      }
2924  
2925      // Check to see if it should be treated as an all day event.
2926      if ($eventrecord->timeduration == DAYSECS) {
2927          // Check to see if the event started at Midnight on the imported calendar.
2928          date_default_timezone_set($timezone);
2929          if (date('H:i:s', $eventrecord->timestart) === "00:00:00") {
2930              // This event should be an all day event. This is not correct, we don't do anything differently for all day events.
2931              // See MDL-56227.
2932              $eventrecord->timeduration = 0;
2933          }
2934          \core_date::set_default_server_timezone();
2935      }
2936  
2937      $eventrecord->location = empty($event->properties['LOCATION'][0]->value) ? '' :
2938              trim(str_replace('\\', '', $event->properties['LOCATION'][0]->value));
2939      $eventrecord->uuid = $event->properties['UID'][0]->value;
2940      $eventrecord->timemodified = time();
2941  
2942      // Add the iCal subscription details if required.
2943      // We should never do anything with an event without a subscription reference.
2944      $sub = calendar_get_subscription($subscriptionid);
2945      $eventrecord->subscriptionid = $subscriptionid;
2946      $eventrecord->userid = $sub->userid;
2947      $eventrecord->groupid = $sub->groupid;
2948      $eventrecord->courseid = $sub->courseid;
2949      $eventrecord->categoryid = $sub->categoryid;
2950      $eventrecord->eventtype = $sub->eventtype;
2951  
2952      if ($updaterecord = $DB->get_record('event', array('uuid' => $eventrecord->uuid,
2953          'subscriptionid' => $eventrecord->subscriptionid))) {
2954  
2955          // Compare iCal event data against the moodle event to see if something has changed.
2956          $result = array_diff((array) $eventrecord, (array) $updaterecord);
2957  
2958          // Unset timemodified field because it's always going to be different.
2959          unset($result['timemodified']);
2960  
2961          if (count($result)) {
2962              $eventrecord->id = $updaterecord->id;
2963              $return = CALENDAR_IMPORT_EVENT_UPDATED; // Update.
2964          } else {
2965              return CALENDAR_IMPORT_EVENT_SKIPPED;
2966          }
2967      } else {
2968          $return = CALENDAR_IMPORT_EVENT_INSERTED; // Insert.
2969      }
2970  
2971      if ($createdevent = \calendar_event::create($eventrecord, false)) {
2972          if (!empty($event->properties['RRULE'])) {
2973              // Repeating events.
2974              date_default_timezone_set($tz); // Change time zone to parse all events.
2975              $rrule = new \core_calendar\rrule_manager($event->properties['RRULE'][0]->value);
2976              $rrule->parse_rrule();
2977              $rrule->create_events($createdevent);
2978              \core_date::set_default_server_timezone(); // Change time zone back to what it was.
2979          }
2980          return $return;
2981      } else {
2982          return 0;
2983      }
2984  }
2985  
2986  /**
2987   * Delete subscription and all related events.
2988   *
2989   * @param int|stdClass $subscription subscription or it's id, which needs to be deleted.
2990   */
2991  function calendar_delete_subscription($subscription) {
2992      global $DB;
2993  
2994      if (!is_object($subscription)) {
2995          $subscription = $DB->get_record('event_subscriptions', array('id' => $subscription), '*', MUST_EXIST);
2996      }
2997  
2998      // Delete subscription and related events.
2999      $DB->delete_records('event', array('subscriptionid' => $subscription->id));
3000      $DB->delete_records('event_subscriptions', array('id' => $subscription->id));
3001      \cache_helper::invalidate_by_definition('core', 'calendar_subscriptions', array(), array($subscription->id));
3002  
3003      // Trigger event, calendar subscription deleted.
3004      $eventparams = array('objectid' => $subscription->id,
3005          'context' => calendar_get_calendar_context($subscription),
3006          'other' => array(
3007              'eventtype' => $subscription->eventtype,
3008          )
3009      );
3010      switch ($subscription->eventtype) {
3011          case 'category':
3012              $eventparams['other']['categoryid'] = $subscription->categoryid;
3013              break;
3014          case 'course':
3015              $eventparams['other']['courseid'] = $subscription->courseid;
3016              break;
3017          case 'group':
3018              $eventparams['other']['courseid'] = $subscription->courseid;
3019              $eventparams['other']['groupid'] = $subscription->groupid;
3020              break;
3021          default:
3022              $eventparams['other']['courseid'] = $subscription->courseid;
3023      }
3024      $event = \core\event\calendar_subscription_deleted::create($eventparams);
3025      $event->trigger();
3026  }
3027  
3028  /**
3029   * From a URL, fetch the calendar and return an iCalendar object.
3030   *
3031   * @param string $url The iCalendar URL
3032   * @return iCalendar The iCalendar object
3033   */
3034  function calendar_get_icalendar($url) {
3035      global $CFG;
3036  
3037      require_once($CFG->libdir . '/filelib.php');
3038      require_once($CFG->libdir . '/bennu/bennu.inc.php');
3039  
3040      $curl = new \curl();
3041      $curl->setopt(array('CURLOPT_FOLLOWLOCATION' => 1, 'CURLOPT_MAXREDIRS' => 5));
3042      $calendar = $curl->get($url);
3043  
3044      // Http code validation should actually be the job of curl class.
3045      if (!$calendar || $curl->info['http_code'] != 200 || !empty($curl->errorno)) {
3046          throw new \moodle_exception('errorinvalidicalurl', 'calendar');
3047      }
3048  
3049      $ical = new \iCalendar();
3050      $ical->unserialize($calendar);
3051  
3052      return $ical;
3053  }
3054  
3055  /**
3056   * Import events from an iCalendar object into a course calendar.
3057   *
3058   * @param iCalendar $ical The iCalendar object.
3059   * @param int|null $subscriptionid The subscription ID.
3060   * @return array A log of the import progress, including errors.
3061   */
3062  function calendar_import_events_from_ical(iCalendar $ical, int $subscriptionid = null): array {
3063      global $DB;
3064  
3065      $errors = [];
3066      $eventcount = 0;
3067      $updatecount = 0;
3068      $skippedcount = 0;
3069      $deletedcount = 0;
3070  
3071      // Large calendars take a while...
3072      if (!CLI_SCRIPT) {
3073          \core_php_time_limit::raise(300);
3074      }
3075  
3076      // Start with a safe default timezone.
3077      $timezone = 'UTC';
3078  
3079      // Grab the timezone from the iCalendar file to be used later.
3080      if (isset($ical->properties['X-WR-TIMEZONE'][0]->value)) {
3081          $timezone = $ical->properties['X-WR-TIMEZONE'][0]->value;
3082  
3083      } else if (isset($ical->properties['PRODID'][0]->value)) {
3084          // If the timezone was not found, check to se if this is MS exchange / Office 365 which uses Windows timezones.
3085          if (strncmp($ical->properties['PRODID'][0]->value, 'Microsoft', 9) == 0) {
3086              if (isset($ical->components['VTIMEZONE'][0]->properties['TZID'][0]->value)) {
3087                  $tzname = $ical->components['VTIMEZONE'][0]->properties['TZID'][0]->value;
3088                  $timezone = IntlTimeZone::getIDForWindowsID($tzname);
3089              }
3090          }
3091      }
3092  
3093      $icaluuids = [];
3094      foreach ($ical->components['VEVENT'] as $event) {
3095          $icaluuids[] = $event->properties['UID'][0]->value;
3096          $res = calendar_add_icalendar_event($event, null, $subscriptionid, $timezone);
3097          switch ($res) {
3098              case CALENDAR_IMPORT_EVENT_UPDATED:
3099                  $updatecount++;
3100                  break;
3101              case CALENDAR_IMPORT_EVENT_INSERTED:
3102                  $eventcount++;
3103                  break;
3104              case CALENDAR_IMPORT_EVENT_SKIPPED:
3105                  $skippedcount++;
3106                  break;
3107              case 0:
3108                  if (empty($event->properties['SUMMARY'])) {
3109                      $errors[] = '(' . get_string('notitle', 'calendar') . ')';
3110                  } else {
3111                      $errors[] = $event->properties['SUMMARY'][0]->value;
3112                  }
3113                  break;
3114          }
3115      }
3116  
3117      $existing = $DB->get_field('event_subscriptions', 'lastupdated', ['id' => $subscriptionid]);
3118      if (!empty($existing)) {
3119          $eventsuuids = $DB->get_records_menu('event', ['subscriptionid' => $subscriptionid], '', 'id, uuid');
3120  
3121          $icaleventscount = count($icaluuids);
3122          $tobedeleted = [];
3123          if (count($eventsuuids) > $icaleventscount) {
3124              foreach ($eventsuuids as $eventid => $eventuuid) {
3125                  if (!in_array($eventuuid, $icaluuids)) {
3126                      $tobedeleted[] = $eventid;
3127                  }
3128              }
3129              if (!empty($tobedeleted)) {
3130                  $DB->delete_records_list('event', 'id', $tobedeleted);
3131                  $deletedcount = count($tobedeleted);
3132              }
3133          }
3134      }
3135  
3136      $result = [
3137          'eventsimported' => $eventcount,
3138          'eventsskipped' => $skippedcount,
3139          'eventsupdated' => $updatecount,
3140          'eventsdeleted' => $deletedcount,
3141          'haserror' => !empty($errors),
3142          'errors' => $errors,
3143      ];
3144  
3145      return $result;
3146  }
3147  
3148  /**
3149   * Fetch a calendar subscription and update the events in the calendar.
3150   *
3151   * @param int $subscriptionid The course ID for the calendar.
3152   * @return string A log of the import progress, including errors.
3153   */
3154  function calendar_update_subscription_events($subscriptionid) {
3155      $sub = calendar_get_subscription($subscriptionid);
3156  
3157      // Don't update a file subscription.
3158      if (empty($sub->url)) {
3159          return 'File subscription not updated.';
3160      }
3161  
3162      $ical = calendar_get_icalendar($sub->url);
3163      $return = calendar_import_events_from_ical($ical, $subscriptionid);
3164      $sub->lastupdated = time();
3165  
3166      calendar_update_subscription($sub);
3167  
3168      return $return;
3169  }
3170  
3171  /**
3172   * Update a calendar subscription. Also updates the associated cache.
3173   *
3174   * @param stdClass|array $subscription Subscription record.
3175   * @throws coding_exception If something goes wrong
3176   * @since Moodle 2.5
3177   */
3178  function calendar_update_subscription($subscription) {
3179      global $DB;
3180  
3181      if (is_array($subscription)) {
3182          $subscription = (object)$subscription;
3183      }
3184      if (empty($subscription->id) || !$DB->record_exists('event_subscriptions', array('id' => $subscription->id))) {
3185          throw new \coding_exception('Cannot update a subscription without a valid id');
3186      }
3187  
3188      $DB->update_record('event_subscriptions', $subscription);
3189  
3190      // Update cache.
3191      $cache = \cache::make('core', 'calendar_subscriptions');
3192      $cache->set($subscription->id, $subscription);
3193  
3194      // Trigger event, calendar subscription updated.
3195      $eventparams = array('userid' => $subscription->userid,
3196          'objectid' => $subscription->id,
3197          'context' => calendar_get_calendar_context($subscription),
3198          'other' => array(
3199              'eventtype' => $subscription->eventtype,
3200          )
3201      );
3202      switch ($subscription->eventtype) {
3203          case 'category':
3204              $eventparams['other']['categoryid'] = $subscription->categoryid;
3205              break;
3206          case 'course':
3207              $eventparams['other']['courseid'] = $subscription->courseid;
3208              break;
3209          case 'group':
3210              $eventparams['other']['courseid'] = $subscription->courseid;
3211              $eventparams['other']['groupid'] = $subscription->groupid;
3212              break;
3213          default:
3214              $eventparams['other']['courseid'] = $subscription->courseid;
3215      }
3216      $event = \core\event\calendar_subscription_updated::create($eventparams);
3217      $event->trigger();
3218  }
3219  
3220  /**
3221   * Checks to see if the user can edit a given subscription feed.
3222   *
3223   * @param mixed $subscriptionorid Subscription object or id
3224   * @return bool true if current user can edit the subscription else false
3225   */
3226  function calendar_can_edit_subscription($subscriptionorid) {
3227      global $USER;
3228      if (is_array($subscriptionorid)) {
3229          $subscription = (object)$subscriptionorid;
3230      } else if (is_object($subscriptionorid)) {
3231          $subscription = $subscriptionorid;
3232      } else {
3233          $subscription = calendar_get_subscription($subscriptionorid);
3234      }
3235  
3236      $allowed = new \stdClass;
3237      $courseid = $subscription->courseid;
3238      $categoryid = $subscription->categoryid;
3239      $groupid = $subscription->groupid;
3240      $category = null;
3241  
3242      if (!empty($categoryid)) {
3243          $category = \core_course_category::get($categoryid);
3244      }
3245      calendar_get_allowed_types($allowed, $courseid, null, $category);
3246      switch ($subscription->eventtype) {
3247          case 'user':
3248              return ($USER->id == $subscription->userid && $allowed->user);
3249          case 'course':
3250              if (isset($allowed->courses[$courseid])) {
3251                  return $allowed->courses[$courseid];
3252              } else {
3253                  return false;
3254              }
3255          case 'category':
3256              if (isset($allowed->categories[$categoryid])) {
3257                  return $allowed->categories[$categoryid];
3258              } else {
3259                  return false;
3260              }
3261          case 'site':
3262              return $allowed->site;
3263          case 'group':
3264              if (isset($allowed->groups[$groupid])) {
3265                  return $allowed->groups[$groupid];
3266              } else {
3267                  return false;
3268              }
3269          default:
3270              return false;
3271      }
3272  }
3273  
3274  /**
3275   * Helper function to determine the context of a calendar subscription.
3276   * Subscriptions can be created in two contexts COURSE, or USER.
3277   *
3278   * @param stdClass $subscription
3279   * @return context instance
3280   */
3281  function calendar_get_calendar_context($subscription) {
3282      // Determine context based on calendar type.
3283      if ($subscription->eventtype === 'site') {
3284          $context = \context_course::instance(SITEID);
3285      } else if ($subscription->eventtype === 'group' || $subscription->eventtype === 'course') {
3286          $context = \context_course::instance($subscription->courseid);
3287      } else {
3288          $context = \context_user::instance($subscription->userid);
3289      }
3290      return $context;
3291  }
3292  
3293  /**
3294   * Implements callback user_preferences, lists preferences that users are allowed to update directly
3295   *
3296   * Used in {@see core_user::fill_preferences_cache()}, see also {@see useredit_update_user_preference()}
3297   *
3298   * @return array
3299   */
3300  function core_calendar_user_preferences() {
3301      $preferences = [];
3302      $preferences['calendar_timeformat'] = array('type' => PARAM_NOTAGS, 'null' => NULL_NOT_ALLOWED, 'default' => '0',
3303          'choices' => array('0', CALENDAR_TF_12, CALENDAR_TF_24)
3304      );
3305      $preferences['calendar_startwday'] = array('type' => PARAM_INT, 'null' => NULL_NOT_ALLOWED, 'default' => 0,
3306          'choices' => array(0, 1, 2, 3, 4, 5, 6));
3307      $preferences['calendar_maxevents'] = array('type' => PARAM_INT, 'choices' => range(1, 20));
3308      $preferences['calendar_lookahead'] = array('type' => PARAM_INT, 'null' => NULL_NOT_ALLOWED, 'default' => 365,
3309          'choices' => array(365, 270, 180, 150, 120, 90, 60, 30, 21, 14, 7, 6, 5, 4, 3, 2, 1));
3310      $preferences['calendar_persistflt'] = array('type' => PARAM_INT, 'null' => NULL_NOT_ALLOWED, 'default' => 0,
3311          'choices' => array(0, 1));
3312      return $preferences;
3313  }
3314  
3315  /**
3316   * Get legacy calendar events
3317   *
3318   * @param int $tstart Start time of time range for events
3319   * @param int $tend End time of time range for events
3320   * @param array|int|boolean $users array of users, user id or boolean for all/no user events
3321   * @param array|int|boolean $groups array of groups, group id or boolean for all/no group events
3322   * @param array|int|boolean $courses array of courses, course id or boolean for all/no course events
3323   * @param boolean $withduration whether only events starting within time range selected
3324   *                              or events in progress/already started selected as well
3325   * @param boolean $ignorehidden whether to select only visible events or all events
3326   * @param array $categories array of category ids and/or objects.
3327   * @param int $limitnum Number of events to fetch or zero to fetch all.
3328   *
3329   * @return array $events of selected events or an empty array if there aren't any (or there was an error)
3330   */
3331  function calendar_get_legacy_events($tstart, $tend, $users, $groups, $courses,
3332          $withduration = true, $ignorehidden = true, $categories = [], $limitnum = 0) {
3333      // Normalise the users, groups and courses parameters so that they are compliant with \core_calendar\local\api::get_events().
3334      // Existing functions that were using the old calendar_get_events() were passing a mixture of array, int, boolean for these
3335      // parameters, but with the new API method, only null and arrays are accepted.
3336      list($userparam, $groupparam, $courseparam, $categoryparam) = array_map(function($param) {
3337          // If parameter is true, return null.
3338          if ($param === true) {
3339              return null;
3340          }
3341  
3342          // If parameter is false, return an empty array.
3343          if ($param === false) {
3344              return [];
3345          }
3346  
3347          // If the parameter is a scalar value, enclose it in an array.
3348          if (!is_array($param)) {
3349              return [$param];
3350          }
3351  
3352          // No normalisation required.
3353          return $param;
3354      }, [$users, $groups, $courses, $categories]);
3355  
3356      // If a single user is provided, we can use that for capability checks.
3357      // Otherwise current logged in user is used - See MDL-58768.
3358      if (is_array($userparam) && count($userparam) == 1) {
3359          \core_calendar\local\event\container::set_requesting_user($userparam[0]);
3360      }
3361      $mapper = \core_calendar\local\event\container::get_event_mapper();
3362      $events = \core_calendar\local\api::get_events(
3363          $tstart,
3364          $tend,
3365          null,
3366          null,
3367          null,
3368          null,
3369          $limitnum,
3370          null,
3371          $userparam,
3372          $groupparam,
3373          $courseparam,
3374          $categoryparam,
3375          $withduration,
3376          $ignorehidden
3377      );
3378  
3379      return array_reduce($events, function($carry, $event) use ($mapper) {
3380          return $carry + [$event->get_id() => $mapper->from_event_to_stdclass($event)];
3381      }, []);
3382  }
3383  
3384  
3385  /**
3386   * Get the calendar view output.
3387   *
3388   * @param   \calendar_information $calendar The calendar being represented
3389   * @param   string  $view The type of calendar to have displayed
3390   * @param   bool    $includenavigation Whether to include navigation
3391   * @param   bool    $skipevents Whether to load the events or not
3392   * @param   int     $lookahead Overwrites site and users's lookahead setting.
3393   * @return  array[array, string]
3394   */
3395  function calendar_get_view(\calendar_information $calendar, $view, $includenavigation = true, bool $skipevents = false,
3396          ?int $lookahead = null) {
3397      global $PAGE, $CFG;
3398  
3399      $renderer = $PAGE->get_renderer('core_calendar');
3400      $type = \core_calendar\type_factory::get_calendar_instance();
3401  
3402      // Calculate the bounds of the month.
3403      $calendardate = $type->timestamp_to_date_array($calendar->time);
3404  
3405      $date = new \DateTime('now', core_date::get_user_timezone_object(99));
3406      $eventlimit = 0;
3407  
3408      if ($view === 'day') {
3409          $tstart = $type->convert_to_timestamp($calendardate['year'], $calendardate['mon'], $calendardate['mday']);
3410          $date->setTimestamp($tstart);
3411          $date->modify('+1 day');
3412      } else if ($view === 'upcoming' || $view === 'upcoming_mini') {
3413          // Number of days in the future that will be used to fetch events.
3414          if (!$lookahead) {
3415              if (isset($CFG->calendar_lookahead)) {
3416                  $defaultlookahead = intval($CFG->calendar_lookahead);
3417              } else {
3418                  $defaultlookahead = CALENDAR_DEFAULT_UPCOMING_LOOKAHEAD;
3419              }
3420              $lookahead = get_user_preferences('calendar_lookahead', $defaultlookahead);
3421          }
3422  
3423          // Maximum number of events to be displayed on upcoming view.
3424          $defaultmaxevents = CALENDAR_DEFAULT_UPCOMING_MAXEVENTS;
3425          if (isset($CFG->calendar_maxevents)) {
3426              $defaultmaxevents = intval($CFG->calendar_maxevents);
3427          }
3428          $eventlimit = get_user_preferences('calendar_maxevents', $defaultmaxevents);
3429  
3430          $tstart = $type->convert_to_timestamp($calendardate['year'], $calendardate['mon'], $calendardate['mday'],
3431                  $calendardate['hours']);
3432          $date->setTimestamp($tstart);
3433          $date->modify('+' . $lookahead . ' days');
3434      } else {
3435          $tstart = $type->convert_to_timestamp($calendardate['year'], $calendardate['mon'], 1);
3436          $monthdays = $type->get_num_days_in_month($calendardate['year'], $calendardate['mon']);
3437          $date->setTimestamp($tstart);
3438          $date->modify('+' . $monthdays . ' days');
3439  
3440          if ($view === 'mini' || $view === 'minithree') {
3441              $template = 'core_calendar/calendar_mini';
3442          } else {
3443              $template = 'core_calendar/calendar_month';
3444          }
3445      }
3446  
3447      // We need to extract 1 second to ensure that we don't get into the next day.
3448      $date->modify('-1 second');
3449      $tend = $date->getTimestamp();
3450  
3451      list($userparam, $groupparam, $courseparam, $categoryparam) = array_map(function($param) {
3452          // If parameter is true, return null.
3453          if ($param === true) {
3454              return null;
3455          }
3456  
3457          // If parameter is false, return an empty array.
3458          if ($param === false) {
3459              return [];
3460          }
3461  
3462          // If the parameter is a scalar value, enclose it in an array.
3463          if (!is_array($param)) {
3464              return [$param];
3465          }
3466  
3467          // No normalisation required.
3468          return $param;
3469      }, [$calendar->users, $calendar->groups, $calendar->courses, $calendar->categories]);
3470  
3471      if ($skipevents) {
3472          $events = [];
3473      } else {
3474          $events = \core_calendar\local\api::get_events(
3475              $tstart,
3476              $tend,
3477              null,
3478              null,
3479              null,
3480              null,
3481              $eventlimit,
3482              null,
3483              $userparam,
3484              $groupparam,
3485              $courseparam,
3486              $categoryparam,
3487              true,
3488              true,
3489              function ($event) {
3490                  if ($proxy = $event->get_course_module()) {
3491                      $cminfo = $proxy->get_proxied_instance();
3492                      return $cminfo->uservisible;
3493                  }
3494  
3495                  if ($proxy = $event->get_category()) {
3496                      $category = $proxy->get_proxied_instance();
3497  
3498                      return $category->is_uservisible();
3499                  }
3500  
3501                  return true;
3502              }
3503          );
3504      }
3505  
3506      $related = [
3507          'events' => $events,
3508          'cache' => new \core_calendar\external\events_related_objects_cache($events),
3509          'type' => $type,
3510      ];
3511  
3512      $data = [];
3513      $calendar->set_viewmode($view);
3514      if ($view == "month" || $view == "monthblock" || $view == "mini" || $view == "minithree" ) {
3515          $month = new \core_calendar\external\month_exporter($calendar, $type, $related);
3516          $month->set_includenavigation($includenavigation);
3517          $month->set_initialeventsloaded(!$skipevents);
3518          $month->set_showcoursefilter(($view == "month" || $view == "monthblock"));
3519          $data = $month->export($renderer);
3520      } else if ($view == "day") {
3521          $day = new \core_calendar\external\calendar_day_exporter($calendar, $related);
3522          $data = $day->export($renderer);
3523          $data->viewingday = true;
3524          $data->showviewselector = true;
3525          $template = 'core_calendar/calendar_day';
3526      } else if ($view == "upcoming" || $view == "upcoming_mini") {
3527          $upcoming = new \core_calendar\external\calendar_upcoming_exporter($calendar, $related);
3528          $data = $upcoming->export($renderer);
3529  
3530          if ($view == "upcoming") {
3531              $template = 'core_calendar/calendar_upcoming';
3532              $data->viewingupcoming = true;
3533              $data->showviewselector = true;
3534          } else if ($view == "upcoming_mini") {
3535              $template = 'core_calendar/calendar_upcoming_mini';
3536          }
3537      }
3538  
3539      return [$data, $template];
3540  }
3541  
3542  /**
3543   * Request and render event form fragment.
3544   *
3545   * @param array $args The fragment arguments.
3546   * @return string The rendered mform fragment.
3547   */
3548  function calendar_output_fragment_event_form($args) {
3549      global $CFG, $OUTPUT, $USER;
3550      require_once($CFG->libdir . '/grouplib.php');
3551      $html = '';
3552      $data = [];
3553      $eventid = isset($args['eventid']) ? clean_param($args['eventid'], PARAM_INT) : null;
3554      $starttime = isset($args['starttime']) ? clean_param($args['starttime'], PARAM_INT) : null;
3555      $courseid = (isset($args['courseid']) && $args['courseid'] != SITEID) ? clean_param($args['courseid'], PARAM_INT) : null;
3556      $categoryid = isset($args['categoryid']) ? clean_param($args['categoryid'], PARAM_INT) : null;
3557      $event = null;
3558      $hasformdata = isset($args['formdata']) && !empty($args['formdata']);
3559      $context = \context_user::instance($USER->id);
3560      $editoroptions = \core_calendar\local\event\forms\create::build_editor_options($context);
3561      $formoptions = ['editoroptions' => $editoroptions, 'courseid' => $courseid];
3562      $draftitemid = 0;
3563  
3564      if ($hasformdata) {
3565          parse_str(clean_param($args['formdata'], PARAM_TEXT), $data);
3566          if (isset($data['description']['itemid'])) {
3567              $draftitemid = $data['description']['itemid'];
3568          }
3569      }
3570  
3571      if ($starttime) {
3572          $formoptions['starttime'] = $starttime;
3573      }
3574      // Let's check first which event types user can add.
3575      $eventtypes = calendar_get_allowed_event_types($courseid);
3576      $formoptions['eventtypes'] = $eventtypes;
3577  
3578      if (is_null($eventid)) {
3579          if (!empty($courseid)) {
3580              $groupcoursedata = groups_get_course_data($courseid);
3581              $formoptions['groups'] = [];
3582              foreach ($groupcoursedata->groups as $groupid => $groupdata) {
3583                  $formoptions['groups'][$groupid] = $groupdata->name;
3584              }
3585          }
3586  
3587          $mform = new \core_calendar\local\event\forms\create(
3588              null,
3589              $formoptions,
3590              'post',
3591              '',
3592              null,
3593              true,
3594              $data
3595          );
3596  
3597          // If the user is on course context and is allowed to add course events set the event type default to course.
3598          if (!empty($courseid) && !empty($eventtypes['course'])) {
3599              $data['eventtype'] = 'course';
3600              $data['courseid'] = $courseid;
3601              $data['groupcourseid'] = $courseid;
3602          } else if (!empty($categoryid) && !empty($eventtypes['category'])) {
3603              $data['eventtype'] = 'category';
3604              $data['categoryid'] = $categoryid;
3605          } else if (!empty($groupcoursedata) && !empty($eventtypes['group'])) {
3606              $data['groupcourseid'] = $courseid;
3607              $data['groups'] = $groupcoursedata->groups;
3608          }
3609          $mform->set_data($data);
3610      } else {
3611          $event = calendar_event::load($eventid);
3612  
3613          if (!calendar_edit_event_allowed($event)) {
3614              print_error('nopermissiontoupdatecalendar');
3615          }
3616  
3617          $mapper = new \core_calendar\local\event\mappers\create_update_form_mapper();
3618          $eventdata = $mapper->from_legacy_event_to_data($event);
3619          $data = array_merge((array) $eventdata, $data);
3620          $event->count_repeats();
3621          $formoptions['event'] = $event;
3622  
3623          if (!empty($event->courseid)) {
3624              $groupcoursedata = groups_get_course_data($event->courseid);
3625              $formoptions['groups'] = [];
3626              foreach ($groupcoursedata->groups as $groupid => $groupdata) {
3627                  $formoptions['groups'][$groupid] = $groupdata->name;
3628              }
3629          }
3630  
3631          $data['description']['text'] = file_prepare_draft_area(
3632              $draftitemid,
3633              $event->context->id,
3634              'calendar',
3635              'event_description',
3636              $event->id,
3637              null,
3638              $data['description']['text']
3639          );
3640          $data['description']['itemid'] = $draftitemid;
3641  
3642          $mform = new \core_calendar\local\event\forms\update(
3643              null,
3644              $formoptions,
3645              'post',
3646              '',
3647              null,
3648              true,
3649              $data
3650          );
3651          $mform->set_data($data);
3652  
3653          // Check to see if this event is part of a subscription or import.
3654          // If so display a warning on edit.
3655          if (isset($event->subscriptionid) && ($event->subscriptionid != null)) {
3656              $renderable = new \core\output\notification(
3657                  get_string('eventsubscriptioneditwarning', 'calendar'),
3658                  \core\output\notification::NOTIFY_INFO
3659              );
3660  
3661              $html .= $OUTPUT->render($renderable);
3662          }
3663      }
3664  
3665      if ($hasformdata) {
3666          $mform->is_validated();
3667      }
3668  
3669      $html .= $mform->render();
3670      return $html;
3671  }
3672  
3673  /**
3674   * Calculate the timestamp from the supplied Gregorian Year, Month, and Day.
3675   *
3676   * @param   int     $d The day
3677   * @param   int     $m The month
3678   * @param   int     $y The year
3679   * @param   int     $time The timestamp to use instead of a separate y/m/d.
3680   * @return  int     The timestamp
3681   */
3682  function calendar_get_timestamp($d, $m, $y, $time = 0) {
3683      // If a day, month and year were passed then convert it to a timestamp. If these were passed
3684      // then we can assume the day, month and year are passed as Gregorian, as no where in core
3685      // should we be passing these values rather than the time.
3686      if (!empty($d) && !empty($m) && !empty($y)) {
3687          if (checkdate($m, $d, $y)) {
3688              $time = make_timestamp($y, $m, $d);
3689          } else {
3690              $time = time();
3691          }
3692      } else if (empty($time)) {
3693          $time = time();
3694      }
3695  
3696      return $time;
3697  }
3698  
3699  /**
3700   * Get the calendar footer options.
3701   *
3702   * @param calendar_information $calendar The calendar information object.
3703   * @param array $options Display options for the footer. If an option is not set, a default value will be provided.
3704   *                      It consists of:
3705   *                      - showfullcalendarlink - bool - Whether to show the full calendar link or not. Defaults to false.
3706   *
3707   * @return array The data for template and template name.
3708   */
3709  function calendar_get_footer_options($calendar, array $options = []) {
3710      global $CFG, $USER, $PAGE;
3711  
3712      // Generate hash for iCal link.
3713      $authtoken = calendar_get_export_token($USER);
3714  
3715      $renderer = $PAGE->get_renderer('core_calendar');
3716      $footer = new \core_calendar\external\footer_options_exporter($calendar, $USER->id, $authtoken, $options);
3717      $data = $footer->export($renderer);
3718      $template = 'core_calendar/footer_options';
3719  
3720      return [$data, $template];
3721  }
3722  
3723  /**
3724   * Get the list of potential calendar filter types as a type => name
3725   * combination.
3726   *
3727   * @return array
3728   */
3729  function calendar_get_filter_types() {
3730      $types = [
3731          'site',
3732          'category',
3733          'course',
3734          'group',
3735          'user',
3736          'other'
3737      ];
3738  
3739      return array_map(function($type) {
3740          return [
3741              'eventtype' => $type,
3742              'name' => get_string("eventtype{$type}", "calendar"),
3743              'icon' => true,
3744              'key' => 'i/' . $type . 'event',
3745              'component' => 'core'
3746          ];
3747      }, $types);
3748  }
3749  
3750  /**
3751   * Check whether the specified event type is valid.
3752   *
3753   * @param string $type
3754   * @return bool
3755   */
3756  function calendar_is_valid_eventtype($type) {
3757      $validtypes = [
3758          'user',
3759          'group',
3760          'course',
3761          'category',
3762          'site',
3763      ];
3764      return in_array($type, $validtypes);
3765  }
3766  
3767  /**
3768   * Get event types the user can create event based on categories, courses and groups
3769   * the logged in user belongs to.
3770   *
3771   * @param int|null $courseid The course id.
3772   * @return array The array of allowed types.
3773   */
3774  function calendar_get_allowed_event_types(int $courseid = null) {
3775      global $DB, $CFG, $USER;
3776  
3777      $types = [
3778          'user' => false,
3779          'site' => false,
3780          'course' => false,
3781          'group' => false,
3782          'category' => false
3783      ];
3784  
3785      if (!empty($courseid) && $courseid != SITEID) {
3786          $context = \context_course::instance($courseid);
3787          $types['user'] = has_capability('moodle/calendar:manageownentries', $context);
3788          calendar_internal_update_course_and_group_permission($courseid, $context, $types);
3789      }
3790  
3791      if (has_capability('moodle/calendar:manageentries', \context_course::instance(SITEID))) {
3792          $types['site'] = true;
3793      }
3794  
3795      if (has_capability('moodle/calendar:manageownentries', \context_system::instance())) {
3796          $types['user'] = true;
3797      }
3798      if (core_course_category::has_manage_capability_on_any()) {
3799          $types['category'] = true;
3800      }
3801  
3802      // We still don't know if the user can create group and course events, so iterate over the courses to find out
3803      // if the user has capabilities in one of the courses.
3804      if ($types['course'] == false || $types['group'] == false) {
3805          if ($CFG->calendar_adminseesall && has_capability('moodle/calendar:manageentries', context_system::instance())) {
3806              $sql = "SELECT c.id, " . context_helper::get_preload_record_columns_sql('ctx') . "
3807                        FROM {course} c
3808                        JOIN {context} ctx ON ctx.contextlevel = ? AND ctx.instanceid = c.id
3809                       WHERE c.id IN (
3810                              SELECT DISTINCT courseid FROM {groups}
3811                          )";
3812              $courseswithgroups = $DB->get_recordset_sql($sql, [CONTEXT_COURSE]);
3813              foreach ($courseswithgroups as $course) {
3814                  context_helper::preload_from_record($course);
3815                  $context = context_course::instance($course->id);
3816  
3817                  if (has_capability('moodle/calendar:manageentries', $context)) {
3818                      if (has_any_capability(['moodle/site:accessallgroups', 'moodle/calendar:managegroupentries'], $context)) {
3819                          // The user can manage group entries or access any group.
3820                          $types['group'] = true;
3821                          $types['course'] = true;
3822                          break;
3823                      }
3824                  }
3825              }
3826              $courseswithgroups->close();
3827  
3828              if (false === $types['course']) {
3829                  // Course is still not confirmed. There may have been no courses with a group in them.
3830                  $ctxfields = context_helper::get_preload_record_columns_sql('ctx');
3831                  $sql = "SELECT
3832                              c.id, c.visible, {$ctxfields}
3833                          FROM {course} c
3834                          JOIN {context} ctx ON (ctx.instanceid = c.id AND ctx.contextlevel = :contextlevel)";
3835                  $params = [
3836                      'contextlevel' => CONTEXT_COURSE,
3837                  ];
3838                  $courses = $DB->get_recordset_sql($sql, $params);
3839                  foreach ($courses as $course) {
3840                      context_helper::preload_from_record($course);
3841                      $context = context_course::instance($course->id);
3842                      if (has_capability('moodle/calendar:manageentries', $context)) {
3843                          $types['course'] = true;
3844                          break;
3845                      }
3846                  }
3847                  $courses->close();
3848              }
3849  
3850          } else {
3851              $courses = calendar_get_default_courses(null, 'id');
3852              if (empty($courses)) {
3853                  return $types;
3854              }
3855  
3856              $courseids = array_map(function($c) {
3857                  return $c->id;
3858              }, $courses);
3859  
3860              // Check whether the user has access to create events within courses which have groups.
3861              list($insql, $params) = $DB->get_in_or_equal($courseids, SQL_PARAMS_NAMED);
3862              $sql = "SELECT c.id, " . context_helper::get_preload_record_columns_sql('ctx') . "
3863                        FROM {course} c
3864                        JOIN {context} ctx ON ctx.contextlevel = :contextlevel AND ctx.instanceid = c.id
3865                       WHERE c.id $insql
3866                         AND c.id IN (SELECT DISTINCT courseid FROM {groups})";
3867              $params['contextlevel'] = CONTEXT_COURSE;
3868              $courseswithgroups = $DB->get_recordset_sql($sql, $params);
3869              foreach ($courseswithgroups as $coursewithgroup) {
3870                  context_helper::preload_from_record($coursewithgroup);
3871                  $context = context_course::instance($coursewithgroup->id);
3872  
3873                  calendar_internal_update_course_and_group_permission($coursewithgroup->id, $context, $types);
3874  
3875                  // Okay, course and group event types are allowed, no need to keep the loop iteration.
3876                  if ($types['course'] == true && $types['group'] == true) {
3877                      break;
3878                  }
3879              }
3880              $courseswithgroups->close();
3881  
3882              if (false === $types['course']) {
3883                  list($insql, $params) = $DB->get_in_or_equal($courseids, SQL_PARAMS_NAMED);
3884                  $contextsql = "SELECT c.id, " . context_helper::get_preload_record_columns_sql('ctx') . "
3885                                  FROM {course} c
3886                                  JOIN {context} ctx ON ctx.contextlevel = :contextlevel AND ctx.instanceid = c.id
3887                                  WHERE c.id $insql";
3888                  $params['contextlevel'] = CONTEXT_COURSE;
3889                  $contextrecords = $DB->get_recordset_sql($contextsql, $params);
3890                  foreach ($contextrecords as $course) {
3891                      context_helper::preload_from_record($course);
3892                      $coursecontext = context_course::instance($course->id);
3893                      if (has_capability('moodle/calendar:manageentries', $coursecontext)
3894                              && ($courseid == $course->id || empty($courseid))) {
3895                          $types['course'] = true;
3896                          break;
3897                      }
3898                  }
3899                  $contextrecords->close();
3900              }
3901  
3902          }
3903      }
3904  
3905      return $types;
3906  }
3907  
3908  /**
3909   * Given a course id, and context, updates the permission types array to add the 'course' or 'group'
3910   * permission if it is relevant for that course.
3911   *
3912   * For efficiency, if they already have 'course' or 'group' then it skips checks.
3913   *
3914   * Do not call this function directly, it is only for use by calendar_get_allowed_event_types().
3915   *
3916   * @param int $courseid Course id
3917   * @param context $context Context for that course
3918   * @param array $types Current permissions
3919   */
3920  function calendar_internal_update_course_and_group_permission(int $courseid, context $context, array &$types) {
3921      if (!$types['course']) {
3922          // If they have manageentries permission on the course, then they can update this course.
3923          if (has_capability('moodle/calendar:manageentries', $context)) {
3924              $types['course'] = true;
3925          }
3926      }
3927      // To update group events they must have EITHER manageentries OR managegroupentries.
3928      if (!$types['group'] && (has_capability('moodle/calendar:manageentries', $context) ||
3929              has_capability('moodle/calendar:managegroupentries', $context))) {
3930          // And they also need for a group to exist on the course.
3931          $groups = groups_get_all_groups($courseid);
3932          if (!empty($groups)) {
3933              // And either accessallgroups, or belong to one of the groups.
3934              if (has_capability('moodle/site:accessallgroups', $context)) {
3935                  $types['group'] = true;
3936              } else {
3937                  foreach ($groups as $group) {
3938                      if (groups_is_member($group->id)) {
3939                          $types['group'] = true;
3940                          break;
3941                      }
3942                  }
3943              }
3944          }
3945      }
3946  }
3947  
3948  /**
3949   * Get the auth token for exporting the given user calendar.
3950   * @param stdClass $user The user to export the calendar for
3951   *
3952   * @return string The export token.
3953   */
3954  function calendar_get_export_token(stdClass $user): string {
3955      global $CFG, $DB;
3956  
3957      return sha1($user->id . $DB->get_field('user', 'password', ['id' => $user->id]) . $CFG->calendar_exportsalt);
3958  }
3959  
3960  /**
3961   * Get the list of URL parameters for calendar expport and import links.
3962   *
3963   * @return array
3964   */
3965  function calendar_get_export_import_link_params(): array {
3966      global $PAGE;
3967  
3968      $params = [];
3969      if ($courseid = $PAGE->url->get_param('course')) {
3970          $params['course'] = $courseid;
3971      }
3972      if ($categoryid = $PAGE->url->get_param('category')) {
3973          $params['category'] = $categoryid;
3974      }
3975  
3976      return $params;
3977  }
3978  
3979  /**
3980   * Implements the inplace editable feature.
3981   *
3982   * @param string $itemtype Type of the inplace editable element
3983   * @param int $itemid Id of the item to edit
3984   * @param int $newvalue New value of the item
3985   * @return \core\output\inplace_editable
3986   */
3987  function calendar_inplace_editable(string $itemtype, int $itemid, int $newvalue): \core\output\inplace_editable {
3988      global $OUTPUT;
3989  
3990      if ($itemtype === 'refreshinterval') {
3991  
3992          $subscription = calendar_get_subscription($itemid);
3993          $context = calendar_get_calendar_context($subscription);
3994          \external_api::validate_context($context);
3995  
3996          $updateresult = \core_calendar\output\refreshintervalcollection::update($itemid, $newvalue);
3997  
3998          $refreshresults = calendar_update_subscription_events($itemid);
3999          \core\notification::add($OUTPUT->render_from_template(
4000              'core_calendar/subscription_update_result',
4001              array_merge($refreshresults, [
4002                  'subscriptionname' => s($subscription->name),
4003              ])
4004          ), \core\notification::INFO);
4005  
4006          return $updateresult;
4007      }
4008  
4009      \external_api::validate_context(context_system::instance());
4010  }