Exemple #1
0
 /**
  * Factory method to return an Rfc5545Duration object from the difference
  * between two dates.
  * 
  * This is flawed, at present: we should really localise both dates and work 
  * out the difference in days, then localise the times and work out the difference
  * between the clock times.  On the other hand we're replacing a quick and dirty
  * hack that did it exactly the same way in the past, so we're not making things
  * any *worse* and at least we're making it clear that it could be improved...
  * 
  * The problem strikes (as they all do) across DST boundaries.
  * 
  * @todo Improve this to calculate the days difference and then the clock time diff
  * and work from there.
  * 
  * @param RepeatRuleDateTime $d1
  * @param RepeatRuleDateTime $d2
  * @return Rfc5545Duration
  */
 static function fromTwoDates($d1, $d2)
 {
     $diff = $d2->epoch() - $d1->epoch();
     return new Rfc5545Duration($diff);
 }
/**
* This function will import a whole calendar
* @param string $ics_content the ics file to import
* @param int $user_no the user wich will receive this ics file
* @param string $path the $path where it will be store such as /user_foo/home/
* @param boolean $caldav_context Whether we are responding via CalDAV or interactively
*
* Any VEVENTs with the same UID will be concatenated together
*/
function import_calendar_collection($ics_content, $user_no, $path, $caldav_context, $appending = false)
{
    global $c, $session, $tz_regex;
    $calendar = new vComponent($ics_content);
    $timezones = $calendar->GetComponents('VTIMEZONE', true);
    $components = $calendar->GetComponents('VTIMEZONE', false);
    // Add a parameter to calendars on import so it will only load events 'after' @author karora
    // date, or an RFC5545 duration format offset from the current date.
    $after = null;
    if (isset($_GET['after'])) {
        $after = $_GET['after'];
        if (strtoupper(substr($after, 0, 1)) == 'P' || strtoupper(substr($after, 0, 1)) == '-P') {
            $duration = new Rfc5545Duration($after);
            $duration = $duration->asSeconds();
            $after = time() - abs($duration);
        } else {
            $after = new RepeatRuleDateTime($after);
            $after = $after->epoch();
        }
    }
    $displayname = $calendar->GetPValue('X-WR-CALNAME');
    if (!$appending && isset($displayname)) {
        $sql = 'UPDATE collection SET dav_displayname = :displayname WHERE dav_name = :dav_name';
        $qry = new AwlQuery($sql, array(':displayname' => $displayname, ':dav_name' => $path));
        if (!$qry->Exec('PUT', __LINE__, __FILE__)) {
            rollback_on_error($caldav_context, $user_no, $path);
        }
    }
    $tz_ids = array();
    foreach ($timezones as $k => $tz) {
        $tz_ids[$tz->GetPValue('TZID')] = $k;
    }
    /** Build an array of resources.  Each resource is an array of vComponent */
    $resources = array();
    foreach ($components as $k => $comp) {
        $uid = $comp->GetPValue('UID');
        if ($uid == null || $uid == '') {
            $uid = uuid();
            $comp->AddProperty('UID', $uid);
            dbg_error_log('LOG WARN', ' PUT: New collection resource does not have a UID - we assign one!');
        }
        if (!isset($resources[$uid])) {
            $resources[$uid] = array();
        }
        $resources[$uid][] = $comp;
        /** Ensure we have the timezone component for this in our array as well */
        $tzid = GetTZID($comp);
        if (!empty($tzid) && !isset($resources[$uid][$tzid]) && isset($tz_ids[$tzid])) {
            $resources[$uid][$tzid] = $timezones[$tz_ids[$tzid]];
        }
    }
    $sql = 'SELECT * FROM collection WHERE dav_name = :dav_name';
    $qry = new AwlQuery($sql, array(':dav_name' => $path));
    if (!$qry->Exec('PUT', __LINE__, __FILE__)) {
        rollback_on_error($caldav_context, $user_no, $path);
    }
    if (!$qry->rows() == 1) {
        dbg_error_log('ERROR', ' PUT: Collection does not exist at "%s" for user %d', $path, $user_no);
        rollback_on_error($caldav_context, $user_no, $path);
    }
    $collection = $qry->Fetch();
    $collection_id = $collection->collection_id;
    // Fetch the current collection data
    $qry->QDo('SELECT dav_name, caldav_data FROM caldav_data WHERE collection_id=:collection_id', array(':collection_id' => $collection_id));
    $current_data = array();
    while ($row = $qry->Fetch()) {
        $current_data[$row->dav_name] = $row->caldav_data;
    }
    if (!(isset($c->skip_bad_event_on_import) && $c->skip_bad_event_on_import)) {
        $qry->Begin();
    }
    $base_params = array(':collection_id' => $collection_id);
    $dav_data_insert = <<<EOSQL
INSERT INTO caldav_data ( user_no, dav_name, dav_etag, caldav_data, caldav_type, logged_user, created, modified, collection_id )
    VALUES( :user_no, :dav_name, :etag, :dav_data, :caldav_type, :session_user, current_timestamp, current_timestamp, :collection_id )
EOSQL;
    $dav_data_update = <<<EOSQL
UPDATE caldav_data SET user_no=:user_no, caldav_data=:dav_data, dav_etag=:etag, caldav_type=:caldav_type, logged_user=:session_user,
  modified=current_timestamp WHERE collection_id=:collection_id AND dav_name=:dav_name
EOSQL;
    $calitem_insert = <<<EOSQL
INSERT INTO calendar_item (user_no, dav_name, dav_id, dav_etag, uid, dtstamp, dtstart, dtend, summary, location, class, transp,
                    description, rrule, tz_id, last_modified, url, priority, created, due, percent_complete, status, collection_id )
    VALUES ( :user_no, :dav_name, currval('dav_id_seq'), :etag, :uid, :dtstamp, :dtstart, ##dtend##, :summary, :location, :class, :transp,
                :description, :rrule, :tzid, :modified, :url, :priority, :created, :due, :percent_complete, :status, :collection_id)
EOSQL;
    $calitem_update = <<<EOSQL
UPDATE calendar_item SET user_no=:user_no, dav_etag=:etag, uid=:uid, dtstamp=:dtstamp,
                dtstart=:dtstart, dtend=##dtend##, summary=:summary, location=:location,
                class=:class, transp=:transp, description=:description, rrule=:rrule,
                tz_id=:tzid, last_modified=:modified, url=:url, priority=:priority,
                due=:due, percent_complete=:percent_complete, status=:status
       WHERE collection_id=:collection_id AND dav_name=:dav_name
EOSQL;
    $last_olson = '';
    if (count($resources) > 0) {
        $qry->QDo('SELECT new_sync_token(0,' . $collection_id . ')');
    }
    foreach ($resources as $uid => $resource) {
        /** Construct the VCALENDAR data */
        $vcal = new vCalendar();
        $vcal->SetComponents($resource);
        $icalendar = $vcal->Render();
        $dav_name = sprintf('%s%s.ics', $path, preg_replace('{[&?\\/@%+:]}', '', $uid));
        /** Do we need to do anything? */
        $inserting = true;
        if (isset($current_data[$dav_name])) {
            if ($icalendar == $current_data[$dav_name]) {
                unset($current_data[$dav_name]);
                continue;
            }
            $sync_change = 200;
            unset($current_data[$dav_name]);
            $inserting = false;
        } else {
            $sync_change = 201;
        }
        if (isset($c->skip_bad_event_on_import) && $c->skip_bad_event_on_import) {
            $qry->Begin();
        }
        /** As ever, we mostly deal with the first resource component */
        $first = $resource[0];
        $dav_data_params = $base_params;
        $dav_data_params[':user_no'] = $user_no;
        // We don't allow any of &?\/@%+: in the UID to appear in the path, but anything else is fair game.
        $dav_data_params[':dav_name'] = $dav_name;
        $dav_data_params[':etag'] = md5($icalendar);
        $calitem_params = $dav_data_params;
        $dav_data_params[':dav_data'] = $icalendar;
        $dav_data_params[':caldav_type'] = $first->GetType();
        $dav_data_params[':session_user'] = $session->user_no;
        $dtstart = $first->GetPValue('DTSTART');
        $calitem_params[':dtstart'] = $dtstart;
        if ((!isset($dtstart) || $dtstart == '') && $first->GetPValue('DUE') != '') {
            $dtstart = $first->GetPValue('DUE');
            if (isset($after)) {
                $dtstart_date = new RepeatRuleDateTime($first->GetProperty('DUE'));
            }
        } else {
            if (isset($after)) {
                $dtstart_date = new RepeatRuleDateTime($first->GetProperty('DTSTART'));
            }
        }
        $calitem_params[':rrule'] = $first->GetPValue('RRULE');
        // Skip it if it's after our start date for this import.
        if (isset($after) && empty($calitem_params[':rrule']) && $dtstart_date->epoch() < $after) {
            continue;
        }
        // Do we actually need to do anything?
        $inserting = true;
        if (isset($current_data[$dav_name])) {
            if ($icalendar == $current_data[$dav_name]) {
                if ($after == null) {
                    unset($current_data[$dav_name]);
                    continue;
                }
            }
            $sync_change = 200;
            unset($current_data[$dav_name]);
            $inserting = false;
        } else {
            $sync_change = 201;
        }
        // Write to the caldav_data table
        if (!$qry->QDo($inserting ? $dav_data_insert : $dav_data_update, $dav_data_params)) {
            rollback_on_error($caldav_context, $user_no, $path);
        }
        // Get the dav_id for this row
        $qry->QDo('SELECT dav_id FROM caldav_data WHERE dav_name = :dav_name ', array(':dav_name' => $dav_data_params[':dav_name']));
        if ($qry->rows() == 1 && ($row = $qry->Fetch())) {
            $dav_id = $row->dav_id;
        }
        $dtend = $first->GetPValue('DTEND');
        if (isset($dtend) && $dtend != '') {
            dbg_error_log('PUT', ' DTEND: "%s", DTSTART: "%s", DURATION: "%s"', $dtend, $dtstart, $first->GetPValue('DURATION'));
            $calitem_params[':dtend'] = $dtend;
            $dtend = ':dtend';
        } else {
            $dtend = 'NULL';
            if ($first->GetPValue('DURATION') != '' and $dtstart != '') {
                $duration = trim(preg_replace('#[PT]#', ' ', $first->GetPValue('DURATION')));
                if ($duration == '') {
                    $duration = '0 seconds';
                }
                $dtend = '(:dtstart::timestamp with time zone + :duration::interval)';
                $calitem_params[':duration'] = $duration;
            } elseif ($first->GetType() == 'VEVENT') {
                /**
                 * From RFC2445 4.6.1:
                 * For cases where a "VEVENT" calendar component specifies a "DTSTART"
                 * property with a DATE data type but no "DTEND" property, the events
                 * non-inclusive end is the end of the calendar date specified by the
                 * "DTSTART" property. For cases where a "VEVENT" calendar component specifies
                 * a "DTSTART" property with a DATE-TIME data type but no "DTEND" property,
                 * the event ends on the same calendar date and time of day specified by the
                 * "DTSTART" property.
                 *
                 * So we're looking for 'VALUE=DATE', to identify the duration, effectively.
                 *
                 */
                $dtstart_prop = $first->GetProperty('DTSTART');
                if (empty($dtstart_prop)) {
                    dbg_error_log('PUT', 'Invalid VEVENT without DTSTART, UID="%s" in collection %d', $uid, $collection_id);
                    continue;
                }
                $value_type = $dtstart_prop->GetParameterValue('VALUE');
                dbg_error_log('PUT', 'DTSTART without DTEND. DTSTART value type is %s', $value_type);
                if (isset($value_type) && $value_type == 'DATE') {
                    $dtend = '(:dtstart::timestamp with time zone::date + \'1 day\'::interval)';
                } else {
                    $dtend = ':dtstart';
                }
            }
        }
        $last_modified = $first->GetPValue('LAST-MODIFIED');
        if (!isset($last_modified) || $last_modified == '') {
            $last_modified = gmdate('Ymd\\THis\\Z');
        }
        $calitem_params[':modified'] = $last_modified;
        $dtstamp = $first->GetPValue('DTSTAMP');
        if (empty($dtstamp)) {
            $dtstamp = $last_modified;
        }
        $calitem_params[':dtstamp'] = $dtstamp;
        /** RFC2445, 4.8.1.3: Default is PUBLIC, or also if overridden by the collection settings */
        $class = $collection->public_events_only == 't' ? 'PUBLIC' : $first->GetPValue('CLASS');
        if (!isset($class) || $class == '') {
            $class = 'PUBLIC';
        }
        $calitem_params[':class'] = $class;
        /** Calculate what timezone to set, first, if possible */
        $tzid = GetTZID($first);
        if (!empty($tzid) && !empty($resource[$tzid])) {
            $tz = $resource[$tzid];
            $olson = $vcal->GetOlsonName($tz);
            dbg_error_log('PUT', ' Using TZID[%s] and location of [%s]', $tzid, isset($olson) ? $olson : '');
            if (!empty($olson) && $olson != $last_olson && preg_match($tz_regex, $olson)) {
                dbg_error_log('PUT', ' Setting timezone to %s', $olson);
                $qry->QDo('SET TIMEZONE TO \'' . $olson . "'");
                $last_olson = $olson;
            }
            $params = array(':tzid' => $tzid);
            $qry = new AwlQuery('SELECT 1 FROM timezones WHERE tzid = :tzid', $params);
            if ($qry->Exec('PUT', __LINE__, __FILE__) && $qry->rows() == 0) {
                $params[':olson_name'] = $olson;
                $params[':vtimezone'] = isset($tz) ? $tz->Render() : null;
                $params[':last_modified'] = isset($tz) ? $tz->GetPValue('LAST-MODIFIED') : null;
                if (empty($params[':last_modified'])) {
                    $params[':last_modified'] = gmdate('Ymd\\THis\\Z');
                }
                $qry->QDo('INSERT INTO timezones (tzid, olson_name, active, vtimezone, last_modified) VALUES(:tzid,:olson_name,false,:vtimezone,:last_modified)', $params);
            }
        } else {
            $tz = $olson = $tzid = null;
        }
        $sql = str_replace('##dtend##', $dtend, $inserting ? $calitem_insert : $calitem_update);
        $calitem_params[':tzid'] = $tzid;
        $calitem_params[':uid'] = $first->GetPValue('UID');
        $calitem_params[':summary'] = $first->GetPValue('SUMMARY');
        $calitem_params[':location'] = $first->GetPValue('LOCATION');
        $calitem_params[':transp'] = $first->GetPValue('TRANSP');
        $calitem_params[':description'] = $first->GetPValue('DESCRIPTION');
        $calitem_params[':url'] = $first->GetPValue('URL');
        $calitem_params[':priority'] = $first->GetPValue('PRIORITY');
        $calitem_params[':due'] = $first->GetPValue('DUE');
        $calitem_params[':percent_complete'] = $first->GetPValue('PERCENT-COMPLETE');
        $calitem_params[':status'] = $first->GetPValue('STATUS');
        if ($inserting) {
            $created = $first->GetPValue('CREATED');
            if ($created == '00001231T000000Z') {
                $created = '20001231T000000Z';
            }
            $calitem_params[':created'] = $created;
        }
        // Write the calendar_item row for this entry
        if (!$qry->QDo($sql, $calitem_params)) {
            rollback_on_error($caldav_context, $user_no, $path);
        }
        write_alarms($dav_id, $first);
        write_attendees($dav_id, $vcal);
        $qry->QDo("SELECT write_sync_change( {$collection_id}, {$sync_change}, :dav_name)", array(':dav_name' => $dav_name));
        do_scheduling_requests($vcal, true);
        if (isset($c->skip_bad_event_on_import) && $c->skip_bad_event_on_import) {
            $qry->Commit();
        }
    }
    if (!$appending && count($current_data) > 0) {
        $params = array(':collection_id' => $collection_id);
        if (isset($c->skip_bad_event_on_import) && $c->skip_bad_event_on_import) {
            $qry->Begin();
        }
        foreach ($current_data as $dav_name => $data) {
            $params[':dav_name'] = $dav_name;
            $qry->QDo('DELETE FROM caldav_data WHERE collection_id = :collection_id AND dav_name = :dav_name', $params);
            $qry->QDo('SELECT write_sync_change(:collection_id, 404, :dav_name)', $params);
        }
        if (isset($c->skip_bad_event_on_import) && $c->skip_bad_event_on_import) {
            $qry->Commit();
        }
    }
    if (!(isset($c->skip_bad_event_on_import) && $c->skip_bad_event_on_import)) {
        if (!$qry->Commit()) {
            rollback_on_error($caldav_context, $user_no, $path);
        }
    }
}
Exemple #3
0
function caldav_get_feed($request, $collection)
{
    global $c, $session;
    dbg_error_log("feed", "GET method handler");
    $collection->NeedPrivilege(array('DAV::read'));
    if (!$collection->Exists()) {
        $request->DoResponse(404, translate("Resource Not Found."));
    }
    if (!$collection->IsCollection() || !$collection->IsCalendar() && !(isset($c->get_includes_subcollections) && $c->get_includes_subcollections)) {
        $request->DoResponse(405, translate("Feeds are only supported for calendars at present."));
    }
    // Try and pull the answer out of a hat
    $cache = getCacheInstance();
    $cache_ns = 'collection-' . $collection->dav_name();
    $cache_key = 'feed' . $session->user_no;
    $response = $cache->get($cache_ns, $cache_key);
    if ($response !== false) {
        return $response;
    }
    $principal = $collection->GetProperty('principal');
    /**
     * The CalDAV specification does not define GET on a collection, but typically this is
     * used as a .ics download for the whole collection, which is what we do also.
     */
    $sql = 'SELECT caldav_data, caldav_type, caldav_data.user_no, caldav_data.dav_name,';
    $sql .= ' caldav_data.modified, caldav_data.created, ';
    $sql .= ' summary, dtstart, dtend, calendar_item.description ';
    $sql .= ' FROM collection INNER JOIN caldav_data USING(collection_id) INNER JOIN calendar_item USING ( dav_id ) WHERE ';
    if (isset($c->get_includes_subcollections) && $c->get_includes_subcollections) {
        $sql .= ' (collection.dav_name ~ :path_match ';
        $sql .= ' OR collection.collection_id IN (SELECT bound_source_id FROM dav_binding WHERE dav_binding.dav_name ~ :path_match)) ';
        $params = array(':path_match' => '^' . $request->path);
    } else {
        $sql .= ' caldav_data.collection_id = :collection_id ';
        $params = array(':collection_id' => $collection->resource_id());
    }
    $sql .= ' ORDER BY caldav_data.created DESC';
    $sql .= ' LIMIT ' . (isset($c->feed_item_limit) ? $c->feed_item_limit : 15);
    $qry = new AwlQuery($sql, $params);
    if (!$qry->Exec("GET", __LINE__, __FILE__)) {
        $request->DoResponse(500, translate("Database Error"));
    }
    /**
     * Here we are constructing the feed response for this collection, including
     * the timezones that are referred to by the events we have selected.
     * Library used: http://framework.zend.com/manual/en/zend.feed.writer.html
     */
    require_once 'AtomFeed.php';
    $feed = new AtomFeed();
    $feed->setTitle('DAViCal Atom Feed: ' . $collection->GetProperty('displayname'));
    $url = $c->protocol_server_port . $collection->url();
    $url = preg_replace('{/$}', '.ics', $url);
    $feed->setLink($url);
    $feed->setFeedLink($c->protocol_server_port_script . $request->path, 'atom');
    $feed->addAuthor(array('name' => $principal->GetProperty('displayname'), 'email' => $principal->GetProperty('email'), 'uri' => $c->protocol_server_port . $principal->url()));
    $feed_description = $collection->GetProperty('description');
    if (isset($feed_description) && $feed_description != '') {
        $feed->setDescription($feed_description);
    }
    require_once 'RRule-v2.php';
    $need_zones = array();
    $timezones = array();
    while ($event = $qry->Fetch()) {
        if ($event->caldav_type != 'VEVENT' && $event->caldav_type != 'VTODO' && $event->caldav_type != 'VJOURNAL') {
            dbg_error_log('feed', 'Skipping peculiar "%s" component in VCALENDAR', $event->caldav_type);
            continue;
        }
        $is_todo = $event->caldav_type == 'VTODO';
        $ical = new vComponent($event->caldav_data);
        $event_data = $ical->GetComponents('VTIMEZONE', false);
        $item = $feed->createEntry();
        $item->setId($c->protocol_server_port_script . ConstructURL($event->dav_name));
        $dt_created = new RepeatRuleDateTime($event->created);
        $item->setDateCreated($dt_created->epoch());
        $dt_modified = new RepeatRuleDateTime($event->modified);
        $item->setDateModified($dt_modified->epoch());
        $summary = $event->summary;
        $p_title = $summary != '' ? $summary : translate('No summary');
        if ($is_todo) {
            $p_title = "TODO: " . $p_title;
        }
        $item->setTitle($p_title);
        $content = "";
        $dt_start = new RepeatRuleDateTime($event->dtstart);
        if ($dt_start != null) {
            $p_time = '<strong>' . translate('Time') . ':</strong> ' . strftime(translate('%F %T'), $dt_start->epoch());
            $dt_end = new RepeatRuleDateTime($event->dtend);
            if ($dt_end != null) {
                $p_time .= ' - ' . ($dt_end->AsDate() == $dt_start->AsDate() ? strftime(translate('%T'), $dt_end->epoch()) : strftime(translate('%F %T'), $dt_end->epoch()));
            }
            $content .= $p_time;
        }
        $p_location = $event_data[0]->GetProperty('LOCATION');
        if ($p_location != null) {
            $content .= '<br />' . '<strong>' . translate('Location') . '</strong>: ' . hyperlink($p_location->Value());
        }
        $p_attach = $event_data[0]->GetProperty('ATTACH');
        if ($p_attach != null) {
            $content .= '<br />' . '<strong>' . translate('Attachment') . '</strong>: ' . hyperlink($p_attach->Value());
        }
        $p_url = $event_data[0]->GetProperty('URL');
        if ($p_url != null) {
            $content .= '<br />' . '<strong>' . translate('URL') . '</strong>: ' . hyperlink($p_url->Value());
        }
        $p_cat = $event_data[0]->GetProperty('CATEGORIES');
        if ($p_cat != null) {
            $content .= '<br />' . '<strong>' . translate('Categories') . '</strong>: ' . $p_cat->Value();
            $categories = explode(',', $p_cat->Value());
            foreach ($categories as $category) {
                $item->addCategory(array('term' => trim($category)));
            }
        }
        $p_description = $event->description;
        if ($p_description != '') {
            $content .= '<br />' . '<br />' . '<strong>' . translate('Description') . '</strong>:<br />' . nl2br(hyperlink($p_description));
            $item->setDescription($p_description);
        }
        $item->setContent($content);
        $feed->addEntry($item);
        //break;
    }
    $last_modified = new RepeatRuleDateTime($collection->GetProperty('modified'));
    $feed->setDateModified($last_modified->epoch());
    $response = $feed->export('atom');
    $cache->set($cache_ns, $cache_key, $response);
    return $response;
}