moodle/calendar/classes/event.php
2017-04-03 11:35:38 +08:00

784 lines
32 KiB
PHP

<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Contains the class for the calendar events.
*
* @package core_calendar
* @copyright 2016 Mark Nelson <markn@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core_calendar;
defined('MOODLE_INTERNAL') || die();
require_once($CFG->dirroot . '/calendar/lib.php');
/**
* The class for the calendar events.
*
* @package core_calendar
* @copyright 2016 Mark Nelson <markn@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class event {
/** @var array An object containing the event properties can be accessed via the magic __get/set methods */
protected $properties = null;
/** @var string The converted event discription with file paths resolved.
* This gets populated when someone requests description for the first time */
protected $_description = null;
/** @var array The options to use with this description editor */
protected $editoroptions = array(
'subdirs' => false,
'forcehttps' => false,
'maxfiles' => -1,
'maxbytes' => null,
'trusttext' => false);
/** @var object The context to use with the description editor */
protected $editorcontext = null;
/**
* Instantiates a new event and optionally populates its properties with the data provided.
*
* @param \stdClass $data Optional. An object containing the properties to for
* an event
*/
public function __construct($data = null) {
global $CFG, $USER;
// First convert to object if it is not already (should either be object or assoc array).
if (!is_object($data)) {
$data = (object) $data;
}
$this->editoroptions['maxbytes'] = $CFG->maxbytes;
$data->eventrepeats = 0;
if (empty($data->id)) {
$data->id = null;
}
if (!empty($data->subscriptionid)) {
$data->subscription = api::get_subscription($data->subscriptionid);
}
// Default to a user event.
if (empty($data->eventtype)) {
$data->eventtype = 'user';
}
// Default to the current user.
if (empty($data->userid)) {
$data->userid = $USER->id;
}
if (!empty($data->timeduration) && is_array($data->timeduration)) {
$data->timeduration = make_timestamp(
$data->timeduration['year'], $data->timeduration['month'], $data->timeduration['day'],
$data->timeduration['hour'], $data->timeduration['minute']) - $data->timestart;
}
if (!empty($data->description) && is_array($data->description)) {
$data->format = $data->description['format'];
$data->description = $data->description['text'];
} else if (empty($data->description)) {
$data->description = '';
$data->format = editors_get_preferred_format();
}
// Ensure form is defaulted correctly.
if (empty($data->format)) {
$data->format = editors_get_preferred_format();
}
$this->properties = $data;
if (empty($data->context)) {
$this->properties->context = $this->calculate_context();
}
}
/**
* Magic set method.
*
* Attempts to call a set_$key method if one exists otherwise falls back
* to simply set the property.
*
* @param string $key property name
* @param mixed $value value of the property
*/
public function __set($key, $value) {
if (method_exists($this, 'set_'.$key)) {
$this->{'set_'.$key}($value);
}
$this->properties->{$key} = $value;
}
/**
* Magic get method.
*
* Attempts to call a get_$key method to return the property and ralls over
* to return the raw property.
*
* @param string $key property name
* @return mixed property value
* @throws \coding_exception
*/
public function __get($key) {
if (method_exists($this, 'get_'.$key)) {
return $this->{'get_'.$key}();
}
if (!isset($this->properties->{$key})) {
throw new \coding_exception('Undefined property requested');
}
return $this->properties->{$key};
}
/**
* Magic isset method.
*
* PHP needs an isset magic method if you use the get magic method and
* still want empty calls to work.
*
* @param string $key $key property name
* @return bool|mixed property value, false if property is not exist
*/
public function __isset($key) {
return !empty($this->properties->{$key});
}
/**
* Calculate the context value needed for an event.
*
* Event's type can be determine by the available value store in $data
* It is important to check for the existence of course/courseid to determine
* the course event.
* Default value is set to CONTEXT_USER
*
* @return \stdClass The context object.
*/
protected function calculate_context() {
global $USER, $DB;
$context = null;
if (isset($this->properties->courseid) && $this->properties->courseid > 0) {
$context = \context_course::instance($this->properties->courseid);
} else if (isset($this->properties->course) && $this->properties->course > 0) {
$context = \context_course::instance($this->properties->course);
} else if (isset($this->properties->groupid) && $this->properties->groupid > 0) {
$group = $DB->get_record('groups', array('id' => $this->properties->groupid));
$context = \context_course::instance($group->courseid);
} else if (isset($this->properties->userid) && $this->properties->userid > 0
&& $this->properties->userid == $USER->id) {
$context = \context_user::instance($this->properties->userid);
} else if (isset($this->properties->userid) && $this->properties->userid > 0
&& $this->properties->userid != $USER->id &&
isset($this->properties->instance) && $this->properties->instance > 0) {
$cm = get_coursemodule_from_instance($this->properties->modulename, $this->properties->instance, 0,
false, MUST_EXIST);
$context = \context_course::instance($cm->course);
} else {
$context = \context_user::instance($this->properties->userid);
}
return $context;
}
/**
* Returns an array of editoroptions for this event.
*
* @return array event editor options
*/
protected function get_editoroptions() {
return $this->editoroptions;
}
/**
* Returns an event description: Called by __get
* Please use $blah = $event->description;
*
* @return string event description
*/
protected function get_description() {
global $CFG;
require_once($CFG->libdir . '/filelib.php');
if ($this->_description === null) {
// Check if we have already resolved the context for this event.
if ($this->editorcontext === null) {
// Switch on the event type to decide upon the appropriate context to use for this event.
$this->editorcontext = $this->properties->context;
if ($this->properties->eventtype != 'user' && $this->properties->eventtype != 'course'
&& $this->properties->eventtype != 'site' && $this->properties->eventtype != 'group') {
return clean_text($this->properties->description, $this->properties->format);
}
}
// Work out the item id for the editor, if this is a repeated event
// then the files will be associated with the original.
if (!empty($this->properties->repeatid) && $this->properties->repeatid > 0) {
$itemid = $this->properties->repeatid;
} else {
$itemid = $this->properties->id;
}
// Convert file paths in the description so that things display correctly.
$this->_description = file_rewrite_pluginfile_urls($this->properties->description, 'pluginfile.php',
$this->editorcontext->id, 'calendar', 'event_description', $itemid);
// Clean the text so no nasties get through.
$this->_description = clean_text($this->_description, $this->properties->format);
}
// Finally return the description.
return $this->_description;
}
/**
* Return the number of repeat events there are in this events series.
*
* @return int number of event repeated
*/
public function count_repeats() {
global $DB;
if (!empty($this->properties->repeatid)) {
$this->properties->eventrepeats = $DB->count_records('event',
array('repeatid' => $this->properties->repeatid));
// We don't want to count ourselves.
$this->properties->eventrepeats--;
}
return $this->properties->eventrepeats;
}
/**
* Update or create an event within the database
*
* Pass in a object containing the event properties and this function will
* insert it into the database and deal with any associated files
*
* @see self::create()
* @see self::update()
*
* @param \stdClass $data object of event
* @param bool $checkcapability if moodle should check calendar managing capability or not
* @return bool event updated
*/
public function update($data, $checkcapability=true) {
global $DB, $USER;
foreach ($data as $key => $value) {
$this->properties->$key = $value;
}
$this->properties->timemodified = time();
$usingeditor = (!empty($this->properties->description) && is_array($this->properties->description));
// Prepare event data.
$eventargs = array(
'context' => $this->properties->context,
'objectid' => $this->properties->id,
'other' => array(
'repeatid' => empty($this->properties->repeatid) ? 0 : $this->properties->repeatid,
'timestart' => $this->properties->timestart,
'name' => $this->properties->name
)
);
if (empty($this->properties->id) || $this->properties->id < 1) {
if ($checkcapability) {
if (!\core_calendar\api::can_add_event($this->properties)) {
print_error('nopermissiontoupdatecalendar');
}
}
if ($usingeditor) {
switch ($this->properties->eventtype) {
case 'user':
$this->properties->courseid = 0;
$this->properties->course = 0;
$this->properties->groupid = 0;
$this->properties->userid = $USER->id;
break;
case 'site':
$this->properties->courseid = SITEID;
$this->properties->course = SITEID;
$this->properties->groupid = 0;
$this->properties->userid = $USER->id;
break;
case 'course':
$this->properties->groupid = 0;
$this->properties->userid = $USER->id;
break;
case 'group':
$this->properties->userid = $USER->id;
break;
default:
// We should NEVER get here, but just incase we do lets fail gracefully.
$usingeditor = false;
break;
}
// If we are actually using the editor, we recalculate the context because some default values
// were set when calculate_context() was called from the constructor.
if ($usingeditor) {
$this->properties->context = $this->calculate_context();
$this->editorcontext = $this->properties->context;
}
$editor = $this->properties->description;
$this->properties->format = $this->properties->description['format'];
$this->properties->description = $this->properties->description['text'];
}
// Insert the event into the database.
$this->properties->id = $DB->insert_record('event', $this->properties);
if ($usingeditor) {
$this->properties->description = file_save_draft_area_files(
$editor['itemid'],
$this->editorcontext->id,
'calendar',
'event_description',
$this->properties->id,
$this->editoroptions,
$editor['text'],
$this->editoroptions['forcehttps']);
$DB->set_field('event', 'description', $this->properties->description,
array('id' => $this->properties->id));
}
// Log the event entry.
$eventargs['objectid'] = $this->properties->id;
$eventargs['context'] = $this->properties->context;
$event = \core\event\calendar_event_created::create($eventargs);
$event->trigger();
$repeatedids = array();
if (!empty($this->properties->repeat)) {
$this->properties->repeatid = $this->properties->id;
$DB->set_field('event', 'repeatid', $this->properties->repeatid, array('id' => $this->properties->id));
$eventcopy = clone($this->properties);
unset($eventcopy->id);
$timestart = new \DateTime('@' . $eventcopy->timestart);
$timestart->setTimezone(\core_date::get_user_timezone_object());
for ($i = 1; $i < $eventcopy->repeats; $i++) {
$timestart->add(new \DateInterval('P7D'));
$eventcopy->timestart = $timestart->getTimestamp();
// Get the event id for the log record.
$eventcopyid = $DB->insert_record('event', $eventcopy);
// If the context has been set delete all associated files.
if ($usingeditor) {
$fs = get_file_storage();
$files = $fs->get_area_files($this->editorcontext->id, 'calendar', 'event_description',
$this->properties->id);
foreach ($files as $file) {
$fs->create_file_from_storedfile(array('itemid' => $eventcopyid), $file);
}
}
$repeatedids[] = $eventcopyid;
// Trigger an event.
$eventargs['objectid'] = $eventcopyid;
$eventargs['other']['timestart'] = $eventcopy->timestart;
$event = \core\event\calendar_event_created::create($eventargs);
$event->trigger();
}
}
return true;
} else {
if ($checkcapability) {
if (!api::can_edit_event($this->properties)) {
print_error('nopermissiontoupdatecalendar');
}
}
if ($usingeditor) {
if ($this->editorcontext !== null) {
$this->properties->description = file_save_draft_area_files(
$this->properties->description['itemid'],
$this->editorcontext->id,
'calendar',
'event_description',
$this->properties->id,
$this->editoroptions,
$this->properties->description['text'],
$this->editoroptions['forcehttps']);
} else {
$this->properties->format = $this->properties->description['format'];
$this->properties->description = $this->properties->description['text'];
}
}
$event = $DB->get_record('event', array('id' => $this->properties->id));
$updaterepeated = (!empty($this->properties->repeatid) && !empty($this->properties->repeateditall));
if ($updaterepeated) {
// Update all.
if ($this->properties->timestart != $event->timestart) {
$timestartoffset = $this->properties->timestart - $event->timestart;
$sql = "UPDATE {event}
SET name = ?,
description = ?,
timestart = timestart + ?,
timeduration = ?,
timemodified = ?
WHERE repeatid = ?";
$params = array($this->properties->name, $this->properties->description, $timestartoffset,
$this->properties->timeduration, time(), $event->repeatid);
} else {
$sql = "UPDATE {event} SET name = ?, description = ?, timeduration = ?, timemodified = ? WHERE repeatid = ?";
$params = array($this->properties->name, $this->properties->description,
$this->properties->timeduration, time(), $event->repeatid);
}
$DB->execute($sql, $params);
// Trigger an update event for each of the calendar event.
$events = $DB->get_records('event', array('repeatid' => $event->repeatid), '', '*');
foreach ($events as $calendarevent) {
$eventargs['objectid'] = $calendarevent->id;
$eventargs['other']['timestart'] = $calendarevent->timestart;
$event = \core\event\calendar_event_updated::create($eventargs);
$event->add_record_snapshot('event', $calendarevent);
$event->trigger();
}
} else {
$DB->update_record('event', $this->properties);
$event = self::load($this->properties->id);
$this->properties = $event->properties();
// Trigger an update event.
$event = \core\event\calendar_event_updated::create($eventargs);
$event->add_record_snapshot('event', $this->properties);
$event->trigger();
}
return true;
}
}
/**
* Deletes an event and if selected an repeated events in the same series
*
* This function deletes an event, any associated events if $deleterepeated=true,
* and cleans up any files associated with the events.
*
* @see self::delete()
*
* @param bool $deleterepeated delete event repeatedly
* @return bool succession of deleting event
*/
public function delete($deleterepeated = false) {
global $DB;
// If $this->properties->id is not set then something is wrong.
if (empty($this->properties->id)) {
debugging('Attempting to delete an event before it has been loaded', DEBUG_DEVELOPER);
return false;
}
$calevent = $DB->get_record('event', array('id' => $this->properties->id), '*', MUST_EXIST);
// Delete the event.
$DB->delete_records('event', array('id' => $this->properties->id));
// Trigger an event for the delete action.
$eventargs = array(
'context' => $this->properties->context,
'objectid' => $this->properties->id,
'other' => array(
'repeatid' => empty($this->properties->repeatid) ? 0 : $this->properties->repeatid,
'timestart' => $this->properties->timestart,
'name' => $this->properties->name
));
$event = \core\event\calendar_event_deleted::create($eventargs);
$event->add_record_snapshot('event', $calevent);
$event->trigger();
// If we are deleting parent of a repeated event series, promote the next event in the series as parent.
if (($this->properties->id == $this->properties->repeatid) && !$deleterepeated) {
$newparent = $DB->get_field_sql("SELECT id from {event} where repeatid = ? order by id ASC",
array($this->properties->id), IGNORE_MULTIPLE);
if (!empty($newparent)) {
$DB->execute("UPDATE {event} SET repeatid = ? WHERE repeatid = ?",
array($newparent, $this->properties->id));
// Get all records where the repeatid is the same as the event being removed.
$events = $DB->get_records('event', array('repeatid' => $newparent));
// For each of the returned events trigger an update event.
foreach ($events as $calendarevent) {
// Trigger an event for the update.
$eventargs['objectid'] = $calendarevent->id;
$eventargs['other']['timestart'] = $calendarevent->timestart;
$event = \core\event\calendar_event_updated::create($eventargs);
$event->add_record_snapshot('event', $calendarevent);
$event->trigger();
}
}
}
// If the editor context hasn't already been set then set it now.
if ($this->editorcontext === null) {
$this->editorcontext = $this->properties->context;
}
// If the context has been set delete all associated files.
if ($this->editorcontext !== null) {
$fs = get_file_storage();
$files = $fs->get_area_files($this->editorcontext->id, 'calendar', 'event_description', $this->properties->id);
foreach ($files as $file) {
$file->delete();
}
}
// If we need to delete repeated events then we will fetch them all and delete one by one.
if ($deleterepeated && !empty($this->properties->repeatid) && $this->properties->repeatid > 0) {
// Get all records where the repeatid is the same as the event being removed.
$events = $DB->get_records('event', array('repeatid' => $this->properties->repeatid));
// For each of the returned events populate an event object and call delete.
// make sure the arg passed is false as we are already deleting all repeats.
foreach ($events as $event) {
$event = new event($event);
$event->delete(false);
}
}
return true;
}
/**
* Fetch all event properties.
*
* This function returns all of the events properties as an object and optionally
* can prepare an editor for the description field at the same time. This is
* designed to work when the properties are going to be used to set the default
* values of a moodle forms form.
*
* @param bool $prepareeditor If set to true a editor is prepared for use with
* the mforms editor element. (for description)
* @return \stdClass Object containing event properties
*/
public function properties($prepareeditor = false) {
global $DB;
// First take a copy of the properties. We don't want to actually change the
// properties or we'd forever be converting back and forwards between an
// editor formatted description and not.
$properties = clone($this->properties);
// Clean the description here.
$properties->description = clean_text($properties->description, $properties->format);
// If set to true we need to prepare the properties for use with an editor
// and prepare the file area.
if ($prepareeditor) {
// We may or may not have a property id. If we do then we need to work
// out the context so we can copy the existing files to the draft area.
if (!empty($properties->id)) {
if ($properties->eventtype === 'site') {
// Site context.
$this->editorcontext = $this->properties->context;
} else if ($properties->eventtype === 'user') {
// User context.
$this->editorcontext = $this->properties->context;
} else if ($properties->eventtype === 'group' || $properties->eventtype === 'course') {
// First check the course is valid.
$course = $DB->get_record('course', array('id' => $properties->courseid));
if (!$course) {
print_error('invalidcourse');
}
// Course context.
$this->editorcontext = $this->properties->context;
// We have a course and are within the course context so we had
// better use the courses max bytes value.
$this->editoroptions['maxbytes'] = $course->maxbytes;
} else {
// If we get here we have a custom event type as used by some
// modules. In this case the event will have been added by
// code and we won't need the editor.
$this->editoroptions['maxbytes'] = 0;
$this->editoroptions['maxfiles'] = 0;
}
if (empty($this->editorcontext) || empty($this->editorcontext->id)) {
$contextid = false;
} else {
// Get the context id that is what we really want.
$contextid = $this->editorcontext->id;
}
} else {
// If we get here then this is a new event in which case we don't need a
// context as there is no existing files to copy to the draft area.
$contextid = null;
}
// If the contextid === false we don't support files so no preparing
// a draft area.
if ($contextid !== false) {
// Just encase it has already been submitted.
$draftiddescription = file_get_submitted_draft_itemid('description');
// Prepare the draft area, this copies existing files to the draft area as well.
$properties->description = file_prepare_draft_area($draftiddescription, $contextid, 'calendar',
'event_description', $properties->id, $this->editoroptions, $properties->description);
} else {
$draftiddescription = 0;
}
// Structure the description field as the editor requires.
$properties->description = array('text' => $properties->description, 'format' => $properties->format,
'itemid' => $draftiddescription);
}
// Finally return the properties.
return $properties;
}
/**
* Toggles the visibility of an event
*
* @param null|bool $force If it is left null the events visibility is flipped,
* If it is false the event is made hidden, if it is true it
* is made visible.
* @return bool if event is successfully updated, toggle will be visible
*/
public function toggle_visibility($force = null) {
global $DB;
// Set visible to the default if it is not already set.
if (empty($this->properties->visible)) {
$this->properties->visible = 1;
}
if ($force === true || ($force !== false && $this->properties->visible == 0)) {
// Make this event visible.
$this->properties->visible = 1;
} else {
// Make this event hidden.
$this->properties->visible = 0;
}
// Update the database to reflect this change.
$success = $DB->set_field('event', 'visible', $this->properties->visible, array('id' => $this->properties->id));
$calendarevent = $DB->get_record('event', array('id' => $this->properties->id), '*', MUST_EXIST);
// Prepare event data.
$eventargs = array(
'context' => $this->properties->context,
'objectid' => $this->properties->id,
'other' => array(
'repeatid' => empty($this->properties->repeatid) ? 0 : $this->properties->repeatid,
'timestart' => $this->properties->timestart,
'name' => $this->properties->name
)
);
$event = \core\event\calendar_event_updated::create($eventargs);
$event->add_record_snapshot('event', $calendarevent);
$event->trigger();
return $success;
}
/**
* Returns an event object when provided with an event id.
*
* This function makes use of MUST_EXIST, if the event id passed in is invalid
* it will result in an exception being thrown.
*
* @param int|object $param event object or event id
* @return event
*/
public static function load($param) {
global $DB;
if (is_object($param)) {
$event = new event($param);
} else {
$event = $DB->get_record('event', array('id' => (int)$param), '*', MUST_EXIST);
$event = new event($event);
}
return $event;
}
/**
* Creates a new event and returns an event object
*
* @param \stdClass|array $properties An object containing event properties
* @param bool $checkcapability Check caps or not
* @throws \coding_exception
*
* @return event|bool The event object or false if it failed
*/
public static function create($properties, $checkcapability = true) {
if (is_array($properties)) {
$properties = (object)$properties;
}
if (!is_object($properties)) {
throw new \coding_exception('When creating an event properties should be either an object or an assoc array');
}
$event = new event($properties);
if ($event->update($properties, $checkcapability)) {
return $event;
} else {
return false;
}
}
/**
* Format the text using the external API.
*
* This function should we used when text formatting is required in external functions.
*
* @return array an array containing the text formatted and the text format
*/
public function format_external_text() {
if ($this->editorcontext === null) {
// Switch on the event type to decide upon the appropriate context to use for this event.
$this->editorcontext = $this->properties->context;
if ($this->properties->eventtype != 'user' && $this->properties->eventtype != 'course'
&& $this->properties->eventtype != 'site' && $this->properties->eventtype != 'group') {
// We don't have a context here, do a normal format_text.
return external_format_text($this->properties->description, $this->properties->format, $this->editorcontext->id);
}
}
// Work out the item id for the editor, if this is a repeated event then the files will be associated with the original.
if (!empty($this->properties->repeatid) && $this->properties->repeatid > 0) {
$itemid = $this->properties->repeatid;
} else {
$itemid = $this->properties->id;
}
return external_format_text($this->properties->description, $this->properties->format, $this->editorcontext->id,
'calendar', 'event_description', $itemid);
}
}