diff --git a/calendar/lib.php b/calendar/lib.php
index a8d908d11ff..3cacace7e38 100644
--- a/calendar/lib.php
+++ b/calendar/lib.php
@@ -23,6 +23,8 @@
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
+require_once "{$CFG->libdir}/bennu/bennu.inc.php";
+
/**
* These are read by the administration component to provide default values
*/
@@ -86,6 +88,26 @@ define('CALENDAR_EVENT_GROUP', 4);
define('CALENDAR_EVENT_USER', 8);
+/**
+ * CALENDAR_IMPORT_FROM_FILE - import the calendar from a file
+ */
+define('CALENDAR_IMPORT_FROM_FILE', 0);
+
+/**
+ * CALENDAR_IMPORT_FROM_URL - import the calendar from a URL
+ */
+define('CALENDAR_IMPORT_FROM_URL', 1);
+
+/**
+ * CALENDAR_IMPORT_EVENT_UPDATED - imported event was updated
+ */
+define('CALENDAR_IMPORT_EVENT_UPDATED', 1);
+
+/**
+ * CALENDAR_IMPORT_EVENT_INSERTED - imported event was added by insert
+ */
+define('CALENDAR_IMPORT_EVENT_INSERTED', 2);
+
/**
* Return the days of the week
*
@@ -2634,3 +2656,519 @@ class calendar_information {
$renderer->add_pretend_calendar_block($block, BLOCK_POS_RIGHT);
}
}
+
+/**
+ * Returns option list for the pollinterval setting.
+ */
+function calendar_get_pollinterval_choices() {
+ return array(
+ '0' => get_string('never', 'calendar'),
+ '3600' => get_string('hourly', 'calendar'),
+ '86400' => get_string('daily', 'calendar'),
+ '604800' => get_string('weekly', 'calendar'),
+ '2628000' => get_string('monthly', 'calendar'),
+ '31536000' => get_string('annually', 'calendar'),
+ );
+}
+
+/**
+ * Returns available options for the calendar event type.
+ */
+function calendar_get_eventtype_choices($courseid) {
+ $choices = array();
+ $allowed = new stdClass;
+ calendar_get_allowed_types($allowed, $courseid);
+
+ if ($allowed->user) {
+ $choices[0] = get_string('userevents', 'calendar');
+ }
+ if ($allowed->site) {
+ $choices[1] = get_string('globalevents', 'calendar');
+ }
+ if (!empty($allowed->courses)) {
+ $choices[$courseid] = get_string('courseevents', 'calendar');
+ }
+ if (!empty($allowed->groups) and is_array($allowed->groups)) {
+ $choices['group'] = get_string('group');
+ }
+
+ return array($choices, $allowed->groups);
+}
+
+/**
+ * Form for adding a subscription to a Moodle course calendar.
+ */
+class calendar_addsubscription_form extends moodleform {
+
+ function definition() {
+ $mform =& $this->_form;
+ $courseid = optional_param('course', 0, PARAM_INT);
+
+ // code to show/hide the form from the heading
+ $mform->addElement('html', '');
+ $mform->addElement('header', 'addsubscriptionform', 'Import calendar...');
+
+ $mform->addElement('text', 'name', get_string('subscriptionname', 'calendar'), 'maxlength="255" size="40"');
+ $mform->addRule('name', get_string('required'), 'required');
+
+ $mform->addElement('html', "Please provide either a URL to a remote calendar, or upload a file.");
+ $choices = array(CALENDAR_IMPORT_FROM_FILE => get_string('importfromfile', 'calendar'),
+ CALENDAR_IMPORT_FROM_URL => get_string('importfromurl', 'calendar'));
+ $mform->addElement('select', 'importfrom', get_string('importcalendarfrom', 'calendar'), $choices);
+ $mform->setDefault('importfrom', CALENDAR_IMPORT_FROM_URL);
+
+ $mform->addElement('text', 'url', get_string('importfromurl', 'calendar'), 'maxlength="255" size="50"');
+ $mform->addElement('filepicker', 'importfile', get_string('importfromfile', 'calendar'));
+
+ $mform->disabledIf('url', 'importfrom', 'eq', CALENDAR_IMPORT_FROM_FILE);
+ $mform->disabledIf('importfile', 'importfrom', 'eq', CALENDAR_IMPORT_FROM_URL);
+
+ $choices = calendar_get_pollinterval_choices();
+ $mform->addElement('select', 'pollinterval', get_string('pollinterval', 'calendar'), $choices);
+
+ $mform->setDefault('pollinterval', 604800);
+ $mform->addHelpButton('pollinterval', 'pollinterval', 'calendar');
+
+ // eventtype: 0 = user, 1 = global, anything else = course ID
+ list($choices, $groups) = calendar_get_eventtype_choices($courseid);
+ $mform->addElement('select', 'eventtype', get_string('eventkind', 'calendar'), $choices);
+ $mform->addRule('eventtype', get_string('required'), 'required');
+
+ if (!empty($groups) and is_array($groups)) {
+ $groupoptions = array();
+ foreach ($groups as $group) {
+ $groupoptions[$group->id] = $group->name;
+ }
+ $mform->addElement('select', 'groupid', get_string('typegroup', 'calendar'), $groupoptions);
+ $mform->disabledIf('groupid', 'eventtype', 'noteq', 'group');
+ }
+
+ $mform->addElement('hidden', 'course', optional_param('course', 0, PARAM_INT));
+ $mform->addElement('hidden', 'view', optional_param('view', 'upcoming', PARAM_ALPHA));
+ $mform->addElement('hidden', 'cal_d', optional_param('cal_d', 0, PARAM_INT));
+ $mform->addElement('hidden', 'cal_m', optional_param('cal_m', 0, PARAM_INT));
+ $mform->addElement('hidden', 'cal_y', optional_param('cal_y', 0, PARAM_INT));
+ $mform->addElement('hidden', 'id', optional_param('id', 0, PARAM_INT));
+
+ $mform->addElement('submit', 'add', get_string('add'));
+
+ // *sigh* folding up the form breaks the filepicker control
+ // $mform->addElement('html', '');
+ }
+
+ function get_ical_data() {
+ $formdata = $this->get_data();
+ switch ($formdata->importfrom) {
+ case CALENDAR_IMPORT_FROM_FILE:
+ $calendar = $this->get_file_content('importfile');
+ break;
+ case CALENDAR_IMPORT_FROM_URL:
+ $calendar = file_get_contents($formdata->importurl);
+ break;
+ }
+ return $calendar;
+ }
+}
+
+/**
+ * Add an iCalendar subscription to the database.
+ * @param object $sub The subscription object (e.g. from the form)
+ * @return int The insert ID, if any.
+ */
+function calendar_add_subscription($sub) {
+ global $DB, $USER;
+ $sub->courseid = $sub->eventtype;
+ if ($sub->eventtype == 'group') {
+ $sub->courseid = $sub->course;
+ }
+ $sub->userid = $USER->id;
+
+ // file subscriptions never update.
+ if (empty($sub->url)) {
+ $sub->pollinterval = 0;
+ }
+
+ if (!empty($sub->name)) {
+ if (empty($sub->id)) {
+ $id = $DB->insert_record('event_subscriptions', $sub);
+ return $id;
+ } else {
+ $DB->update_record('event_subscriptions', $sub);
+ return $sub->id;
+ }
+ } else {
+ print_error('errorbadsubscription', 'importcalendar');
+ }
+}
+
+/**
+ * Add an iCalendar event to the Moodle calendar.
+ * @param object $event The RFC-2445 iCalendar event
+ * @param int $courseid The course ID
+ * @param int $subscriptionid The iCalendar subscription ID
+ * @return int Code: 1=updated, 2=inserted, 0=error
+ */
+function calendar_add_icalendar_event($event, $courseid, $subscriptionid=null) {
+ global $DB, $USER;
+
+ $eventrecord = new stdClass;
+
+ // probably an unsupported X-MICROSOFT-CDO-BUSYSTATUS event.
+ if (empty($event->properties['SUMMARY'])) {
+ return 0;
+ }
+
+ $name = $event->properties['SUMMARY'][0]->value;
+ $name = str_replace('\n', '
', $name);
+ $name = str_replace('\\', '', $name);
+ $name = preg_replace('/\s+/', ' ', $name);
+ $eventrecord->name = clean_param($name, PARAM_CLEAN);
+
+ if (empty($event->properties['DESCRIPTION'][0]->value)) {
+ $description = '';
+ } else {
+ $description = $event->properties['DESCRIPTION'][0]->value;
+ $description = str_replace('\n', '
', $description);
+ $description = str_replace('\\', '', $description);
+ $description = preg_replace('/\s+/', ' ', $description);
+ }
+ $eventrecord->description = clean_param($description, PARAM_CLEAN);
+
+ // probably a repeating event with RRULE etc. TODO: skip for now
+ if (empty($event->properties['DTSTART'][0]->value)) {
+ return 0;
+ }
+
+ $eventrecord->timestart = strtotime($event->properties['DTSTART'][0]->value);
+ if (empty($event->properties['DTEND'])) {
+ $eventrecord->timeduration = 3600; // one hour if no end time specified
+ } else {
+ $eventrecord->timeduration = strtotime($event->properties['DTEND'][0]->value) - $eventrecord->timestart;
+ }
+ $eventrecord->uuid = $event->properties['UID'][0]->value;
+ $eventrecord->timemodified = time();
+
+ // Add the iCal subscription details if required
+ if ($sub = $DB->get_record('event_subscriptions', array('id' => $subscriptionid))) {
+ $eventrecord->subscriptionid = $subscriptionid;
+ $eventrecord->userid = $sub->userid;
+ $eventrecord->groupid = $sub->groupid;
+ $eventrecord->courseid = $sub->courseid;
+ } else {
+ $eventrecord->userid = $USER->id;
+ $eventrecord->groupid = 0; // TODO: ???
+ $eventrecord->courseid = $courseid;
+ }
+
+ if ($updaterecord = $DB->get_record('event', array('uuid' => $eventrecord->uuid))) {
+ $eventrecord->id = $updaterecord->id;
+ if ($DB->update_record('event', $eventrecord)) {
+ return CALENDAR_IMPORT_EVENT_UPDATED;
+ } else {
+ return 0;
+ }
+ } else {
+ if ($DB->insert_record('event', $eventrecord)) {
+ return CALENDAR_IMPORT_EVENT_INSERTED;
+ } else {
+ return 0;
+ }
+ }
+}
+
+/**
+ * Create the list of iCalendar subscriptions for a course calendar.
+ * @param int $courseid The course ID
+ * @return string The table output.
+ */
+function calendar_show_subscriptions($courseid, $importresults='') {
+ global $DB, $OUTPUT, $CFG, $USER;
+
+ $view = optional_param('view', '', PARAM_ALPHA);
+ $sesskey = sesskey();
+ $out = '';
+
+ $str->update = get_string('update');
+ $str->remove = get_string('remove');
+ $str->add = get_string('add');
+
+ $out .= $OUTPUT->box_start('generalbox calendarsubs');
+ $out .= $importresults;
+
+ $table = new html_table();
+ $table->head = array('Calendar', 'Last Updated', 'Poll', 'Actions');
+ $table->align = array('left', 'left', 'left', 'center');
+ $table->width = '100%';
+ $table->data = array();
+
+ $subs = $DB->get_records_sql('select * from {event_subscriptions}
+ where courseid = :courseid
+ or (courseid = 0 and userid = :userid)',
+ array('courseid' => $courseid, 'userid' => $USER->id));
+ if (empty($subs)) {
+ $c = new html_table_cell("No calendar subscriptions.");
+ $c->colspan = 4;
+ $table->data[] = new html_table_row(array($c));
+ }
+ foreach ($subs as $id => $sub) {
+ $label = empty($sub->url) ? $sub->name : "url}\">{$sub->name}";
+ $c_url = new html_table_cell($label);
+ $lastupdated = empty($sub->lastupdated)
+ ? get_string('never', 'calendar')
+ : date('Y-m-d H:i:s', $sub->lastupdated);
+ $c_updated = new html_table_cell($lastupdated);
+
+ if (empty($sub->url)) {
+ // don't update an iCal file, which has no URL.
+ $pollinterval = '';
+ } else {
+ // assemble pollinterval control
+ $pollinterval = "
Calendar subscription '{$sub->name}' updated.
" . calendar_update_subscription_events($id); + break; + + case $str->remove: + $sesskey = required_param('sesskey', PARAM_ALPHANUM); + $DB->delete_records('event', array('subscriptionid' => $id)); + $DB->delete_records('event_subscriptions', array('id' => $id)); + return "Calendar subscription '{$sub->name}' removed."; + break; + + default: + break; + } + return ''; +} + +/** + * From a URL, fetch the calendar and return an iCalendar object. + * @param string $url The iCalendar URL + * @return object The iCalendar object + */ +function calendar_get_icalendar($url) { + $calendar = file_get_contents($url); + $ical = new iCalendar(); + $ical->unserialize($calendar); + return $ical; +} + +/** + * Import events from an iCalendar object into a course calendar. + * @param object $ical The iCalendar object + * @param integer $courseid The course ID for the calendar + * @param integer $subscriptionid The subscription ID + * @return string A log of the import progress, including + * errors + */ +function calendar_import_icalendar_events($ical, $courseid, $subscriptionid=null) { + global $DB; + $return = ''; + $eventcount = 0; + $updatecount = 0; + + // large calendars take a while... + ini_set('max_execution_time', 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)); + } + foreach($ical->components['VEVENT'] as $event) { + $res = calendar_add_icalendar_event($event, $courseid, $subscriptionid); + switch ($res) { + case CALENDAR_IMPORT_EVENT_UPDATED: + $updatecount++; + break; + case CALENDAR_IMPORT_EVENT_INSERTED: + $eventcount++; + break; + case 0: + $return .= 'Failed to add event: '.(empty($event->properties['SUMMARY'])?'(no title)':$event->properties['SUMMARY'][0]->value)."
\n"; + break; + } + } + $return .= "Events imported: {$eventcount}
\n"; + $return .= "Events updated: {$updatecount}
\n"; + + // 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)) { + $sql = "delete from {event} where timemodified = :time and subscriptionid = :id"; + $DB->execute($sql, array('time' => 0, 'id' => $subscriptionid)); + $return .= "Events deleted: {$deletecount}
\n"; + } + } + + return $return; +} + +/** + * Fetch a calendar subscription and update the events in the calendar. + * @param integer $subscriptionid The course ID for the calendar + * @return string A log of the import progress, including + * errors + */ +function calendar_update_subscription_events($subscriptionid) { + global $DB; + $return = ''; + + $sub = $DB->get_record('event_subscriptions', array('id' => $subscriptionid)); + if (empty($sub)) { + print_error('errorbadsubscription', 'calendar'); + } + // Don't update a file subscription. TODO: Update from a new uploaded file? + if (empty($sub->url)) { + return 'File subscription not updated.'; + } + $ical = calendar_get_icalendar($sub->url); + $return = calendar_import_icalendar_events($ical, $sub->courseid, $subscriptionid); + $sub->lastupdated = time(); + $DB->update_record('event_subscriptions', $sub); + return $return; +} + +/** + * Update calendar subscriptions. + */ +function calendar_cron() { + global $DB; + mtrace("Updating calendar subscriptions:"); + $time = time(); + foreach ($DB->get_records_sql('select * from {event_subscriptions} + where pollinterval > 0 and lastupdated + pollinterval < ?', array($time)) as $sub) { + mtrace(" Updating calendar subscription '{$sub->name}' in course {$sub->courseid}"); + $log = calendar_update_subscription_events($sub->id); + mtrace(trim(strip_tags($log))); + } + mtrace("Finished updating calendar subscriptions."); + return true; +} + diff --git a/calendar/view.php b/calendar/view.php index 8f5695667a5..01dead64a0f 100644 --- a/calendar/view.php +++ b/calendar/view.php @@ -80,6 +80,10 @@ if ($courseid != SITEID && !empty($courseid)) { } require_course_login($course); +if (calendar_user_can_add_event($course)) { + $importresults = calendar_process_subscription_form($courseid); +} + $calendar = new calendar_information($day, $mon, $yr); $calendar->prepare_for_view($course, $courses); @@ -147,6 +151,9 @@ switch($view) { //Link to calendar export page echo $OUTPUT->container_start('bottom'); +if (calendar_user_can_add_event($course)) { + echo calendar_show_subscriptions($courseid, $importresults); +} if (!empty($CFG->enablecalendarexport)) { echo $OUTPUT->single_button(new moodle_url('export.php', array('course'=>$courseid)), get_string('exportcalendar', 'calendar')); if (isloggedin()) { diff --git a/lang/en/calendar.php b/lang/en/calendar.php index 96b6c4f9a13..2aa1979b150 100644 --- a/lang/en/calendar.php +++ b/lang/en/calendar.php @@ -25,6 +25,7 @@ $string['advancedoptions'] = 'Advanced options'; $string['allday'] = 'All day'; +$string['annually'] = 'Annually'; $string['calendar'] = 'Calendar'; $string['calendarheading'] = '{$a} Calendar'; $string['calendarpreferences'] = 'Calendar preferences'; @@ -37,6 +38,7 @@ $string['course'] = 'Course'; $string['courseevent'] = 'Course event'; $string['courseevents'] = 'Course events'; $string['courses'] = 'Courses'; +$string['daily'] = 'Daily'; $string['dayview'] = 'Day view'; $string['dayviewtitle'] = 'Day view: {$a}'; $string['daywithnoevents'] = 'There are no events this day.'; @@ -49,12 +51,14 @@ $string['durationminutes'] = 'Duration in minutes'; $string['durationnone'] = 'Without duration'; $string['durationuntil'] = 'Until'; $string['editevent'] = 'Editing event'; +$string['errorbadsubscription'] = 'Calendar subscription not found.'; $string['errorbeforecoursestart'] = 'Cannot set event before course start date'; $string['errorinvaliddate'] = 'Invalid date'; $string['errorinvalidminutes'] = 'Specify duration in minutes by giving a number between 1 and 999.'; $string['errorinvalidrepeats'] = 'Specify the number of events by giving a number between 1 and 99.'; $string['errornodescription'] = 'Description is required'; $string['errornoeventname'] = 'Name is required'; +$string['errorrequiredurlorfile'] = 'Either a URL or a file is required to import a calendar.'; $string['eventdate'] = 'Date'; $string['eventdescription'] = 'Description'; $string['eventduration'] = 'Duration'; @@ -92,19 +96,28 @@ $string['hidecourseevents'] = 'Hide course events'; $string['hideglobalevents'] = 'Hide global events'; $string['hidegroupsevents'] = 'Hide group events'; $string['hideuserevents'] = 'Hide user events'; +$string['hourly'] = 'Hourly'; $string['ical'] = 'iCal'; +$string['importcalendar'] = 'Import calendar'; +$string['importcalendarfrom'] = 'Import from'; +$string['importfromfile'] = 'Calendar file (.ics)'; +$string['importfromurl'] = 'Calendar URL'; $string['invalidtimedurationminutes'] = 'The duration in minutes you have entered is invalid. Please enter the duration in minutes greater than 0 or select no duration.'; $string['invalidtimedurationuntil'] = 'The date and time you selected for duration until is before the start time of the event. Please correct this before proceeding.'; $string['iwanttoexport'] = 'Export'; $string['manyevents'] = '{$a} events'; $string['mon'] = 'Mon'; $string['monday'] = 'Monday'; +$string['monthly'] = 'Monthly'; $string['monthlyview'] = 'Monthly view'; $string['monthnext'] = 'Next month'; $string['monththis'] = 'This month'; +$string['never'] = 'Never'; $string['newevent'] = 'New event'; $string['noupcomingevents'] = 'There are no upcoming events'; $string['oneevent'] = '1 event'; +$string['pollinterval'] = 'Poll interval'; +$string['pollinterval_help'] = 'How often you would like the calendar to update with new events.'; $string['preferences'] = 'Preferences'; $string['preferences_available'] = 'Your personal preferences'; $string['pref_lookahead'] = 'Upcoming events look-ahead'; @@ -134,6 +147,7 @@ $string['showgroupsevents'] = 'Show group events'; $string['showuserevents'] = 'Show user events'; $string['shown'] = 'shown'; $string['spanningevents'] = 'Events underway'; +$string['subscriptionname'] = 'Calendar name'; $string['sun'] = 'Sun'; $string['sunday'] = 'Sunday'; $string['thu'] = 'Thu'; @@ -165,6 +179,7 @@ $string['userevent'] = 'User event'; $string['userevents'] = 'User events'; $string['wed'] = 'Wed'; $string['wednesday'] = 'Wednesday'; +$string['weekly'] = 'Weekly'; $string['weeknext'] = 'Next week'; $string['weekthis'] = 'This week'; $string['yesterday'] = 'Yesterday'; diff --git a/lib/cronlib.php b/lib/cronlib.php index c0b86afb8c1..79e808bd4d3 100644 --- a/lib/cronlib.php +++ b/lib/cronlib.php @@ -350,6 +350,9 @@ function cron_run() { cron_execute_plugin_type('gradereport'); mtrace('Finished gradebook plugins'); + // run calendar cron + require_once "{$CFG->dirroot}/calendar/lib.php"; + calendar_cron(); // Run external blog cron if needed if (!empty($CFG->enableblogs) && $CFG->useexternalblogs) { diff --git a/lib/db/install.xml b/lib/db/install.xml old mode 100644 new mode 100755 index 36176538feb..0b2ac107d37 --- a/lib/db/install.xml +++ b/lib/db/install.xml @@ -1,5 +1,5 @@ -