Merge branch 'wip-MDL-34530_MASTER' of git://github.com/jason-platts/moodle
[moodle.git] / calendar / lib.php
index 9fc23c5..86ca22f 100644 (file)
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 
+if (!defined('MOODLE_INTERNAL')) {
+    die('Direct access to this script is forbidden.');    ///  It must be included from a Moodle page
+}
+
 /**
  *  These are read by the administration component to provide default values
  */
@@ -86,6 +90,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
  *
@@ -298,13 +322,13 @@ function calendar_get_mini($courses, $groups, $users, $cal_month = false, $cal_y
                     $popupalt  = $event->modulename;
                     $component = $event->modulename;
                 } else if ($event->courseid == SITEID) {                                // Site event
-                    $popupicon = 'c/site';
+                    $popupicon = 'i/siteevent';
                 } else if ($event->courseid != 0 && $event->courseid != SITEID && $event->groupid == 0) {      // Course event
-                    $popupicon = 'c/course';
+                    $popupicon = 'i/courseevent';
                 } else if ($event->groupid) {                                      // Group event
-                    $popupicon = 'c/group';
+                    $popupicon = 'i/groupevent';
                 } else if ($event->userid) {                                       // User event
-                    $popupicon = 'c/user';
+                    $popupicon = 'i/userevent';
                 }
 
                 $dayhref->set_anchor('event_'.$event->id);
@@ -332,7 +356,7 @@ function calendar_get_mini($courses, $groups, $users, $cal_month = false, $cal_y
             } else if(isset($typesbyday[$day]['startuser'])) {
                 $class .= ' calendar_event_user';
             }
-            $cell = html_writer::link((string)$dayhref, $day, array('aria-controls' => $popupid.'_panel', 'id' => $popupid));
+            $cell = html_writer::link($dayhref, $day, array('aria-controls' => $popupid.'_panel', 'id' => $popupid));
         } else {
             $cell = $day;
         }
@@ -375,7 +399,7 @@ function calendar_get_mini($courses, $groups, $users, $cal_month = false, $cal_y
             if(! isset($eventsbyday[$day])) {
                 $class .= ' eventnone';
                 $popupid = calendar_get_popup(true, false);
-                $cell = html_writer::link('#', $day, array('aria-controls' => $popupid.'_panel', 'id' => $popupid));;
+                $cell = html_writer::link('#', $day, array('aria-controls' => $popupid.'_panel', 'id' => $popupid));
             }
             $cell = get_accesshide($today.' ').$cell;
         }
@@ -583,14 +607,14 @@ function calendar_add_event_metadata($event) {
         $context = context_course::instance($module->course);
         $fullname = format_string($coursecache[$module->course]->fullname, true, array('context' => $context));
 
-        $event->icon = '<img height="16" width="16" src="'.$icon.'" alt="'.$eventtype.'" title="'.$modulename.'" style="vertical-align: middle;" />';
+        $event->icon = '<img src="'.$icon.'" alt="'.$eventtype.'" title="'.$modulename.'" class="icon" />';
         $event->referer = '<a href="'.$CFG->wwwroot.'/mod/'.$event->modulename.'/view.php?id='.$module->id.'">'.$event->name.'</a>';
         $event->courselink = '<a href="'.$CFG->wwwroot.'/course/view.php?id='.$module->course.'">'.$fullname.'</a>';
         $event->cmid = $module->id;
 
 
     } else if($event->courseid == SITEID) {                              // Site event
-        $event->icon = '<img height="16" width="16" src="'.$OUTPUT->pix_url('c/site') . '" alt="'.get_string('globalevent', 'calendar').'" style="vertical-align: middle;" />';
+        $event->icon = '<img src="'.$OUTPUT->pix_url('i/siteevent') . '" alt="'.get_string('globalevent', 'calendar').'" class="icon" />';
         $event->cssclass = 'calendar_event_global';
     } else if($event->courseid != 0 && $event->courseid != SITEID && $event->groupid == 0) {          // Course event
         calendar_get_course_cached($coursecache, $event->courseid);
@@ -598,14 +622,14 @@ function calendar_add_event_metadata($event) {
         $context = context_course::instance($event->courseid);
         $fullname = format_string($coursecache[$event->courseid]->fullname, true, array('context' => $context));
 
-        $event->icon = '<img height="16" width="16" src="'.$OUTPUT->pix_url('c/course') . '" alt="'.get_string('courseevent', 'calendar').'" style="vertical-align: middle;" />';
+        $event->icon = '<img src="'.$OUTPUT->pix_url('i/courseevent') . '" alt="'.get_string('courseevent', 'calendar').'" class="icon" />';
         $event->courselink = '<a href="'.$CFG->wwwroot.'/course/view.php?id='.$event->courseid.'">'.$fullname.'</a>';
         $event->cssclass = 'calendar_event_course';
     } else if ($event->groupid) {                                    // Group event
-        $event->icon = '<img height="16" width="16" src="'.$OUTPUT->pix_url('c/group') . '" alt="'.get_string('groupevent', 'calendar').'" style="vertical-align: middle;" />';
+        $event->icon = '<img src="'.$OUTPUT->pix_url('i/groupevent') . '" alt="'.get_string('groupevent', 'calendar').'" class="icon" />';
         $event->cssclass = 'calendar_event_group';
     } else if($event->userid) {                                      // User event
-        $event->icon = '<img height="16" width="16" src="'.$OUTPUT->pix_url('c/user') . '" alt="'.get_string('userevent', 'calendar').'" style="vertical-align: middle;" />';
+        $event->icon = '<img src="'.$OUTPUT->pix_url('i/userevent') . '" alt="'.get_string('userevent', 'calendar').'" class="icon" />';
         $event->cssclass = 'calendar_event_user';
     }
     return $event;
@@ -1037,7 +1061,7 @@ function calendar_get_link_href($linkbase, $d, $m, $y) {
         return '';
     }
     if (!($linkbase instanceof moodle_url)) {
-        $linkbase = new moodle_url();
+        $linkbase = new moodle_url($linkbase);
     }
     if (!empty($d)) {
         $linkbase->param('cal_d', $d);
@@ -1129,7 +1153,7 @@ function calendar_get_block_upcoming($events, $linkhref = NULL) {
             continue;
         }
         $events[$i] = calendar_add_event_metadata($events[$i]);
-        $content .= '<div class="event"><span class="icon c0">'.$events[$i]->icon.'</span> ';
+        $content .= '<div class="event"><span class="icon c0">'.$events[$i]->icon.'</span>';
         if (!empty($events[$i]->referer)) {
             // That's an activity event, so let's provide the hyperlink
             $content .= $events[$i]->referer;
@@ -1441,6 +1465,11 @@ function calendar_edit_event_allowed($event) {
         return false;
     }
 
+    // You cannot edit calendar subscription events presently.
+    if (!empty($event->subscriptionid)) {
+        return false;
+    }
+
     $sitecontext = context_system::instance();
     // if user has manageentries at site level, return true
     if (has_capability('moodle/calendar:manageentries', $sitecontext)) {
@@ -1986,7 +2015,7 @@ class calendar_event {
             $cm = get_coursemodule_from_instance($data->modulename, $data->instance, 0, false, MUST_EXIST);
             $context =  context_course::instance($cm->course);
         } else {
-            $context =  context_user::instance();
+            $context =  context_user::instance($data->userid);
         }
 
         return $context;
@@ -2091,24 +2120,22 @@ class calendar_event {
             if ($usingeditor) {
                 switch ($this->properties->eventtype) {
                     case 'user':
-                        $this->editorcontext = $this->properties->context;
                         $this->properties->courseid = 0;
+                        $this->properties->course = 0;
                         $this->properties->groupid = 0;
                         $this->properties->userid = $USER->id;
                         break;
                     case 'site':
-                        $this->editorcontext = $this->properties->context;
                         $this->properties->courseid = SITEID;
+                        $this->properties->course = SITEID;
                         $this->properties->groupid = 0;
                         $this->properties->userid = $USER->id;
                         break;
                     case 'course':
-                        $this->editorcontext = $this->properties->context;
                         $this->properties->groupid = 0;
                         $this->properties->userid = $USER->id;
                         break;
                     case 'group':
-                        $this->editorcontext = $this->properties->context;
                         $this->properties->userid = $USER->id;
                         break;
                     default:
@@ -2118,6 +2145,13 @@ class calendar_event {
                         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->properties);
+                    $this->editorcontext = $this->properties->context;
+                }
+
                 $editor = $this->properties->description;
                 $this->properties->format = $this->properties->description['format'];
                 $this->properties->description = $this->properties->description['text'];
@@ -2136,7 +2170,6 @@ class calendar_event {
                                                 $this->editoroptions,
                                                 $editor['text'],
                                                 $this->editoroptions['forcehttps']);
-
                 $DB->set_field('event', 'description', $this->properties->description, array('id'=>$this->properties->id));
             }
 
@@ -2621,7 +2654,7 @@ class calendar_information {
         return make_timestamp($this->year, $this->month, $this->day+1);
     }
     /**
-     * Adds the pretend blocks for teh calendar
+     * Adds the pretend blocks for the calendar
      *
      * @param core_calendar_renderer $renderer
      * @param bool $showfilters display filters, false is set as default
@@ -2642,3 +2675,334 @@ class calendar_information {
         $renderer->add_pretend_calendar_block($block, BLOCK_POS_RIGHT);
     }
 }
+
+/**
+ * Returns option list for the poll interval setting.
+ *
+ * @return array An array of poll interval options. Interval => description.
+ */
+function calendar_get_pollinterval_choices() {
+    return array(
+        '0' => new lang_string('never', 'calendar'),
+        '3600' => new lang_string('hourly', 'calendar'),
+        '86400' => new lang_string('daily', 'calendar'),
+        '604800' => new lang_string('weekly', 'calendar'),
+        '2628000' => new lang_string('monthly', 'calendar'),
+        '31536000' => new lang_string('annually', 'calendar')
+    );
+}
+
+/**
+ * Returns option list of available options for the calendar event type, given the current user and course.
+ *
+ * @param int $courseid The id of the course
+ * @return array An array containing the event types the user can create.
+ */
+function calendar_get_eventtype_choices($courseid) {
+    $choices = array();
+    $allowed = new stdClass;
+    calendar_get_allowed_types($allowed, $courseid);
+
+    if ($allowed->user) {
+        $choices['user'] = get_string('userevents', 'calendar');
+    }
+    if ($allowed->site) {
+        $choices['site'] = get_string('siteevents', 'calendar');
+    }
+    if (!empty($allowed->courses)) {
+        $choices['course'] = get_string('courseevents', 'calendar');
+    }
+    if (!empty($allowed->groups) and is_array($allowed->groups)) {
+        $choices['group'] = get_string('group');
+    }
+
+    return array($choices, $allowed->groups);
+}
+
+/**
+ * Add an iCalendar subscription to the database.
+ *
+ * @param stdClass $sub The subscription object (e.g. from the form)
+ * @return int The insert ID, if any.
+ */
+function calendar_add_subscription($sub) {
+    global $DB, $USER, $SITE;
+
+    if ($sub->eventtype === 'site') {
+        $sub->courseid = $SITE->id;
+    } else if ($sub->eventtype === 'group' || $sub->eventtype === 'course') {
+        $sub->courseid = $sub->course;
+    } else {
+        // User events.
+        $sub->courseid = 0;
+    }
+    $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;
+
+    // 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', '<br />', $name);
+    $name = str_replace('\\', '', $name);
+    $name = preg_replace('/\s+/', ' ', $name);
+
+    $eventrecord = new stdClass;
+    $eventrecord->name = clean_param($name, PARAM_NOTAGS);
+
+    if (empty($event->properties['DESCRIPTION'][0]->value)) {
+        $description = '';
+    } else {
+        $description = $event->properties['DESCRIPTION'][0]->value;
+        $description = str_replace('\n', '<br />', $description);
+        $description = str_replace('\\', '', $description);
+        $description = preg_replace('/\s+/', ' ', $description);
+    }
+    $eventrecord->description = clean_param($description, PARAM_NOTAGS);
+
+    // 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;
+        $eventrecord->eventtype = $sub->eventtype;
+    } else {
+        // We should never do anything with an event without a subscription reference.
+        return 0;
+    }
+
+    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;
+        }
+    }
+}
+
+/**
+ * Update a subscription from the form data in one of the rows in the existing subscriptions table.
+ *
+ * @param int $subscriptionid The ID of the subscription we are acting upon.
+ * @param int $pollinterval The poll interval to use.
+ * @param int $action The action to be performed. One of update or remove.
+ * @return string A log of the import progress, including errors
+ */
+function calendar_process_subscription_row($subscriptionid, $pollinterval, $action) {
+    global $DB;
+
+    if (empty($subscriptionid)) {
+        return '';
+    }
+
+    // Fetch the subscription from the database making sure it exists.
+    $sub = $DB->get_record('event_subscriptions', array('id' => $subscriptionid), '*', MUST_EXIST);
+
+    $strupdate = get_string('update');
+    $strremove = get_string('remove');
+    // Update or remove the subscription, based on action.
+    switch ($action) {
+        case $strupdate:
+            // Skip updating file subscriptions.
+            if (empty($sub->url)) {
+                break;
+            }
+            $sub->pollinterval = $pollinterval;
+            $DB->update_record('event_subscriptions', $sub);
+
+            // Update the events.
+            return "<p>".get_string('subscriptionupdated', 'calendar', $sub->name)."</p>" . calendar_update_subscription_events($subscriptionid);
+
+        case $strremove:
+            $DB->delete_records('event', array('subscriptionid' => $subscriptionid));
+            $DB->delete_records('event_subscriptions', array('id' => $subscriptionid));
+            return get_string('subscriptionremoved', 'calendar', $sub->name);
+            break;
+
+        default:
+            break;
+    }
+    return '';
+}
+
+/**
+ * From a URL, fetch the calendar and return an iCalendar object.
+ *
+ * @param string $url The iCalendar URL
+ * @return stdClass 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 stdClass $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, $courseid, $subscriptionid = null) {
+    global $DB;
+    $return = '';
+    $eventcount = 0;
+    $updatecount = 0;
+
+    // Large calendars take a while...
+    set_time_limit(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 .= '<p>'.get_string('erroraddingevent', 'calendar').': '.(empty($event->properties['SUMMARY'])?'('.get_string('notitle', 'calendar').')':$event->properties['SUMMARY'][0]->value)." </p>\n";
+            break;
+        }
+    }
+    $return .= "<p> ".get_string('eventsimported', 'calendar', $eventcount)."</p>";
+    $return .= "<p> ".get_string('eventsupdated', 'calendar', $updatecount)."</p>";
+
+    // 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 .= "<p> ".get_string('eventsdeleted', 'calendar').": {$deletecount} </p>\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) {
+    global $DB;
+
+    $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.
+ *
+ * @return bool
+ */
+function calendar_cron() {
+    global $CFG, $DB;
+
+    // In order to execute this we need bennu.
+    require_once($CFG->libdir.'/bennu/bennu.inc.php');
+
+    mtrace('Updating calendar subscriptions:');
+
+    $time = time();
+    $subscriptions = $DB->get_records_sql('SELECT * FROM {event_subscriptions} WHERE pollinterval > 0 AND lastupdated + pollinterval < ?', array($time));
+    foreach ($subscriptions as $sub) {
+        mtrace("Updating calendar subscription {$sub->name} in course {$sub->courseid}");
+        try {
+            $log = calendar_update_subscription_events($sub->id);
+        } catch (moodle_exception $ex) {
+
+        }
+        mtrace(trim(strip_tags($log)));
+    }
+
+    mtrace('Finished updating calendar subscriptions.');
+
+    return true;
+}