Search moodle.org's
Developer Documentation

  • Bug fixes for general core bugs in 3.7.x will end 11 May 2020 (12 months).
  • Bug fixes for security issues in 3.7.x will end 9 November 2020 (18 months) - Support has ended.
  • minimum PHP 7.1.0 Note: minimum PHP version has increased since Moodle 3.6. PHP 7.2.x and 7.3.x are supported too. PHP 7.x could have some engine limitations.
  • /calendar/ -> lib.php (source)

    Differences Between: [Versions 35 and 37] [Versions 36 and 37] [Versions 37 and 310] [Versions 37 and 311] [Versions 37 and 38] [Versions 37 and 39]

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