" . get_string('subscriptionupdated', 'calendar', $sub->name) . "
" . calendar_update_subscription_events($subscriptionid); case CALENDAR_SUBSCRIPTION_REMOVE: calendar_delete_subscription($subscriptionid); return get_string('subscriptionremoved', 'calendar', $sub->name); break; default: break; } return ''; } /** * Delete subscription and all related events. * * @param int|stdClass $subscription subscription or it's id, which needs to be deleted. */ function calendar_delete_subscription($subscription) { global $DB; if (!is_object($subscription)) { $subscription = $DB->get_record('event_subscriptions', array('id' => $subscription), '*', MUST_EXIST); } // Delete subscription and related events. $DB->delete_records('event', array('subscriptionid' => $subscription->id)); $DB->delete_records('event_subscriptions', array('id' => $subscription->id)); \cache_helper::invalidate_by_definition('core', 'calendar_subscriptions', array(), array($subscription->id)); // Trigger event, calendar subscription deleted. $eventparams = array('objectid' => $subscription->id, 'context' => calendar_get_calendar_context($subscription), 'other' => array( 'eventtype' => $subscription->eventtype, ) ); switch ($subscription->eventtype) { case 'category': $eventparams['other']['categoryid'] = $subscription->categoryid; break; case 'course': $eventparams['other']['courseid'] = $subscription->courseid; break; case 'group': $eventparams['other']['courseid'] = $subscription->courseid; $eventparams['other']['groupid'] = $subscription->groupid; break; default: $eventparams['other']['courseid'] = $subscription->courseid; } $event = \core\event\calendar_subscription_deleted::create($eventparams); $event->trigger(); } /** * From a URL, fetch the calendar and return an iCalendar object. * * @param string $url The iCalendar URL * @return iCalendar The iCalendar object */ function calendar_get_icalendar($url) { global $CFG; require_once($CFG->libdir . '/filelib.php'); $curl = new \curl(); $curl->setopt(array('CURLOPT_FOLLOWLOCATION' => 1, 'CURLOPT_MAXREDIRS' => 5)); $calendar = $curl->get($url); // Http code validation should actually be the job of curl class. if (!$calendar || $curl->info['http_code'] != 200 || !empty($curl->errorno)) { throw new \moodle_exception('errorinvalidicalurl', 'calendar'); } $ical = new \iCalendar(); $ical->unserialize($calendar); return $ical; } /** * Import events from an iCalendar object into a course calendar. * * @param iCalendar $ical The iCalendar object. * @param int $courseid The course ID for the calendar. * @param int $subscriptionid The subscription ID. * @return string A log of the import progress, including errors. */ function calendar_import_icalendar_events($ical, $unused = null, $subscriptionid = null) { global $DB; $return = ''; $eventcount = 0; $updatecount = 0; // Large calendars take a while... if (!CLI_SCRIPT) { \core_php_time_limit::raise(300); } // Mark all events in a subscription with a zero timestamp. if (!empty($subscriptionid)) { $sql = "UPDATE {event} SET timemodified = :time WHERE subscriptionid = :id"; $DB->execute($sql, array('time' => 0, 'id' => $subscriptionid)); } // Grab the timezone from the iCalendar file to be used later. if (isset($ical->properties['X-WR-TIMEZONE'][0]->value)) { $timezone = $ical->properties['X-WR-TIMEZONE'][0]->value; } else { $timezone = 'UTC'; } $return = ''; foreach ($ical->components['VEVENT'] as $event) { $res = calendar_add_icalendar_event($event, null, $subscriptionid, $timezone); switch ($res) { case CALENDAR_IMPORT_EVENT_UPDATED: $updatecount++; break; case CALENDAR_IMPORT_EVENT_INSERTED: $eventcount++; break; case 0: $return .= '' . get_string('erroraddingevent', 'calendar') . ': '; if (empty($event->properties['SUMMARY'])) { $return .= '(' . get_string('notitle', 'calendar') . ')'; } else { $return .= $event->properties['SUMMARY'][0]->value; } $return .= "
\n"; break; } } $return .= "" . get_string('eventsimported', 'calendar', $eventcount) . "
"; $return .= "" . get_string('eventsupdated', 'calendar', $updatecount) . "
"; // Delete remaining zero-marked events since they're not in remote calendar. if (!empty($subscriptionid)) { $deletecount = $DB->count_records('event', array('timemodified' => 0, 'subscriptionid' => $subscriptionid)); if (!empty($deletecount)) { $DB->delete_records('event', array('timemodified' => 0, 'subscriptionid' => $subscriptionid)); $return .= "" . get_string('eventsdeleted', 'calendar') . ": {$deletecount}
\n"; } } return $return; } /** * Fetch a calendar subscription and update the events in the calendar. * * @param int $subscriptionid The course ID for the calendar. * @return string A log of the import progress, including errors. */ function calendar_update_subscription_events($subscriptionid) { $sub = calendar_get_subscription($subscriptionid); // Don't update a file subscription. if (empty($sub->url)) { return 'File subscription not updated.'; } $ical = calendar_get_icalendar($sub->url); $return = calendar_import_icalendar_events($ical, null, $subscriptionid); $sub->lastupdated = time(); calendar_update_subscription($sub); return $return; } /** * Update a calendar subscription. Also updates the associated cache. * * @param stdClass|array $subscription Subscription record. * @throws coding_exception If something goes wrong * @since Moodle 2.5 */ function calendar_update_subscription($subscription) { global $DB; if (is_array($subscription)) { $subscription = (object)$subscription; } if (empty($subscription->id) || !$DB->record_exists('event_subscriptions', array('id' => $subscription->id))) { throw new \coding_exception('Cannot update a subscription without a valid id'); } $DB->update_record('event_subscriptions', $subscription); // Update cache. $cache = \cache::make('core', 'calendar_subscriptions'); $cache->set($subscription->id, $subscription); // Trigger event, calendar subscription updated. $eventparams = array('userid' => $subscription->userid, 'objectid' => $subscription->id, 'context' => calendar_get_calendar_context($subscription), 'other' => array( 'eventtype' => $subscription->eventtype, ) ); switch ($subscription->eventtype) { case 'category': $eventparams['other']['categoryid'] = $subscription->categoryid; break; case 'course': $eventparams['other']['courseid'] = $subscription->courseid; break; case 'group': $eventparams['other']['courseid'] = $subscription->courseid; $eventparams['other']['groupid'] = $subscription->groupid; break; default: $eventparams['other']['courseid'] = $subscription->courseid; } $event = \core\event\calendar_subscription_updated::create($eventparams); $event->trigger(); } /** * Checks to see if the user can edit a given subscription feed. * * @param mixed $subscriptionorid Subscription object or id * @return bool true if current user can edit the subscription else false */ function calendar_can_edit_subscription($subscriptionorid) { if (is_array($subscriptionorid)) { $subscription = (object)$subscriptionorid; } else if (is_object($subscriptionorid)) { $subscription = $subscriptionorid; } else { $subscription = calendar_get_subscription($subscriptionorid); } $allowed = new \stdClass; $courseid = $subscription->courseid; $categoryid = $subscription->categoryid; $groupid = $subscription->groupid; $category = null; if (!empty($categoryid)) { $category = \coursecat::get($categoryid); } calendar_get_allowed_types($allowed, $courseid, null, $category); switch ($subscription->eventtype) { case 'user': return $allowed->user; case 'course': if (isset($allowed->courses[$courseid])) { return $allowed->courses[$courseid]; } else { return false; } case 'category': if (isset($allowed->categories[$categoryid])) { return $allowed->categories[$categoryid]; } else { return false; } case 'site': return $allowed->site; case 'group': if (isset($allowed->groups[$groupid])) { return $allowed->groups[$groupid]; } else { return false; } default: return false; } } /** * Helper function to determine the context of a calendar subscription. * Subscriptions can be created in two contexts COURSE, or USER. * * @param stdClass $subscription * @return context instance */ function calendar_get_calendar_context($subscription) { // Determine context based on calendar type. if ($subscription->eventtype === 'site') { $context = \context_course::instance(SITEID); } else if ($subscription->eventtype === 'group' || $subscription->eventtype === 'course') { $context = \context_course::instance($subscription->courseid); } else { $context = \context_user::instance($subscription->userid); } return $context; } /** * Implements callback user_preferences, whitelists preferences that users are allowed to update directly * * Used in {@see core_user::fill_preferences_cache()}, see also {@see useredit_update_user_preference()} * * @return array */ function core_calendar_user_preferences() { $preferences = []; $preferences['calendar_timeformat'] = array('type' => PARAM_NOTAGS, 'null' => NULL_NOT_ALLOWED, 'default' => '0', 'choices' => array('0', CALENDAR_TF_12, CALENDAR_TF_24) ); $preferences['calendar_startwday'] = array('type' => PARAM_INT, 'null' => NULL_NOT_ALLOWED, 'default' => 0, 'choices' => array(0, 1, 2, 3, 4, 5, 6)); $preferences['calendar_maxevents'] = array('type' => PARAM_INT, 'choices' => range(1, 20)); $preferences['calendar_lookahead'] = array('type' => PARAM_INT, 'null' => NULL_NOT_ALLOWED, 'default' => 365, 'choices' => array(365, 270, 180, 150, 120, 90, 60, 30, 21, 14, 7, 6, 5, 4, 3, 2, 1)); $preferences['calendar_persistflt'] = array('type' => PARAM_INT, 'null' => NULL_NOT_ALLOWED, 'default' => 0, 'choices' => array(0, 1)); return $preferences; } /** * Get legacy calendar events * * @param int $tstart Start time of time range for events * @param int $tend End time of time range for events * @param array|int|boolean $users array of users, user id or boolean for all/no user events * @param array|int|boolean $groups array of groups, group id or boolean for all/no group events * @param array|int|boolean $courses array of courses, course id or boolean for all/no course events * @param boolean $withduration whether only events starting within time range selected * or events in progress/already started selected as well * @param boolean $ignorehidden whether to select only visible events or all events * @param array $categories array of category ids and/or objects. * @return array $events of selected events or an empty array if there aren't any (or there was an error) */ function calendar_get_legacy_events($tstart, $tend, $users, $groups, $courses, $withduration = true, $ignorehidden = true, $categories = []) { // Normalise the users, groups and courses parameters so that they are compliant with \core_calendar\local\api::get_events(). // Existing functions that were using the old calendar_get_events() were passing a mixture of array, int, boolean for these // parameters, but with the new API method, only null and arrays are accepted. list($userparam, $groupparam, $courseparam, $categoryparam) = array_map(function($param) { // If parameter is true, return null. if ($param === true) { return null; } // If parameter is false, return an empty array. if ($param === false) { return []; } // If the parameter is a scalar value, enclose it in an array. if (!is_array($param)) { return [$param]; } // No normalisation required. return $param; }, [$users, $groups, $courses, $categories]); $mapper = \core_calendar\local\event\container::get_event_mapper(); $events = \core_calendar\local\api::get_events( $tstart, $tend, null, null, null, null, 40, null, $userparam, $groupparam, $courseparam, $categoryparam, $withduration, $ignorehidden ); return array_reduce($events, function($carry, $event) use ($mapper) { return $carry + [$event->get_id() => $mapper->from_event_to_stdclass($event)]; }, []); } /** * Get the calendar view output. * * @param \calendar_information $calendar The calendar being represented * @param string $view The type of calendar to have displayed * @param bool $includenavigation Whether to include navigation * @return array[array, string] */ function calendar_get_view(\calendar_information $calendar, $view, $includenavigation = true) { global $PAGE, $CFG; $renderer = $PAGE->get_renderer('core_calendar'); $type = \core_calendar\type_factory::get_calendar_instance(); // Calculate the bounds of the month. $calendardate = $type->timestamp_to_date_array($calendar->time); $date = new \DateTime('now', core_date::get_user_timezone_object(99)); $eventlimit = 200; if ($view === 'day') { $tstart = $type->convert_to_timestamp($calendardate['year'], $calendardate['mon'], $calendardate['mday']); $date->setTimestamp($tstart); $date->modify('+1 day'); } else if ($view === 'upcoming' || $view === 'upcoming_mini') { // Number of days in the future that will be used to fetch events. if (isset($CFG->calendar_lookahead)) { $defaultlookahead = intval($CFG->calendar_lookahead); } else { $defaultlookahead = CALENDAR_DEFAULT_UPCOMING_LOOKAHEAD; } $lookahead = get_user_preferences('calendar_lookahead', $defaultlookahead); // Maximum number of events to be displayed on upcoming view. $defaultmaxevents = CALENDAR_DEFAULT_UPCOMING_MAXEVENTS; if (isset($CFG->calendar_maxevents)) { $defaultmaxevents = intval($CFG->calendar_maxevents); } $eventlimit = get_user_preferences('calendar_maxevents', $defaultmaxevents); $tstart = $type->convert_to_timestamp($calendardate['year'], $calendardate['mon'], $calendardate['mday'], $calendardate['hours']); $date->setTimestamp($tstart); $date->modify('+' . $lookahead . ' days'); } else { $tstart = $type->convert_to_timestamp($calendardate['year'], $calendardate['mon'], 1); $monthdays = $type->get_num_days_in_month($calendardate['year'], $calendardate['mon']); $date->setTimestamp($tstart); $date->modify('+' . $monthdays . ' days'); if ($view === 'mini' || $view === 'minithree') { $template = 'core_calendar/calendar_mini'; } else { $template = 'core_calendar/calendar_month'; } } // We need to extract 1 second to ensure that we don't get into the next day. $date->modify('-1 second'); $tend = $date->getTimestamp(); list($userparam, $groupparam, $courseparam, $categoryparam) = array_map(function($param) { // If parameter is true, return null. if ($param === true) { return null; } // If parameter is false, return an empty array. if ($param === false) { return []; } // If the parameter is a scalar value, enclose it in an array. if (!is_array($param)) { return [$param]; } // No normalisation required. return $param; }, [$calendar->users, $calendar->groups, $calendar->courses, $calendar->categories]); $events = \core_calendar\local\api::get_events( $tstart, $tend, null, null, null, null, $eventlimit, null, $userparam, $groupparam, $courseparam, $categoryparam, true, true, function ($event) { if ($proxy = $event->get_course_module()) { $cminfo = $proxy->get_proxied_instance(); return $cminfo->uservisible; } if ($proxy = $event->get_category()) { $category = $proxy->get_proxied_instance(); return $category->is_uservisible(); } return true; } ); $related = [ 'events' => $events, 'cache' => new \core_calendar\external\events_related_objects_cache($events), 'type' => $type, ]; $data = []; if ($view == "month" || $view == "mini" || $view == "minithree") { $month = new \core_calendar\external\month_exporter($calendar, $type, $related); $month->set_includenavigation($includenavigation); $data = $month->export($renderer); } else if ($view == "day") { $day = new \core_calendar\external\calendar_day_exporter($calendar, $related); $data = $day->export($renderer); $template = 'core_calendar/calendar_day'; } else if ($view == "upcoming" || $view == "upcoming_mini") { $upcoming = new \core_calendar\external\calendar_upcoming_exporter($calendar, $related); $data = $upcoming->export($renderer); if ($view == "upcoming") { $template = 'core_calendar/calendar_upcoming'; } else if ($view == "upcoming_mini") { $template = 'core_calendar/calendar_upcoming_mini'; } } return [$data, $template]; } /** * Request and render event form fragment. * * @param array $args The fragment arguments. * @return string The rendered mform fragment. */ function calendar_output_fragment_event_form($args) { global $CFG, $OUTPUT, $USER; $html = ''; $data = []; $eventid = isset($args['eventid']) ? clean_param($args['eventid'], PARAM_INT) : null; $starttime = isset($args['starttime']) ? clean_param($args['starttime'], PARAM_INT) : null; $courseid = isset($args['courseid']) ? clean_param($args['courseid'], PARAM_INT) : null; $categoryid = isset($args['categoryid']) ? clean_param($args['categoryid'], PARAM_INT) : null; $event = null; $hasformdata = isset($args['formdata']) && !empty($args['formdata']); $context = \context_user::instance($USER->id); $editoroptions = \core_calendar\local\event\forms\create::build_editor_options($context); $formoptions = ['editoroptions' => $editoroptions]; $draftitemid = 0; if ($hasformdata) { parse_str(clean_param($args['formdata'], PARAM_TEXT), $data); if (isset($data['description']['itemid'])) { $draftitemid = $data['description']['itemid']; } } if ($starttime) { $formoptions['starttime'] = $starttime; } if (is_null($eventid)) { $mform = new \core_calendar\local\event\forms\create( null, $formoptions, 'post', '', null, true, $data ); // Let's check first which event types user can add. calendar_get_allowed_types($allowed, $courseid); // If the user is on course context and is allowed to add course events set the event type default to course. if ($courseid != SITEID && !empty($allowed->courses)) { $data['eventtype'] = 'course'; $data['courseid'] = $courseid; $data['groupcourseid'] = $courseid; } else if (!empty($categoryid) && !empty($allowed->category)) { $data['eventtype'] = 'category'; $data['categoryid'] = $categoryid; } $mform->set_data($data); } else { $event = calendar_event::load($eventid); $mapper = new \core_calendar\local\event\mappers\create_update_form_mapper(); $eventdata = $mapper->from_legacy_event_to_data($event); $data = array_merge((array) $eventdata, $data); $event->count_repeats(); $formoptions['event'] = $event; $data['description']['text'] = file_prepare_draft_area( $draftitemid, $event->context->id, 'calendar', 'event_description', $event->id, null, $data['description']['text'] ); $data['description']['itemid'] = $draftitemid; $mform = new \core_calendar\local\event\forms\update( null, $formoptions, 'post', '', null, true, $data ); $mform->set_data($data); // Check to see if this event is part of a subscription or import. // If so display a warning on edit. if (isset($event->subscriptionid) && ($event->subscriptionid != null)) { $renderable = new \core\output\notification( get_string('eventsubscriptioneditwarning', 'calendar'), \core\output\notification::NOTIFY_INFO ); $html .= $OUTPUT->render($renderable); } } if ($hasformdata) { $mform->is_validated(); } $html .= $mform->render(); return $html; } /** * Calculate the timestamp from the supplied Gregorian Year, Month, and Day. * * @param int $d The day * @param int $m The month * @param int $y The year * @param int $time The timestamp to use instead of a separate y/m/d. * @return int The timestamp */ function calendar_get_timestamp($d, $m, $y, $time = 0) { // If a day, month and year were passed then convert it to a timestamp. If these were passed // then we can assume the day, month and year are passed as Gregorian, as no where in core // should we be passing these values rather than the time. if (!empty($d) && !empty($m) && !empty($y)) { if (checkdate($m, $d, $y)) { $time = make_timestamp($y, $m, $d); } else { $time = time(); } } else if (empty($time)) { $time = time(); } return $time; } /** * Get the calendar footer options. * * @param calendar_information $calendar The calendar information object. * @return array The data for template and template name. */ function calendar_get_footer_options($calendar) { global $CFG, $USER, $DB, $PAGE; // Generate hash for iCal link. $rawhash = $USER->id . $DB->get_field('user', 'password', ['id' => $USER->id]) . $CFG->calendar_exportsalt; $authtoken = sha1($rawhash); $renderer = $PAGE->get_renderer('core_calendar'); $footer = new \core_calendar\external\footer_options_exporter($calendar, $USER->id, $authtoken); $data = $footer->export($renderer); $template = 'core_calendar/footer_options'; return [$data, $template]; } /** * Get the list of potential calendar filter types as a type => name * combination. * * @return array */ function calendar_get_filter_types() { $types = [ 'site', 'category', 'course', 'group', 'user', ]; return array_map(function($type) { return [ 'eventtype' => $type, 'name' => get_string("eventtype{$type}", "calendar"), ]; }, $types); } /** * Check whether the specified event type is valid. * * @param string $type * @return bool */ function calendar_is_valid_eventtype($type) { $validtypes = [ 'user', 'group', 'course', 'category', 'site', ]; return in_array($type, $validtypes); }