Page MenuHomePhorge

No OneTemporary

diff --git a/lib/Kolab/CalDAV/CalendarBackend.php b/lib/Kolab/CalDAV/CalendarBackend.php
index 89e2d25..1ebc3ef 100644
--- a/lib/Kolab/CalDAV/CalendarBackend.php
+++ b/lib/Kolab/CalDAV/CalendarBackend.php
@@ -1,949 +1,953 @@
<?php
/**
* SabreDAV Calendaring backend for Kolab.
*
* @author Thomas Bruederli <bruederli@kolabsys.com>
*
* Copyright (C) 2013, Kolab Systems AG <contact@kolabsys.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program 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 Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
namespace Kolab\CalDAV;
use \PEAR;
use \rcube;
use \rcube_charset;
use \kolab_storage;
use \libcalendaring;
use Kolab\Utils\DAVBackend;
use Kolab\Utils\VObjectUtils;
use Sabre\CalDAV;
use Sabre\VObject;
/**
* Kolab Calendaring backend.
*
* Checkout the Sabre\CalDAV\Backend\BackendInterface for all the methods that must be implemented.
*
*/
class CalendarBackend extends CalDAV\Backend\AbstractBackend
{
private $calendars;
private $folders;
private $aliases;
private $useragent;
/**
* Read available calendar folders from server
*/
private function _read_calendars()
{
// already read sources
if (isset($this->calendars))
return $this->calendars;
// get all folders that have "event" type
$folders = kolab_storage::get_folders('event');
$this->calendars = $this->folders = $this->aliases = array();
foreach (DAVBackend::sort_folders($folders) as $folder) {
$id = DAVBackend::get_uid($folder);
$this->folders[$id] = $folder;
$fdata = $folder->get_imap_data(); // fetch IMAP folder data for CTag generation
$this->calendars[$id] = array(
'id' => $id,
'uri' => $id,
'{DAV:}displayname' => html_entity_decode($folder->get_name(), ENT_COMPAT, RCUBE_CHARSET),
'{http://apple.com/ns/ical/}calendar-color' => $folder->get_color(),
'{http://calendarserver.org/ns/}getctag' => sprintf('%d-%d-%d', $fdata['UIDVALIDITY'], $fdata['HIGHESTMODSEQ'], $fdata['UIDNEXT']),
'{urn:ietf:params:xml:ns:caldav}supported-calendar-component-set' => new CalDAV\Property\SupportedCalendarComponentSet(array('VEVENT')),
'{urn:ietf:params:xml:ns:caldav}schedule-calendar-transp' => new CalDAV\Property\ScheduleCalendarTransp('opaque'),
);
$this->aliases[$folder->name] = $id;
// these properties are used for sharing supprt (not yet active)
if (false && $folder->get_namespace() != 'personal') {
$rights = $folder->get_myrights();
$this->calendars[$id]['{http://calendarserver.org/ns/}shared-url'] = '/calendars/' . $folder->get_owner() . '/' . $id;
$this->calendars[$id]['{http://calendarserver.org/ns/}owner-principal'] = $folder->get_owner();
$this->calendars[$id]['{http://sabredav.org/ns}read-only'] = strpos($rights, 'i') === false;
}
}
return $this->calendars;
}
/**
* Getter for a kolab_storage_folder representing the calendar for the given ID
*
* @param string Calendar ID
* @return object kolab_storage_folder instance
*/
public function get_storage_folder($id)
{
// resolve alias name
if ($this->aliases[$id]) {
$id = $this->aliases[$id];
}
if ($this->folders[$id]) {
return $this->folders[$id];
}
else {
return DAVBackend::get_storage_folder($id, 'event');
}
}
/**
* Returns a list of calendars for a principal.
*
* Every calendars is an array with the following keys:
* * id, a unique id that will be used by other functions to modify the
* calendar. This can be the same as the uri or a database key.
* * uri, which the basename of the uri with which the calendar is
* accessed.
* * principaluri. The owner of the calendar. Almost always the same as
* principalUri passed to this method.
*
* Furthermore it can contain webdav properties in clark notation. A very
* common one is '{DAV:}displayname'.
*
* @param string $principalUri
* @return array
*/
public function getCalendarsForUser($principalUri)
{
console(__METHOD__, $principalUri);
$this->_read_calendars();
$calendars = array();
foreach ($this->calendars as $id => $cal) {
$this->calendars[$id]['principaluri'] = $principalUri;
$calendars[] = $this->calendars[$id];
}
return $calendars;
}
/**
* Returns calendar properties for a specific node identified by name/uri
*
* @param string Node name/uri
* @return array Hash array with calendar properties or null if not found
*/
public function getCalendarByName($calendarUri)
{
console(__METHOD__, $calendarUri);
$this->_read_calendars();
$id = $calendarUri;
// resolve aliases (calendar by folder name)
if ($this->aliases[$calendarUri]) {
$id = $this->aliases[$calendarUri];
}
return $this->calendars[$id];
}
/**
* Creates a new calendar for a principal.
*
* If the creation was a success, an id must be returned that can be used to reference
* this calendar in other methods, such as updateCalendar.
*
* @param string $principalUri
* @param string $calendarUri
* @param array $properties
* @return void
*/
public function createCalendar($principalUri, $calendarUri, array $properties)
{
console(__METHOD__, $calendarUri, $properties);
return DAVBackend::folder_create('event', $properties, $calendarUri);
}
/**
* Updates properties for a calendar.
*
* The mutations array uses the propertyName in clark-notation as key,
* and the array value for the property value. In the case a property
* should be deleted, the property value will be null.
*
* This method must be atomic. If one property cannot be changed, the
* entire operation must fail.
*
* If the operation was successful, true can be returned.
* If the operation failed, false can be returned.
*
* Deletion of a non-existent property is always successful.
*
* Lastly, it is optional to return detailed information about any
* failures. In this case an array should be returned with the following
* structure:
*
* array(
* 403 => array(
* '{DAV:}displayname' => null,
* ),
* 424 => array(
* '{DAV:}owner' => null,
* )
* )
*
* In this example it was forbidden to update {DAV:}displayname.
* (403 Forbidden), which in turn also caused {DAV:}owner to fail
* (424 Failed Dependency) because the request needs to be atomic.
*
* @param mixed $calendarId
* @param array $mutations
* @return bool|array
*/
public function updateCalendar($calendarId, array $mutations)
{
console(__METHOD__, $calendarId, $mutations);
$folder = $this->get_storage_folder($calendarId);
return DAVBackend::folder_update($folder, $mutations);
}
/**
* Delete a calendar and all it's objects
*
* @param mixed $calendarId
* @return void
*/
public function deleteCalendar($calendarId)
{
console(__METHOD__, $calendarId);
$folder = $this->get_storage_folder($calendarId);
if ($folder && !kolab_storage::folder_delete($folder->name)) {
rcube::raise_error(array(
'code' => 600, 'type' => 'php',
'file' => __FILE__, 'line' => __LINE__,
'message' => "Error deleting calendar folder $folder->name"),
true, false);
}
}
/**
* Returns all calendar objects within a calendar.
*
* Every item contains an array with the following keys:
* * id - unique identifier which will be used for subsequent updates
* * calendardata - The iCalendar-compatible calendar data (optional)
* * uri - a unique key which will be used to construct the uri. This can be any arbitrary string.
* * lastmodified - a timestamp of the last modification time
* * etag - An arbitrary string, surrounded by double-quotes. (e.g.: "abcdef"')
* * calendarid - The calendarid as it was passed to this function.
* * size - The size of the calendar objects, in bytes.
*
* Note that the etag is optional, but it's highly encouraged to return for
* speed reasons.
*
* If neither etag or size are specified, the calendardata will be
* used/fetched to determine these numbers. If both are specified the
* amount of times this is needed is reduced by a great degree.
*
* @param mixed $calendarId
* @return array
*/
public function getCalendarObjects($calendarId)
{
console(__METHOD__, $calendarId);
$query = array();
$events = array();
$storage = $this->get_storage_folder($calendarId);
if ($storage) {
foreach ((array)$storage->select($query) as $event) {
$events[] = array(
'id' => $event['uid'],
'uri' => $event['uid'] . '.ics',
'lastmodified' => $event['changed']->format('U'),
'calendarid' => $calendarId,
'etag' => self::_get_etag($event),
'size' => $event['_size'],
);
}
}
return $events;
}
/**
* Returns information from a single calendar object, based on it's object
* uri.
*
* The returned array must have the same keys as getCalendarObjects. The
* 'calendardata' object is required here though, while it's not required
* for getCalendarObjects.
*
* @param mixed $calendarId
* @param string $objectUri
* @return array
*/
public function getCalendarObject($calendarId, $objectUri)
{
console(__METHOD__, $calendarId, $objectUri);
$uid = basename($objectUri, '.ics');
$storage = $this->get_storage_folder($calendarId);
// attachment content is requested
if (preg_match('!^(.+).ics:attachment:(\d+):.+$!', $objectUri, $m)) {
$uid = $m[1]; $part = $m[2];
}
if ($storage && ($event = $storage->get_object($uid))) {
// deliver attachment content directly
if ($part && !empty($event['_attachments'])) {
foreach ($event['_attachments'] as $attachment) {
if ($attachment['id'] == $part) {
header('Content-Type: ' . $attachment['mimetype']);
header('Content-Disposition: inline; filename="' . $attachment['name'] . '"');
$storage->get_attachment($uid, $part, null, true);
exit;
}
}
}
$base_uri = DAVBackend::abs_url(array(
CalDAV\Plugin::CALENDAR_ROOT,
preg_replace('!principals/!', '', $this->calendars[$calendarId]['principaluri']),
$calendarId,
$event['uid'] . '.ics',
));
// default response
return array(
'id' => $event['uid'],
'uri' => $event['uid'] . '.ics',
'lastmodified' => $event['changed']->format('U'),
'calendarid' => $calendarId,
'calendardata' => $this->_to_ical($event, $base_uri, $storage),
'etag' => self::_get_etag($event),
);
}
return array();
}
/**
* Creates a new calendar object.
*
* It is possible return an etag from this function, which will be used in
* the response to this PUT request. Note that the ETag must be surrounded
* by double-quotes.
*
* However, you should only really return this ETag if you don't mangle the
* calendar-data. If the result of a subsequent GET to this object is not
* the exact same as this request body, you should omit the ETag.
*
* @param mixed $calendarId
* @param string $objectUri
* @param string $calendarData
* @return string|null
*/
public function createCalendarObject($calendarId, $objectUri, $calendarData)
{
console(__METHOD__, $calendarId, $objectUri, $calendarData);
$uid = basename($objectUri, '.ics');
$storage = $this->get_storage_folder($calendarId);
$object = $this->parse_calendar_data($calendarData, $uid);
if ($object['uid'] == $uid) {
$success = $storage->save($object, 'event');
if (!$success) {
rcube::raise_error(array(
'code' => 600, 'type' => 'php',
'file' => __FILE__, 'line' => __LINE__,
'message' => "Error saving event object to Kolab server"),
true, false);
+
+ throw new DAV\Exception('Error saving event object to backend');
}
}
else {
rcube::raise_error(array(
'code' => 600, 'type' => 'php',
'file' => __FILE__, 'line' => __LINE__,
'message' => "Error creating calendar object: UID doesn't match object URI"),
true, false);
+
+ throw new DAV\Exception\NotFound("UID doesn't match object URI");
}
// return new Etag
return $success ? self::_get_etag($object) : null;
}
/**
* Updates an existing calendarobject, based on it's uri.
*
* It is possible return an etag from this function, which will be used in
* the response to this PUT request. Note that the ETag must be surrounded
* by double-quotes.
*
* However, you should only really return this ETag if you don't mangle the
* calendar-data. If the result of a subsequent GET to this object is not
* the exact same as this request body, you should omit the ETag.
*
* @param mixed $calendarId
* @param string $objectUri
* @param string $calendarData
* @return string|null
*/
public function updateCalendarObject($calendarId, $objectUri, $calendarData)
{
console(__METHOD__, $calendarId, $objectUri, $calendarData);
$uid = basename($objectUri, '.ics');
$storage = $this->get_storage_folder($calendarId);
$object = $this->parse_calendar_data($calendarData, $uid);
// sanity check
if ($object['uid'] != $uid) {
rcube::raise_error(array(
'code' => 600, 'type' => 'php',
'file' => __FILE__, 'line' => __LINE__,
'message' => "Error creating calendar object: UID doesn't match object URI"),
true, false);
- return null;
+ throw new DAV\Exception\NotFound("UID doesn't match object URI");
}
// copy meta data (starting with _) from old object
$old = $storage->get_object($uid);
foreach ((array)$old as $key => $val) {
if (!isset($object[$key]) && $key[0] == '_')
$object[$key] = $val;
}
// TODO: remove attachments not listed anymore
// save object
$saved = $storage->save($object, 'event', $uid);
if (!$saved) {
rcube::raise_error(array(
'code' => 600, 'type' => 'php',
'file' => __FILE__, 'line' => __LINE__,
'message' => "Error saving event object to Kolab server"),
true, false);
- return null;
+ throw new DAV\Exception('Error saving event object to backend');
}
// return new Etag
return self::_get_etag($object);
}
/**
* Deletes an existing calendar object.
*
* @param mixed $calendarId
* @param string $objectUri
* @return void
*/
public function deleteCalendarObject($calendarId, $objectUri)
{
console(__METHOD__, $calendarId, $objectUri);
$uid = basename($objectUri, '.ics');
if ($storage = $this->get_storage_folder($calendarId)) {
$storage->delete($uid);
}
}
/**
* Performs a calendar-query on the contents of this calendar.
*
* The calendar-query is defined in RFC4791 : CalDAV. Using the
* calendar-query it is possible for a client to request a specific set of
* object, based on contents of iCalendar properties, date-ranges and
* iCalendar component types (VTODO, VEVENT).
*
* This method should just return a list of (relative) urls that match this
* query.
*
* The list of filters are specified as an array. The exact array is
* documented by Sabre\CalDAV\CalendarQueryParser.
*
* Note that it is extremely likely that getCalendarObject for every path
* returned from this method will be called almost immediately after. You
* may want to anticipate this to speed up these requests.
*
* Requests that are extremely common are:
* * requests for just VEVENTS
* * requests for just VTODO
* * requests with a time-range-filter on either VEVENT or VTODO.
*
* ..and combinations of these requests. It may not be worth it to try to
* handle every possible situation and just rely on the (relatively
* easy to use) CalendarQueryValidator to handle the rest.
*
* Note that especially time-range-filters may be difficult to parse. A
* time-range filter specified on a VEVENT must for instance also handle
* recurrence rules correctly.
* A good example of how to interprete all these filters can also simply
* be found in Sabre\CalDAV\CalendarQueryFilter. This class is as correct
* as possible, so it gives you a good idea on what type of stuff you need
* to think of.
*
* @param mixed $calendarId
* @param array $filters
* @return array
*/
public function calendarQuery($calendarId, array $filters)
{
console(__METHOD__, $calendarId, $filters);
// build kolab storage query from $filters
$query = array();
foreach ((array)$filters['comp-filters'] as $filter) {
if ($filter['name'] != 'VEVENT')
continue;
if (is_array($filter['time-range'])) {
$query[] = array('dtstart', '<=', $filter['time-range']['end']);
$query[] = array('dtend', '>=', $filter['time-range']['start']);
}
}
$results = array();
if ($storage = $this->get_storage_folder($calendarId)) {
foreach ((array)$storage->select($query) as $event) {
// TODO: cache the already fetched events in memory (really?)
$results[] = $event['uid'] . '.ics';
}
}
return $results;
}
/**
* Set User-Agent string of the connected client
*/
public function setUserAgent($uastring)
{
$ua_classes = array(
'ical' => 'iCal/\d',
'outlook' => 'iCal4OL/\d',
'lightning' => 'Lightning/\d',
);
foreach ($ua_classes as $class => $regex) {
if (preg_match("!$regex!", $uastring)) {
$this->useragent = $class;
break;
}
}
}
/********** Data conversion utilities ***********/
private $attendee_keymap = array('name' => 'CN', 'status' => 'PARTSTAT', 'role' => 'ROLE', 'cutype' => 'CUTYPE', 'rsvp' => 'RSVP');
/**
* Parse the given iCal string into a hash array kolab_format_event can handle
*
* @param string iCal data block
* @return array Hash array with event properties or null on failure
*/
private function parse_calendar_data($calendarData, $uid)
{
try {
// use already parsed object
if (Plugin::$parsed_vevent && Plugin::$parsed_vevent->UID == $uid) {
$vobject = Plugin::$parsed_vcalendar;
$vevent = Plugin::$parsed_vevent;
}
else {
$vobject = VObject\Reader::read($calendarData, VObject\Reader::OPTION_FORGIVING | VObject\Reader::OPTION_IGNORE_INVALID_LINES);
if ($vobject->name == 'VCALENDAR') {
foreach ($vobject->getBaseComponents('VEVENT') as $ve) {
$vevent = $ve;
break;
}
}
}
// convert the VEvent object into a hash array
if ($vevent && $vevent->name == 'VEVENT') {
$object = $this->_to_array($vevent);
if (!empty($object['uid'])) {
// parse recurrence exceptions
if ($object['recurrence']) {
foreach ($vobject->children as $i => $component) {
if ($component->name == 'VEVENT' && isset($component->{'RECURRENCE-ID'})) {
$object['recurrence']['EXCEPTIONS'][] = $this->_to_array($component);
}
}
}
return $object;
}
}
}
catch (VObject\ParseException $e) {
rcube::raise_error(array(
'code' => 600, 'type' => 'php',
'file' => __FILE__, 'line' => __LINE__,
'message' => "iCal data parse error: " . $e->getMessage()),
true, false);
}
return null;
}
/**
* Convert the given Sabre\VObject\Component\Vevent object to a libkolab compatible event format
*
* @param object Vevent object to convert
* @return array Hash array with event properties
* @TODO: move this to libcalendaring for common use
*/
private function _to_array($ve)
{
$event = array(
'uid' => strval($ve->UID),
'title' => strval($ve->SUMMARY),
'created' => $ve->CREATED ? $ve->CREATED->getDateTime() : null,
'changed' => $ve->DTSTAMP->getDateTime(),
'start' => VObjectUtils::convert_datetime($ve->DTSTART),
'end' => VObjectUtils::convert_datetime($ve->DTEND),
// set defaults
'free_busy' => 'busy',
'priority' => 0,
'attendees' => array(),
);
// check for all-day dates
if ($event['start']->_dateonly) {
$event['allday'] = true;
}
if ($event['allday'] && is_object($event['end'])) {
$event['end']->sub(new \DateInterval('PT23H'));
}
// map other attributes to internal fields
$_attendees = array();
foreach ($ve->children as $prop) {
if (!($prop instanceof VObject\Property))
continue;
switch ($prop->name) {
case 'TRANSP':
$event['free_busy'] = $prop->value == 'TRANSPARENT' ? 'free' : 'busy';
break;
case 'STATUS':
if ($prop->value == 'TENTATIVE')
$event['free_busy'] = 'tentative';
else if ($prop->value == 'cancelled')
$event['cancelled'] = true;
break;
case 'PRIORITY':
if (is_numeric($prop->value))
$event['priority'] = $prop->value;
break;
case 'RRULE':
$params = array();
// parse recurrence rule attributes
foreach (explode(';', $prop->value) as $par) {
list($k, $v) = explode('=', $par);
$params[$k] = $v;
}
if ($params['UNTIL'])
$params['UNTIL'] = date_create($params['UNTIL']);
if (!$params['INTERVAL'])
$params['INTERVAL'] = 1;
$event['recurrence'] = $params;
break;
case 'EXDATE':
$event['recurrence']['EXDATE'] = array_merge((array)$event['recurrence']['EXDATE'], (array)VObjectUtils::convert_datetime($prop));
break;
case 'RECURRENCE-ID':
// $event['recurrence_id'] = VObjectUtils::convert_datetime($prop);
break;
case 'SEQUENCE':
$event['sequence'] = intval($prop->value);
break;
case 'DESCRIPTION':
case 'LOCATION':
case 'URL':
$event[strtolower($prop->name)] = $prop->value;
break;
case 'CATEGORY':
case 'CATEGORIES':
$event['categories'] = $prop->getParts();
break;
case 'CLASS':
case 'X-CALENDARSERVER-ACCESS':
$event['sensitivity'] = strtolower($prop->value);
break;
case 'X-MICROSOFT-CDO-BUSYSTATUS':
if ($prop->value == 'OOF')
$event['free_busy'] == 'outofoffice';
else if (in_array($prop->value, array('FREE', 'BUSY', 'TENTATIVE')))
$event['free_busy'] = strtolower($prop->value);
break;
case 'ATTENDEE':
case 'ORGANIZER':
$params = array();
foreach ($prop->parameters as $param) {
switch ($param->name) {
case 'RSVP': $params[$param->name] = strtolower($param->value) == 'true'; break;
default: $params[$param->name] = $param->value; break;
}
}
$attendee = VObjectUtils::map_keys($params, array_flip($this->attendee_keymap));
$attendee['email'] = preg_replace('/^mailto:/i', '', $prop->value);
if ($prop->name == 'ORGANIZER') {
$attendee['status'] = 'ACCEPTED';
$event['organizer'] = $attendee;
}
else if ($attendee['email'] != $event['organizer']['email']) {
$event['attendees'][] = $attendee;
}
break;
case 'ATTACH':
if (substr($prop->value, 0, 4) == 'http' && !strpos($prop->value, ':attachment:')) {
$event['links'][] = $prop->value;
}
break;
default:
if (substr($prop->name, 0, 2) == 'X-')
$event['x-custom'][] = array($prop->name, strval($prop->value));
break;
}
}
// find alarms
if ($valarms = $ve->select('VALARM')) {
$action = 'DISPLAY';
$trigger = null;
$valarm = reset($valarms);
foreach ($valarm->children as $prop) {
switch ($prop->name) {
case 'TRIGGER':
foreach ($prop->parameters as $param) {
if ($param->name == 'VALUE' && $param->value == 'DATE-TIME') {
$trigger = '@' . $prop->getDateTime()->format('U');
}
}
if (!$trigger) {
$trigger = preg_replace('/PT/', '', $prop->value);
}
break;
case 'ACTION':
$action = $prop->value;
break;
}
}
if ($trigger)
$event['alarms'] = $trigger . ':' . $action;
}
return $event;
}
/**
* Build a valid iCal format block from the given event
*
* @param array Hash array with event properties from libkolab
* @param string Absolute URI referenceing this event object
* @param object RECURRENCE-ID property when serializing a recurrence exception
* @return mixed VCALENDAR string containing the VEVENT data
* or VObject\VEvent object with a recurrence exception instance
* @TODO: move this to libcalendaring for common use
*/
private function _to_ical($event, $base_uri, $storage, $recurrence_id = null)
{
$ve = VObject\Component::create('VEVENT');
$ve->add('UID', $event['uid']);
if (!empty($event['created']))
$ve->add(VObjectUtils::datetime_prop('CREATED', $event['created'], true));
if (!empty($event['changed']))
$ve->add(VObjectUtils::datetime_prop('DTSTAMP', $event['changed'], true));
$ve->add(VObjectUtils::datetime_prop('DTSTART', $event['start'], false));
$ve->add(VObjectUtils::datetime_prop('DTEND', $event['end'], false));
if ($recurrence_id)
$ve->add($recurrence_id);
$ve->add('SUMMARY', $event['title']);
if ($event['location'])
$ve->add('LOCATION', $event['location']);
if ($event['description'])
$ve->add('DESCRIPTION', strtr($event['description'], array("\r\n" => "\n", "\r" => "\n"))); // normalize line endings
if ($event['sequence'])
$ve->add('SEQUENCE', $event['sequence']);
if ($event['recurrence'] && !$recurrence_id) {
if ($exdates = $event['recurrence']['EXDATE']) {
unset($event['recurrence']['EXDATE']); // don't serialize EXDATEs into RRULE value
}
$ve->add('RRULE', libcalendaring::to_rrule($event['recurrence']));
// add EXDATEs each one per line (for Thunderbird Lightning)
if ($exdates) {
foreach ($exdates as $ex) {
if ($ex instanceof \DateTime) {
$exd = clone $event['start'];
$exd->setDate($ex->format('Y'), $ex->format('n'), $ex->format('j'));
$exd->setTimeZone(new \DateTimeZone('UTC'));
$ve->add(new VObject\Property('EXDATE', $exd->format('Ymd\\THis\\Z')));
}
}
}
}
if ($event['categories']) {
$cat = VObject\Property::create('CATEGORIES');
$cat->setParts((array)$event['categories']);
$ve->add($cat);
}
$ve->add('TRANSP', $event['free_busy'] == 'free' ? 'TRANSPARENT' : 'OPAQUE');
if ($event['priority'])
$ve->add('PRIORITY', $event['priority']);
if ($event['cancelled'])
$ve->add('STATUS', 'CANCELLED');
else if ($event['free_busy'] == 'tentative')
$ve->add('STATUS', 'TENTATIVE');
if (!empty($event['sensitivity']))
$ve->add('CLASS', strtoupper($event['sensitivity']));
if ($event['alarms']) {
$va = VObject\Component::create('VALARM');
list($trigger, $va->action) = explode(':', $event['alarms']);
$val = libcalendaring::parse_alaram_value($trigger);
if ($val[1]) $va->add('TRIGGER', preg_replace('/^([-+])(.+)/', '\\1PT\\2', $trigger));
else $va->add('TRIGGER', gmdate('Ymd\THis\Z', $val[0]), array('VALUE' => 'DATE-TIME'));
$ve->add($va);
}
if ($event['organizer']) {
unset($event['organizer']['rsvp'], $event['organizer']['role']);
$ve->add('ORGANIZER', 'mailto:' . $event['organizer']['email'], VObjectUtils::map_keys($event['organizer'], $this->attendee_keymap));
}
foreach ((array)$event['attendees'] as $attendee) {
if ($event['organizer'] && $attendee['role'] == 'ORGANIZER')
continue;
$attendee['rsvp'] = $attendee['rsvp'] ? 'TRUE' : null;
$ve->add('ATTENDEE', 'mailto:' . $attendee['email'], VObjectUtils::map_keys($attendee, $this->attendee_keymap));
}
foreach ((array)$event['_attachments'] as $attachment) {
if ($this->useragent == 'ical') {
// embed attachments for iCal
$ve->add('ATTACH',
base64_encode($storage->get_attachment($event['uid'], $attachment['id'])),
array('FMTTYPE' => $attachment['mimetype'], 'ENCODING' => 'BASE64', 'VALUE' => 'BINARY'));
}
else {
// list attachments as absolute URIs
$ve->add('ATTACH',
$base_uri . ':attachment:' . $attachment['id'] . ':' . urlencode($attachment['name']),
array('FMTTYPE' => $attachment['mimetype'], 'VALUE' => 'URI'));
}
}
foreach ((array)$event['url'] as $url) {
$ve->add('URL', $url);
}
foreach ((array)$event['links'] as $uri) {
$ve->add('ATTACH', $uri);
}
// add custom properties
foreach ((array)$event['x-custom'] as $prop) {
$ve->add($prop[0], $prop[1]);
}
// we're dealing with a recurrence exception here, so no final serialization is desired
if ($recurrence_id)
return $ve;
// encapsulate in VCALENDAR container
$vcal = VObject\Component::create('VCALENDAR');
$vcal->version = '2.0';
$vcal->prodid = '-//Kolab DAV Server ' .KOLAB_DAV_VERSION . '//Sabre//Sabre VObject ' . CalDAV\Version::VERSION . '//EN';
$vcal->calscale = 'GREGORIAN';
$vcal->add($ve);
// append recurrence exceptions
if ($event['recurrence']['EXCEPTIONS']) {
foreach ($event['recurrence']['EXCEPTIONS'] as $ex) {
$exdate = clone $event['start'];
$exdate->setDate($ex['start']->format('Y'), $ex['start']->format('n'), $ex['start']->format('j'));
$recurrence_id = VObjectUtils::datetime_prop('RECURRENCE-ID', $exdate);
// if ($ex['thisandfuture']) // not supported by any client :-(
// $recurrence_id->add('RANGE', 'THISANDFUTURE');
$vcal->add($this->_to_ical($ex, $base_uri, $storage, $recurrence_id));
}
}
return $vcal->serialize();
}
/**
* Generate an Etag string from the given event data
*
* @param array Hash array with event properties from libkolab
* @return string Etag string
*/
private static function _get_etag($event)
{
return sprintf('"%s-%d"', substr(md5($event['uid']), 0, 16), $event['_msguid']);
}
}
diff --git a/lib/Kolab/CardDAV/ContactsBackend.php b/lib/Kolab/CardDAV/ContactsBackend.php
index 8ed0df0..85efb3d 100644
--- a/lib/Kolab/CardDAV/ContactsBackend.php
+++ b/lib/Kolab/CardDAV/ContactsBackend.php
@@ -1,849 +1,858 @@
<?php
/**
* SabreDAV Contacts backend for Kolab.
*
* @author Thomas Bruederli <bruederli@kolabsys.com>
*
* Copyright (C) 2013, Kolab Systems AG <contact@kolabsys.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program 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 Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
namespace Kolab\CardDAV;
use \rcube;
use \rcube_charset;
use \kolab_storage;
use Sabre\DAV;
use Sabre\CardDAV;
use Sabre\VObject;
use Kolab\Utils\DAVBackend;
use Kolab\Utils\VObjectUtils;
/**
* Kolab Contacts backend.
*
* Checkout the Sabre\CardDAV\Backend\BackendInterface for all the methods that must be implemented.
*/
class ContactsBackend extends CardDAV\Backend\AbstractBackend
{
private $sources;
private $folders;
private $aliases;
private $useragent;
/**
* Read available contact folders from server
*/
private function _read_sources()
{
// already read sources
if (isset($this->sources))
return $this->sources;
// get all folders that have "contact" type
$folders = kolab_storage::get_folders('contact');
$this->sources = $this->folders = $this->aliases = array();
foreach (DAVBackend::sort_folders($folders) as $folder) {
$id = DAVBackend::get_uid($folder);
$fdata = $folder->get_imap_data(); // fetch IMAP folder data for CTag generation
$this->folders[$id] = $folder;
$this->sources[$id] = array(
'id' => $id,
'uri' => $id,
'{DAV:}displayname' => html_entity_decode($folder->get_name(), ENT_COMPAT, RCUBE_CHARSET),
'{http://calendarserver.org/ns/}getctag' => sprintf('%d-%d-%d', $fdata['UIDVALIDITY'], $fdata['HIGHESTMODSEQ'], $fdata['UIDNEXT']),
'{urn:ietf:params:xml:ns:caldav}supported-address-data' => new CardDAV\Property\SupportedAddressData(),
);
$this->aliases[$folder->name] = $id;
// map default folder to the magic 'all' resource
if ($folder->default)
- $this->folders['__all__'] = $folder;
+ $this->aliases['__all__'] = $id;
}
return $this->sources;
}
/**
* Getter for a kolab_storage_folder representing the address book for the given ID
*
* @param string Folder ID
* @return object kolab_storage_folder instance
*/
public function get_storage_folder($id)
{
// resolve alias name
if ($this->aliases[$id]) {
$id = $this->aliases[$id];
}
if ($this->folders[$id]) {
return $this->folders[$id];
}
else {
return DAVBackend::get_storage_folder($id, 'contact');
}
}
/**
* Returns the list of addressbooks for a specific user.
*
* @param string $principalUri
* @return array
*/
public function getAddressBooksForUser($principalUri)
{
console(__METHOD__, $principalUri, $this->useragent);
$this->_read_sources();
// special case for the apple address book which only supports one (!) address book
if ($this->useragent == 'macosx' && count($this->sources) > 1) {
- $ctags = array('f');
- foreach ($this->sources as $source) {
- $ctags[] = $source['{http://calendarserver.org/ns/}getctag'];
- }
-
- return array(array(
- 'id' => '__all__',
- 'uri' => '__all__',
- '{DAV:}displayname' => 'All',
- '{http://calendarserver.org/ns/}getctag' => join(':', $ctags),
- '{urn:ietf:params:xml:ns:caldav}supported-address-data' => new CardDAV\Property\SupportedAddressData(),
- 'principaluri' => $principalUri,
- ));
+ $source = $this->getAddressBookByName('__all__');
+ $source['principaluri'] = $principalUri;
+ return array($source);
}
$addressBooks = array();
foreach ($this->sources as $id => $source) {
$source['principaluri'] = $principalUri;
$addressBooks[] = $source;
}
return $addressBooks;
}
/**
* Returns properties for a specific node identified by name/uri
*
* @param string Node name/uri
- * @return array Hash array with address book properties or null if not found
+ * @return array Hash array with addressbook properties or null if not found
*/
public function getAddressBookByName($addressBookUri)
{
console(__METHOD__, $addressBookUri);
$this->_read_sources();
$id = $addressBookUri;
- // resolve aliases (calendar by folder name)
+ // return the magic *single* address book for Apple's Address Book App
+ if ($id == '__all__') {
+ $ctags = array();
+ foreach ($this->sources as $source) {
+ $ctags[] = $source['{http://calendarserver.org/ns/}getctag'];
+ }
+
+ return array(
+ 'id' => '__all__',
+ 'uri' => '__all__',
+ '{DAV:}displayname' => 'All',
+ '{http://calendarserver.org/ns/}getctag' => join(':', $ctags),
+ '{urn:ietf:params:xml:ns:caldav}supported-address-data' => new CardDAV\Property\SupportedAddressData(),
+ );
+ }
+
+ // resolve aliases (addressbook by folder name)
if ($this->aliases[$addressBookUri]) {
$id = $this->aliases[$addressBookUri];
}
return $this->sources[$id];
}
/**
* Updates an addressbook's properties
*
* See Sabre\DAV\IProperties for a description of the mutations array, as
* well as the return value.
*
* @param mixed $addressBookId
* @param array $mutations
* @see Sabre\DAV\IProperties::updateProperties
* @return bool|array
*/
public function updateAddressBook($addressBookId, array $mutations)
{
console(__METHOD__, $addressBookId, $mutations);
if ($addressBookId == '__all__')
return false;
$folder = $this->get_storage_folder($addressBookId);
return $folder ? DAVBackend::folder_update($folder, $mutations) : false;
}
/**
* Creates a new address book
*
* @param string $principalUri
* @param string $url Just the 'basename' of the url.
* @param array $properties
* @return void
*/
public function createAddressBook($principalUri, $url, array $properties)
{
console(__METHOD__, $principalUri, $url, $properties);
return DAVBackend::folder_create('contact', $properties, $url);
}
/**
* Deletes an entire addressbook and all its contents
*
* @param int $addressBookId
* @return void
*/
public function deleteAddressBook($addressBookId)
{
console(__METHOD__, $addressBookId);
if ($addressBookId == '__all__')
return;
$folder = $this->get_storage_folder($addressBookId);
if ($folder && !kolab_storage::folder_delete($folder->name)) {
rcube::raise_error(array(
'code' => 600, 'type' => 'php',
'file' => __FILE__, 'line' => __LINE__,
'message' => "Error deleting calendar folder $folder->name"),
true, false);
}
}
/**
* Returns all cards for a specific addressbook id.
*
* This method should return the following properties for each card:
* * carddata - raw vcard data
* * uri - Some unique url
* * lastmodified - A unix timestamp
* * etag - A unique etag. This must change every time the card changes.
* * size - The size of the card in bytes.
*
* If these last two properties are provided, less time will be spent
* calculating them. If they are specified, you can also ommit carddata.
* This may speed up certain requests, especially with large cards.
*
* @param mixed $addressBookId
* @return array
*/
public function getCards($addressBookId)
{
console(__METHOD__, $addressBookId);
// recursively fetch contacts from all folders
if ($addressBookId == '__all__') {
$cards = array();
foreach ($this->sources as $id => $source) {
$cards = array_merge($cards, $this->getCards($id));
}
return $cards;
}
$query = array();
$cards = array();
if ($storage = $this->get_storage_folder($addressBookId)) {
foreach ((array)$storage->select($query) as $contact) {
$cards[] = array(
'id' => $contact['uid'],
'uri' => $contact['uid'] . '.vcf',
'lastmodified' => $contact['changed']->format('U'),
'etag' => self::_get_etag($contact),
'size' => $contact['_size'],
);
}
}
return $cards;
}
/**
* Returns a specfic card.
*
* The same set of properties must be returned as with getCards. The only
* exception is that 'carddata' is absolutely required.
*
* @param mixed $addressBookId
* @param string $cardUri
* @return array
*/
public function getCard($addressBookId, $cardUri)
{
console(__METHOD__, $addressBookId, $cardUri);
$uid = basename($cardUri, '.vcf');
// search all folders for the given card
if ($addressBookId == '__all__') {
$contact = $this->get_card_by_uid($uid, $storage);
}
else {
$storage = $this->get_storage_folder($addressBookId);
$contact = $storage->get_object($uid);
}
if ($contact) {
- $rights = $this->storage ? $this->storage->get_myrights() : null;
-
return array(
'id' => $contact['uid'],
'uri' => $contact['uid'] . '.vcf',
'lastmodified' => $contact['changed']->format('U'),
'carddata' => $this->_to_vcard($contact),
'etag' => self::_get_etag($contact),
);
}
return array();
}
/**
* Creates a new card.
*
* The addressbook id will be passed as the first argument. This is the
* same id as it is returned from the getAddressbooksForUser method.
*
* The cardUri is a base uri, and doesn't include the full path. The
* cardData argument is the vcard body, and is passed as a string.
*
* It is possible to return an ETag from this method. This ETag is for the
* newly created resource, and must be enclosed with double quotes (that
* is, the string itself must contain the double quotes).
*
* You should only return the ETag if you store the carddata as-is. If a
* subsequent GET request on the same card does not have the same body,
* byte-by-byte and you did return an ETag here, clients tend to get
* confused.
*
* @param mixed $addressBookId
* @param string $cardUri
* @param string $cardData
* @return string|null
*/
public function createCard($addressBookId, $cardUri, $cardData)
{
console(__METHOD__, $addressBookId, $cardUri, $cardData);
$uid = basename($cardUri, '.vcf');
$storage = $this->get_storage_folder($addressBookId);
$object = $this->parse_vcard($cardData, $uid);
if ($object['uid'] == $uid) {
$success = $storage->save($object, 'contact');
if (!$success) {
rcube::raise_error(array(
'code' => 600, 'type' => 'php',
'file' => __FILE__, 'line' => __LINE__,
'message' => "Error saving contact object to Kolab server"),
true, false);
+
+ throw new DAV\Exception('Error saving contact card to backend');
}
}
else {
rcube::raise_error(array(
'code' => 600, 'type' => 'php',
'file' => __FILE__, 'line' => __LINE__,
'message' => "Error creating contact object: UID doesn't match object URI"),
true, false);
+
+ throw new DAV\Exception\NotFound("UID doesn't match object URI");
}
// return new Etag
return $success ? self::_get_etag($object) : null;
}
/**
* Updates a card.
*
* The addressbook id will be passed as the first argument. This is the
* same id as it is returned from the getAddressbooksForUser method.
*
* The cardUri is a base uri, and doesn't include the full path. The
* cardData argument is the vcard body, and is passed as a string.
*
* It is possible to return an ETag from this method. This ETag should
* match that of the updated resource, and must be enclosed with double
* quotes (that is: the string itself must contain the actual quotes).
*
* If you don't return an ETag, you can just return null.
*
* @param mixed $addressBookId
* @param string $cardUri
* @param string $cardData
* @return string|null
*/
public function updateCard($addressBookId, $cardUri, $cardData)
{
console(__METHOD__, $addressBookId, $cardUri, $cardData);
$uid = basename($cardUri, '.vcf');
$object = $this->parse_vcard($cardData, $uid);
// sanity check
if ($object['uid'] != $uid) {
rcube::raise_error(array(
'code' => 600, 'type' => 'php',
'file' => __FILE__, 'line' => __LINE__,
'message' => "Error creating contact object: UID doesn't match object URI"),
true, false);
- return null;
+ throw new DAV\Exception\NotFound("UID doesn't match object URI");
}
if ($addressBookId == '__all__') {
$old = $this->get_card_by_uid($uid, $storage);
}
else {
if ($storage = $this->get_storage_folder($addressBookId))
$old = $storage->get_object($uid);
}
if (!$storage) {
rcube::raise_error(array(
'code' => 600, 'type' => 'php',
'file' => __FILE__, 'line' => __LINE__,
'message' => "Unable to find storage folder for contact $addressBookId/$cardUri"),
true, false);
- return null;
+
+ throw new DAV\Exception\NotFound("Invalid address book URI");
}
if (!$this->is_writeable($storage)) {
throw new DAV\Exception\MethodNotAllowed('Insufficient privileges to update this card');
}
// copy meta data (starting with _) from old object
foreach ((array)$old as $key => $val) {
if (!isset($object[$key]) && $key[0] == '_')
$object[$key] = $val;
}
// save object
$saved = $storage->save($object, 'contact', $uid);
if (!$saved) {
rcube::raise_error(array(
'code' => 600, 'type' => 'php',
'file' => __FILE__, 'line' => __LINE__,
'message' => "Error saving contact object to Kolab server"),
true, false);
- return null;
+ throw new DAV\Exception('Error saving contact card to backend');
}
// return new Etag
return self::_get_etag($object);
}
/**
* Deletes a card
*
* @param mixed $addressBookId
* @param string $cardUri
* @return bool
*/
public function deleteCard($addressBookId, $cardUri)
{
console(__METHOD__, $addressBookId, $cardUri);
// TODO: implement this
}
/**
* Set User-Agent string of the connected client
*/
public function setUserAgent($uastring)
{
$ua_classes = array(
'thunderbird' => 'Thunderbird/\d',
'macosx' => 'AddressBook/\d.+\sCardDAVPlugin',
);
foreach ($ua_classes as $class => $regex) {
if (preg_match("!$regex!", $uastring)) {
$this->useragent = $class;
break;
}
}
}
/**
* Find an object and the containing folder by UID
*
* @param string Object UID
* @param object Return parameter for the kolab_storage_folder instance
* @return array|false
*/
private function get_card_by_uid($uid, &$storage)
{
$obj = kolab_storage::get_object($uid, 'contact');
if ($obj) {
- $storage = kolab_storage::get_folder($obj['_mbox']);
+ $storage = kolab_storage::get_folder($obj['_mailbox']);
return $obj;
}
return false;
}
/**
* Internal helper method to determine whether the given kolab_storage_folder is writeable
*
*/
private function is_writeable($storage)
{
$rights = $storage->get_myrights();
return (strpos($rights, 'i') !== false || $storage->get_namespace() == 'personal');
}
/********** Data conversion utilities ***********/
private $phonetypes = array(
'main' => 'voice',
'homefax' => 'fax',
'workfax' => 'fax',
'mobile' => 'cell',
'other' => 'textphone',
);
private $improtocols = array(
'jabber' => 'xmpp',
);
/**
* Parse the given VCard string into a hash array kolab_format_contact can handle
*
* @param string VCard data block
* @return array Hash array with contact properties or null on failure
*/
private function parse_vcard($cardData, $uid)
{
try {
// use already parsed object
if (Plugin::$parsed_vcard && Plugin::$parsed_vcard->UID == $uid) {
$vobject = Plugin::$parsed_vcard;
}
else {
VObject\Property::$classMap['REV'] = 'Sabre\\VObject\\Property\\DateTime';
$vobject = VObject\Reader::read($cardData, VObject\Reader::OPTION_FORGIVING | VObject\Reader::OPTION_IGNORE_INVALID_LINES);
}
if ($vobject && $vobject->name == 'VCARD') {
$contact = $this->_to_array($vobject);
if (!empty($contact['uid'])) {
return $contact;
}
}
}
catch (VObject\ParseException $e) {
rcube::raise_error(array(
'code' => 600, 'type' => 'php',
'file' => __FILE__, 'line' => __LINE__,
'message' => "VCard data parse error: " . $e->getMessage()),
true, false);
}
return null;
}
/**
* Build a valid VCard format block from the given contact record
*
* @param array Hash array with contact properties from libkolab
* @return string VCARD string containing the contact data
*/
private function _to_vcard($contact)
{
$vc = VObject\Component::create('VCARD');
$vc->version = '3.0';
$vc->prodid = '-//Kolab DAV Server ' .KOLAB_DAV_VERSION . '//Sabre//Sabre VObject ' . CardDAV\Version::VERSION . '//EN';
$vc->add('UID', $contact['uid']);
$vc->add('FN', $contact['name']);
$n = VObject\Property::create('N');
$n->setParts(array($contact['surname'], $contact['firstname'], $contact['middlename'], $contact['prefix'], $contact['suffix']));
$vc->add($n);
if (!empty($contact['nickname']))
$vc->add('NICKNAME', $contact['nickname']);
if (!empty($contact['jobtitle']))
$vc->add('TITLE', $contact['jobtitle']);
if (!empty($contact['profession']))
$vc->add('X-PROFESSION', $contact['profession']);
if (!empty($contact['organization']) || !empty($contact['department'])) {
$org = VObject\Property::create('ORG');
$org->setParts(array($contact['organization'], $contact['department']));
$vc->add($org);
}
// TODO: save as RELATED
if (!empty($contact['assistant']))
$vc->add('X-ASSISTANT', join(',', (array)$contact['assistant']));
if (!empty($contact['manager']))
$vc->add('X-MANAGER', join(',', (array)$contact['manager']));
if (!empty($contact['spouse']))
$vc->add('X-SPOUSE', $contact['spouse']);
if (!empty($contact['children']))
$vc->add('X-CHILDREN', join(',', (array)$contact['children']));
foreach ((array)$contact['email'] as $email) {
$vc->add('EMAIL', $email['address'], array('type' => rtrim('INTERNET,' . strtoupper($email['type']), ',')));
}
foreach ((array)$contact['phone'] as $phone) {
$type = $this->phonetypes[$phone['type']] ?: $phone['type'];
$vc->add('TEL', $phone['number'], array('type' => strtoupper($type)));
}
foreach ((array)$contact['website'] as $website) {
$vc->add('URL', $website['url'], array('type' => strtoupper($website['type'])));
}
$improtocolmap = array_flip($this->improtocols);
foreach ((array)$contact['im'] as $im) {
list($prot, $val) = explode(':', $im);
if ($val) $vc->add('x-' . ($improtocolmap[$prot] ?: $prot), $val);
else $vc->add('IMPP', $im);
}
foreach ((array)$contact['address'] as $adr) {
$vadr = VObject\Property::create('ADR', null, array('type' => strtoupper($adr['type'])));
$vadr->setParts(array('','', $adr['street'], $adr['locality'], $adr['region'], $adr['code'], $adr['country']));
$vc->add($vadr);
}
if (!empty($contact['notes']))
$vc->add('NOTE', $contact['notes']);
if (!empty($contact['gender']))
$vc->add('SEX', $contact['gender']);
if (!empty($contact['birthday']) && $contact['birthday'] instanceof \DateTime) {
// FIXME: Date values are ignored by Thunderbird
$contact['birthday']->_dateonly = true;
$vc->add(VObjectUtils::datetime_prop('BDAY', $contact['birthday'], false));
}
if (!empty($contact['anniversary']) && $contact['birthday'] instanceof \DateTime) {
$contact['anniversary']->_dateonly = true;
$vc->add(VObjectUtils::datetime_prop('ANNIVERSARY', $contact['anniversary'], false));
}
if (!empty($contact['categories'])) {
$cat = VObject\Property::create('CATEGORIES');
$cat->setParts((array)$contact['categories']);
$vc->add($cat);
}
if (!empty($contact['freebusyurl']))
$vc->add('FBURL', $contact['freebusyurl']);
if (!empty($contact['photo'])) {
$vc->PHOTO = base64_encode($contact['photo']);
$vc->PHOTO->add('BASE64', null);
}
// add custom properties
foreach ((array)$contact['x-custom'] as $prop) {
$vc->add($prop[0], $prop[1]);
}
if (!empty($contact['changed']))
$vc->add(VObjectUtils::datetime_prop('REV', $contact['changed'], true));
return $vc->serialize();
}
/**
* Convert the given Sabre\VObject\Component\Vcard object to a libkolab compatible contact format
*
* @param object Vcard object to convert
* @return array Hash array with contact properties
*/
private function _to_array($vc)
{
$contact = array(
'uid' => strval($vc->UID),
'name' => strval($vc->FN),
);
if ($vc->REV) {
try { $contact['changed'] = $vc->REV->getDateTime(); }
catch (\Exception $e) {
try { $contact['changed'] = new \DateTime(strval($vc->REV)); }
catch (\Exception $e) { }
}
}
$phonetypemap = array_flip($this->phonetypes);
// map attributes to internal fields
foreach ($vc->children as $prop) {
if (!($prop instanceof VObject\Property))
continue;
switch ($prop->name) {
case 'N':
list($contact['surname'], $contact['firstname'], $contact['middlename'], $contact['prefix'], $contact['suffix']) = $prop->getParts();
break;
case 'NOTE':
$contact['notes'] = $prop->value;
break;
case 'TITLE':
case 'NICKNAME':
$contact[strtolower($prop->name)] = $prop->value;
break;
case 'ORG':
list($contact['organization'], $contact['department']) = $prop->getParts();
break;
case 'CATEGORY':
case 'CATEGORIES':
$contact['categories'] = $prop->getParts();
break;
case 'EMAIL':
$types = array_values(self::array_filter($prop->offsetGet('type'), 'internet,pref', true));
$contact['email'][] = array('address' => $prop->value, 'type' => strtolower($types[0] ?: 'other'));
break;
case 'URL':
$types = array_values(self::array_filter($prop->offsetGet('type'), 'internet,pref', true));
$contact['website'][] = array('url' => $prop->value, 'type' => strtolower($types[0]));
break;
case 'TEL':
$types = array_values(self::array_filter($prop->offsetGet('type'), 'internet,pref', true));
$type = strtolower($types[0]);
$contact['phone'][] = array('number' => $prop->value, 'type' => $phonetypemap[$type] ?: $type);
break;
case 'ADR':
$type = $prop->offsetGet('type');
$adr = array('type' => strtolower($type));
list(,, $adr['street'], $adr['locality'], $adr['region'], $adr['code'], $adr['country']) = $prop->getParts();
$contact['address'][] = $adr;
break;
case 'BDAY':
$contact['birthday'] = new \DateTime($prop->value);
$contact['birthday']->_dateonly = true;
break;
case 'ANNIVERSARY':
case 'X-ANNIVERSARY':
$contact['anniversary'] = new \DateTime($prop->value);
$contact['anniversary']->_dateonly = true;
break;
case 'SEX':
case 'X-GENDER':
$contact['gender'] = $prop->value;
break;
case 'X-PROFESSION':
case 'X-SPOUSE':
$contact[strtolower(substr($prop->name, 2))] = $prop->value;
break;
case 'X-MANAGER':
case 'X-ASSISTANT':
case 'X-CHILDREN':
$contact[strtolower(substr($prop->name, 2))] = explode(',', $prop->value);
break;
case 'X-JABBER':
case 'X-ICQ':
case 'X-MSN':
case 'X-AIM':
case 'X-YAHOO':
case 'X-SKYPE':
$protocol = strtolower(substr($prop->name, 2));
$contact['im'][] = ($this->improtocols[$protocol] ?: $protocol) . ':' . preg_replace('/^[a-z]+:/i', '', $prop->value);
break;
case 'IMPP':
$type = strtolower((string)$prop->offsetGet('X-SERVICE-TYPE'));
$protocol = $type && !preg_match('/^[a-z]+:/i', $prop->value) ? ($this->improtocols[$type] ?: $type) . ':' : '';
$contact['im'][] = $protocol . urldecode($prop->value);
break;
case 'PHOTO':
$param = $prop->parameters[0];
if ($param->value && strtolower($param->value) == 'b' || strtolower($param->name) == 'base64') {
$contact['photo'] = base64_decode($prop->value);
}
break;
case 'CUSTOM1':
case 'CUSTOM2':
case 'CUSTOM3':
case 'CUSTOM4':
default:
if (substr($prop->name, 0, 2) == 'X-' || substr($prop->name, 0, 6) == 'CUSTOM')
$contact['x-custom'][] = array($prop->name, strval($prop->value));
break;
}
}
if (is_array($contact['im']))
$contact['im'] = array_unique($contact['im']);
return $contact;
}
/**
* Extract array values by a filter
*
* @param array Array to filter
* @param keys Array or comma separated list of values to keep
* @param boolean Invert key selection: remove the listed values
*
* @return array The filtered array
*/
private static function array_filter($arr, $values, $inverse = false)
{
if (!is_array($values)) {
$values = explode(',', $values);
}
$result = array();
$keep = array_flip((array)$values);
if (!empty($arr)) {
foreach ($arr as $key => $val) {
if ($inverse != isset($keep[strtolower($val)])) {
$result[$key] = $val;
}
}
}
return $result;
}
/**
* Generate an Etag string from the given contact data
*
* @param array Hash array with contact properties from libkolab
* @return string Etag string
*/
private static function _get_etag($contact)
{
return sprintf('"%s-%d"', substr(md5($contact['uid']), 0, 16), $contact['_msguid']);
}
}

File Metadata

Mime Type
text/x-diff
Expires
Mon, Aug 25, 9:55 PM (2 h, 40 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
257816
Default Alt Text
(66 KB)

Event Timeline