Search moodle.org's
Developer Documentation

See Release Notes
Long Term Support Release

  • Bug fixes for general core bugs in 3.9.x will end* 10 May 2021 (12 months).
  • Bug fixes for security issues in 3.9.x will end* 8 May 2023 (36 months).
  • PHP version: minimum PHP 7.2.0 Note: minimum PHP version has increased since Moodle 3.8. PHP 7.3.x and 7.4.x are supported too.
/calendar/ -> lib.php (source)

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