Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F174703
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Flag For Later
Size
81 KB
Referenced Files
None
Subscribers
None
View Options
diff --git a/lib/kolab_sync_data_calendar.php b/lib/kolab_sync_data_calendar.php
index abf2de6..3d3409a 100644
--- a/lib/kolab_sync_data_calendar.php
+++ b/lib/kolab_sync_data_calendar.php
@@ -1,1095 +1,1095 @@
<?php
/**
+--------------------------------------------------------------------------+
| Kolab Sync (ActiveSync for Kolab) |
| |
| Copyright (C) 2011-2012, 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/> |
+--------------------------------------------------------------------------+
| Author: Aleksander Machniak <machniak@kolabsys.com> |
+--------------------------------------------------------------------------+
*/
/**
* Calendar (Events) data class for Syncroton
*/
class kolab_sync_data_calendar extends kolab_sync_data implements Syncroton_Data_IDataCalendar
{
/**
* Mapping from ActiveSync Calendar namespace fields
*/
protected $mapping = array(
'allDayEvent' => 'allday',
'startTime' => 'start', // keep it before endTime here
//'attendees' => 'attendees',
'body' => 'description',
//'bodyTruncated' => 'bodytruncated',
'busyStatus' => 'free_busy',
//'categories' => 'categories',
'dtStamp' => 'changed',
'endTime' => 'end',
//'exceptions' => 'exceptions',
'location' => 'location',
//'meetingStatus' => 'meetingstatus',
//'organizerEmail' => 'organizeremail',
//'organizerName' => 'organizername',
//'recurrence' => 'recurrence',
//'reminder' => 'reminder',
//'responseRequested' => 'responserequested',
//'responseType' => 'responsetype',
'sensitivity' => 'sensitivity',
'subject' => 'title',
//'timezone' => 'timezone',
'uID' => 'uid',
);
/**
* Kolab object type
*
* @var string
*/
protected $modelName = 'event';
/**
* Type of the default folder
*
* @var int
*/
protected $defaultFolderType = Syncroton_Command_FolderSync::FOLDERTYPE_CALENDAR;
/**
* Default container for new entries
*
* @var string
*/
protected $defaultFolder = 'Calendar';
/**
* Type of user created folders
*
* @var int
*/
protected $folderType = Syncroton_Command_FolderSync::FOLDERTYPE_CALENDAR_USER_CREATED;
/**
* attendee status
*/
const ATTENDEE_STATUS_UNKNOWN = 0;
const ATTENDEE_STATUS_TENTATIVE = 2;
const ATTENDEE_STATUS_ACCEPTED = 3;
const ATTENDEE_STATUS_DECLINED = 4;
const ATTENDEE_STATUS_NOTRESPONDED = 5;
/**
* attendee types
*/
const ATTENDEE_TYPE_REQUIRED = 1;
const ATTENDEE_TYPE_OPTIONAL = 2;
const ATTENDEE_TYPE_RESOURCE = 3;
/**
* busy status constants
*/
const BUSY_STATUS_FREE = 0;
const BUSY_STATUS_TENTATIVE = 1;
const BUSY_STATUS_BUSY = 2;
const BUSY_STATUS_OUTOFOFFICE = 3;
/**
* Sensitivity values
*/
const SENSITIVITY_NORMAL = 0;
const SENSITIVITY_PERSONAL = 1;
const SENSITIVITY_PRIVATE = 2;
const SENSITIVITY_CONFIDENTIAL = 3;
const KEY_DTSTAMP = 'x-custom.X-ACTIVESYNC-DTSTAMP';
const KEY_RESPONSE_DTSTAMP = 'x-custom.X-ACTIVESYNC-RESPONSE-DTSTAMP';
/**
* Mapping of attendee status
*
* @var array
*/
protected $attendeeStatusMap = array(
'UNKNOWN' => self::ATTENDEE_STATUS_UNKNOWN,
'TENTATIVE' => self::ATTENDEE_STATUS_TENTATIVE,
'ACCEPTED' => self::ATTENDEE_STATUS_ACCEPTED,
'DECLINED' => self::ATTENDEE_STATUS_DECLINED,
'DELEGATED' => self::ATTENDEE_STATUS_UNKNOWN,
'NEEDS-ACTION' => self::ATTENDEE_STATUS_NOTRESPONDED,
);
/**
* Mapping of attendee type
*
* NOTE: recurrences need extra handling!
* @var array
*/
protected $attendeeTypeMap = array(
'REQ-PARTICIPANT' => self::ATTENDEE_TYPE_REQUIRED,
'OPT-PARTICIPANT' => self::ATTENDEE_TYPE_OPTIONAL,
// 'NON-PARTICIPANT' => self::ATTENDEE_TYPE_RESOURCE,
// 'CHAIR' => self::ATTENDEE_TYPE_RESOURCE,
);
/**
* Mapping of busy status
*
* @var array
*/
protected $busyStatusMap = array(
'free' => self::BUSY_STATUS_FREE,
'tentative' => self::BUSY_STATUS_TENTATIVE,
'busy' => self::BUSY_STATUS_BUSY,
'outofoffice' => self::BUSY_STATUS_OUTOFOFFICE,
);
/**
* mapping of sensitivity
*
* @var array
*/
protected $sensitivityMap = array(
'public' => self::SENSITIVITY_PERSONAL,
'private' => self::SENSITIVITY_PRIVATE,
'confidential' => self::SENSITIVITY_CONFIDENTIAL,
);
/**
* Appends contact data to xml element
*
* @param Syncroton_Model_SyncCollection $collection Collection data
* @param string $serverId Local entry identifier
* @param boolean $as_array Return entry as array
*
* @return array|Syncroton_Model_Event|array Event object
*/
public function getEntry(Syncroton_Model_SyncCollection $collection, $serverId, $as_array = false)
{
$event = is_array($serverId) ? $serverId : $this->getObject($collection->collectionId, $serverId);
$config = $this->getFolderConfig($event['_mailbox']);
$result = array();
// Timezone
// Kolab Format 3.0 and xCal does support timezone per-date, but ActiveSync allows
// only one timezone per-event. We'll use timezone of the start date
if ($event['start'] instanceof DateTime) {
$timezone = $event['start']->getTimezone();
if ($timezone && ($tz_name = $timezone->getName()) != 'UTC') {
$tzc = kolab_sync_timezone_converter::getInstance();
if ($tz_name = $tzc->encodeTimezone($tz_name)) {
$result['timezone'] = $tz_name;
}
}
}
// Calendar namespace fields
foreach ($this->mapping as $key => $name) {
$value = $this->getKolabDataItem($event, $name);
switch ($name) {
case 'changed':
case 'end':
case 'start':
// For all-day events Kolab uses different times
// At least Android doesn't display such event as all-day event
if ($value && is_a($value, 'DateTime')) {
$date = clone $value;
if ($event['allday']) {
// need this for self::date_from_kolab()
$date->_dateonly = false;
if ($name == 'start') {
$date->setTime(0, 0, 0);
}
else if ($name == 'end') {
$date->setTime(0, 0, 0);
$date->modify('+1 day');
}
}
// set this date for use in recurrence exceptions handling
if ($name == 'start') {
$event['_start'] = $date;
}
$value = self::date_from_kolab($date);
}
break;
case 'sensitivity':
$value = intval($this->sensitivityMap[$value]);
break;
case 'free_busy':
$value = $this->busyStatusMap[$value];
break;
case 'description':
$value = $this->body_from_kolab($value, $collection);
break;
}
// Ignore empty values (but not integer 0)
if ((empty($value) || is_array($value)) && $value !== 0) {
continue;
}
$result[$key] = $value;
}
// Event reminder time
if ($config['ALARMS']) {
$result['reminder'] = $this->from_kolab_alarm($event);
}
$result['categories'] = array();
$result['attendees'] = array();
// Categories, Roundcube Calendar plugin supports only one category at a time
if (!empty($event['categories'])) {
$result['categories'] = (array) $event['categories'];
}
// Organizer
if (!empty($event['attendees'])) {
foreach ($event['attendees'] as $idx => $attendee) {
if ($attendee['role'] == 'ORGANIZER') {
if ($name = $attendee['name']) {
$result['organizerName'] = $name;
}
if ($email = $attendee['email']) {
$result['organizerEmail'] = $email;
}
unset($event['attendees'][$idx]);
break;
}
}
}
// Attendees
if (!empty($event['attendees'])) {
$user_emails = $this->user_emails();
$user_rsvp = false;
foreach ($event['attendees'] as $idx => $attendee) {
$att = array();
if ($email = $attendee['email']) {
$att['email'] = $email;
}
else {
// In Activesync email is required
continue;
}
$att['name'] = $attendee['name'] ?: $email;
$type = isset($attendee['role']) ? $this->attendeeTypeMap[$attendee['role']] : null;
$status = isset($attendee['status']) ? $this->attendeeStatusMap[$attendee['status']] : null;
if ($this->asversion >= 12) {
$att['attendeeType'] = $type ?: self::ATTENDEE_TYPE_REQUIRED;
$att['attendeeStatus'] = $status ?: self::ATTENDEE_STATUS_UNKNOWN;
}
if ($email && in_array_nocase($email, $user_emails)) {
$user_rsvp = !empty($attendee['rsvp']);
$resp_type = $status ?: self::ATTENDEE_STATUS_UNKNOWN;
}
$result['attendees'][] = new Syncroton_Model_EventAttendee($att);
}
}
// Event meeting status
$this->meeting_status_from_kolab($collection, $event, $result);
// Recurrence (and exceptions)
$this->recurrence_from_kolab($collection, $event, $result);
// RSVP status
$result['responseRequested'] = $result['meetingStatus'] == 3 && $user_rsvp ? 1 : 0;
$result['responseType'] = $result['meetingStatus'] == 3 ? $resp_type : null;
return $as_array ? $result : new Syncroton_Model_Event($result);
}
/**
* convert contact from xml to libkolab array
*
* @param Syncroton_Model_IEntry $data Contact to convert
* @param string $folderid Folder identifier
* @param array $entry Existing entry
* @param DateTimeZone $timezone Timezone of the event
*
* @return array
*/
public function toKolab(Syncroton_Model_IEntry $data, $folderid, $entry = null, $timezone = null)
{
$event = !empty($entry) ? $entry : array();
$foldername = isset($event['_mailbox']) ? $event['_mailbox'] : $this->getFolderName($folderid);
$config = $this->getFolderConfig($foldername);
$is_exception = $data instanceof Syncroton_Model_EventException;
$dummy_tz = str_repeat('A', 230) . '==';
$is_outlook = stripos($this->device->devicetype, 'outlook') !== false;
// check data validity
$this->check_event($data);
if (!empty($event['start']) && ($event['start'] instanceof DateTime)) {
$old_timezone = $event['start']->getTimezone();
}
// Timezone
if (!$timezone && isset($data->timezone) && $data->timezone != $dummy_tz) {
$tzc = kolab_sync_timezone_converter::getInstance();
$expected = $old_timezone ?: kolab_format::$timezone;
try {
$timezone = $tzc->getTimezone($data->timezone, $expected->getName());
$timezone = new DateTimeZone($timezone);
}
catch (Exception $e) {
$timezone = null;
}
}
if (empty($timezone)) {
$timezone = $old_timezone ?: new DateTimeZone('UTC');
}
$event['allday'] = 0;
// Calendar namespace fields
foreach ($this->mapping as $key => $name) {
// skip UID field, unsupported in event exceptions
// we need to do this here, because the next line (data getter) will throw an exception
if ($is_exception && $key == 'uID') {
continue;
}
$value = $data->$key;
switch ($name) {
case 'changed':
$value = null;
break;
case 'end':
case 'start':
if ($timezone && $value) {
$value->setTimezone($timezone);
}
if ($value && $data->allDayEvent) {
$value->_dateonly = true;
// In ActiveSync all-day event ends on 00:00:00 next day
// In Kolab we just ignore the time spec.
if ($name == 'end') {
$diff = date_diff($event['start'], $value);
$value = clone $event['start'];
if ($diff->days > 1) {
$value->add(new DateInterval('P' . ($diff->days - 1) . 'D'));
}
}
}
break;
case 'sensitivity':
$map = array_flip($this->sensitivityMap);
$value = $map[$value];
break;
case 'free_busy':
$map = array_flip($this->busyStatusMap);
$value = $map[$value];
break;
case 'description':
$value = $this->getBody($value, Syncroton_Model_EmailBody::TYPE_PLAINTEXT);
// If description isn't specified keep old description
if ($value === null) {
continue 2;
}
break;
}
$this->setKolabDataItem($event, $name, $value);
}
// Try to fix allday events from Android
// It doesn't set all-day flag but the period is a whole day
if (!$event['allday'] && $event['end'] && $event['start']) {
$interval = @date_diff($event['start'], $event['end']);
if ($interval && $interval->format('%y%m%d%h%i%s') === '001000') {
$event['allday'] = 1;
$event['end'] = clone $event['start'];
}
}
// Reminder
// @TODO: should alarms be used when importing event from phone?
if ($config['ALARMS']) {
$event['valarms'] = $this->to_kolab_alarm($data->reminder, $event);
}
$attendees = array();
$categories = array();
// Categories
if (isset($data->categories)) {
foreach ($data->categories as $category) {
$categories[] = $category;
}
}
// Organizer
if (!$is_exception && ($organizer_email = $data->organizerEmail)) {
$attendees[] = array(
'role' => 'ORGANIZER',
'name' => $data->organizerName,
'email' => $organizer_email,
);
}
// Attendees
// Outlook 2013 sends a dummy update just after MeetingResponse has been processed,
// this update resets attendee status set in the MeetingResponse request.
// We ignore changes to attendees data on such updates
if ($is_outlook && $this->isDummyOutlookUpdate($data, $entry, $event)) {
$attendees = $entry['attendees'];
}
else if (isset($data->attendees)) {
$statusMap = array_flip($this->attendeeStatusMap);
foreach ($data->attendees as $attendee) {
if ($attendee->email && $attendee->email == $organizer_email) {
continue;
}
$role = false;
if (isset($attendee->attendeeType)) {
$role = array_search($attendee->attendeeType, $this->attendeeTypeMap);
}
if ($role === false) {
$role = array_search(self::ATTENDEE_TYPE_REQUIRED, $this->attendeeTypeMap);
}
$_attendee = array(
'role' => $role,
'name' => $attendee->name != $attendee->email ? $attendee->name : '',
'email' => $attendee->email,
);
if (isset($attendee->attendeeStatus)) {
$_attendee['status'] = $attendee->attendeeStatus ? array_search($attendee->attendeeStatus, $this->attendeeStatusMap) : null;
if (!$_attendee['status']) {
$_attendee['status'] = 'NEEDS-ACTION';
$_attendee['rsvp'] = true;
}
}
else if (!empty($event['attendees']) && !empty($attendee->email)) {
// copy the old attendee status
foreach ($event['attendees'] as $old_attendee) {
if ($old_attendee['email'] == $_attendee['email'] && isset($old_attendee['status'])) {
$_attendee['status'] = $old_attendee['status'];
$_attendee['rsvp'] = $old_attendee['rsvp'];
break;
}
}
}
$attendees[] = $_attendee;
}
}
// Make sure the event has the organizer set
if (!$organizer_email && ($identity = kolab_sync::get_instance()->user->get_identity())) {
$attendees[] = array(
'role' => 'ORGANIZER',
'name' => $identity['name'],
'email' => $identity['email'],
);
}
$event['attendees'] = $attendees;
$event['categories'] = $categories;
// recurrence (and exceptions)
if (!$is_exception) {
$event['recurrence'] = $this->recurrence_to_kolab($data, $folderid, $timezone);
}
// Bump SEQUENCE number on update (Outlook only).
// It's been confirmed that any change of the event that has attendees specified
// bumps SEQUENCE number of the event (we can see this in sent iTips).
// Unfortunately Outlook also sends an update when no SEQUENCE bump
// is needed, e.g. when updating attendee status.
// We try our best to bump the SEQUENCE only when expected
if (!empty($entry) && !$is_exception && !empty($data->attendees) && $data->timezone != $dummy_tz) {
if ($last_update = $this->getKolabDataItem($event, self::KEY_DTSTAMP)) {
$last_update = new DateTime($last_update);
}
if ($data->dtStamp && $data->dtStamp != $last_update) {
if ($this->has_significant_changes($event, $entry)) {
$event['sequence']++;
$this->logger->debug('Found significant changes in the updated event. Bumping SEQUENCE to ' . $event['sequence']);
}
}
}
// Because we use last event modification time above, we make sure
// the event modification time is not (re)set by the server,
// we use the original Outlook's timestamp.
if ($is_outlook && $data->dtStamp) {
$this->setKolabDataItem($event, self::KEY_DTSTAMP, $data->dtStamp->format(DateTime::ATOM));
}
// This prevents kolab_format code to bump the sequence when not needed
if (!isset($event['sequence'])) {
$event['sequence'] = 0;
}
return $event;
}
/**
* Set attendee status for meeting
*
* @param Syncroton_Model_MeetingResponse $request The meeting response
*
* @return string ID of new calendar entry
*/
public function setAttendeeStatus(Syncroton_Model_MeetingResponse $request)
{
$status_map = array(
1 => 'ACCEPTED',
2 => 'TENTATIVE',
3 => 'DECLINED',
);
if ($status = $status_map[$request->userResponse]) {
// extract event from the invitation
list($event, $existing) = $this->get_event_from_invitation($request);
/*
switch ($status) {
case 'ACCEPTED': $event['free_busy'] = 'busy'; break;
case 'TENTATIVE': $event['free_busy'] = 'tentative'; break;
case 'DECLINED': $event['free_busy'] = 'free'; break;
}
*/
// Store Outlook response timestamp for further use
if (stripos($this->device->devicetype, 'outlook') !== false) {
$dtstamp = new DateTime('now', new DateTimeZone('UTC'));
$dtstamp = $dtstamp->format(DateTime::ATOM);
}
// Update/Save the event
if (empty($existing)) {
if ($dtstamp) {
$this->setKolabDataItem($event, self::KEY_RESPONSE_DTSTAMP, $dtstamp);
}
$folder = $this->save_event($event, $status);
// Create SyncState for the new event, so it is not synced twice
if ($folder) {
$folderId = $this->getFolderId($folder);
try {
$syncBackend = Syncroton_Registry::getSyncStateBackend();
$folderBackend = Syncroton_Registry::getFolderBackend();
$contentBackend = Syncroton_Registry::getContentStateBackend();
$syncFolder = $folderBackend->getFolder($this->device->id, $folderId);
$syncState = $syncBackend->getSyncState($this->device->id, $syncFolder->id);
$contentBackend->create(new Syncroton_Model_Content(array(
'device_id' => $this->device->id,
'folder_id' => $syncFolder->id,
'contentid' => $this->serverId($event['uid'], $folder),
'creation_time' => $syncState->lastsync,
'creation_synckey' => $syncState->counter,
)));
}
catch (Exception $e) {
// ignore
}
}
}
else {
if ($dtstamp) {
$this->setKolabDataItem($existing, self::KEY_RESPONSE_DTSTAMP, $dtstamp);
}
$folder = $this->update_event($event, $existing, $status, $request->instanceId);
}
if (!$folder) {
throw new Syncroton_Exception_Status_MeetingResponse(Syncroton_Exception_Status_MeetingResponse::MEETING_ERROR);
}
// TODO: ActiveSync version >= 16, send the iTip response.
if (isset($request->sendResponse)) {
// SendResponse can contain Body to use as email body (can be empty)
// TODO: Activesync >= 16.1 proposedStartTime and proposedEndTime.
}
}
// FIXME: We should not return an UID when status=DECLINED
// as it's expected by the specification. Server
// should delete an event in such a case, but we
// keep the event copy with appropriate attendee status instead.
return empty($status) ? null : $this->serverId($event['uid'], $folder);
}
/**
* Get an event from the invitation email or calendar folder
*/
protected function get_event_from_invitation(Syncroton_Model_MeetingResponse $request)
{
// Limitation: LongId might be used instead of RequestId, this is not supported
if ($request->requestId) {
$mail_class = new kolab_sync_data_email($this->device, $this->syncTimeStamp);
// Event from an invitation email
if ($event = $mail_class->get_invitation_event($request->requestId)) {
// find the event in calendar
$existing = $this->find_event_by_uid($event['uid']);
return array($event, $existing);
}
// Event from calendar folder
if ($event = $this->getObject($request->collectionId, $request->requestId, $folder)) {
return array($event, $event);
}
throw new Syncroton_Exception_Status_MeetingResponse(Syncroton_Exception_Status_MeetingResponse::INVALID_REQUEST);
}
throw new Syncroton_Exception_Status_MeetingResponse(Syncroton_Exception_Status_MeetingResponse::MEETING_ERROR);
}
/**
* Find the Kolab event in any (of subscribed personal calendars) folder
*/
protected function find_event_by_uid($uid)
{
if (empty($uid)) {
return;
}
// TODO: should we check every existing event folder even if not subscribed for sync?
foreach ($this->listFolders() as $folder) {
$storage_folder = $this->getFolderObject($folder['imap_name']);
if ($storage_folder->get_namespace() == 'personal'
&& ($result = $storage_folder->get_object($uid))
) {
return $result;
}
}
}
/**
* Wrapper to update an event object
*/
protected function update_event($event, $old, $status, $instanceId = null)
{
// TODO: instanceId - DateTime - of the exception to be processed, if not set process all occurrences
if ($instanceId) {
throw new Syncroton_Exception_Status_MeetingResponse(Syncroton_Exception_Status_MeetingResponse::INVALID_REQUEST);
}
if ($event['free_busy']) {
$old['free_busy'] = $event['free_busy'];
}
// Updating an existing event is most-likely a response
// to an iTip request with bumped SEQUENCE
$old['sequence'] += 1;
// Update the event
return $this->save_event($old, $status);
}
/**
* Save the Kolab event (create if not exist)
* If an event does not exist it will be created in the default folder
*/
protected function save_event(&$event, $status = null)
{
// Find default folder to which we'll save the event
if (!isset($event['_mailbox'])) {
$folders = $this->listFolders();
$storage = rcube::get_instance()->get_storage();
// find the default
foreach ($folders as $folder) {
if ($folder['type'] == 8 && $storage->folder_namespace($folder['imap_name']) == 'personal') {
$event['_mailbox'] = $folder['imap_name'];
break;
}
}
// if there's no folder marked as default, use any
if (!isset($event['_mailbox']) && !empty($folders)) {
foreach ($folders as $folder) {
if ($storage->folder_namespace($folder['imap_name']) == 'personal') {
$event['_mailbox'] = $folder['imap_name'];
break;
}
}
}
// TODO: what if the user has no subscribed event folders for this device
// should we use any existing event folder even if not subscribed for sync?
}
if ($status) {
$this->update_attendee_status($event, $status);
}
// TODO: Free/busy trigger?
if (isset($event['_mailbox'])) {
$folder = $this->getFolderObject($event['_mailbox']);
if ($folder && $folder->valid && $folder->save($event)) {
return $folder;
}
}
return false;
}
/**
* Update the attendee status of the user
*/
protected function update_attendee_status(&$event, $status)
{
$organizer = null;
$emails = $this->user_emails();
foreach ((array) $event['attendees'] as $i => $attendee) {
if ($attendee['role'] == 'ORGANIZER') {
$organizer = $attendee;
}
else if ($attendee['email'] && in_array_nocase($attendee['email'], $emails)) {
$event['attendees'][$i]['status'] = $status;
$event['attendees'][$i]['rsvp'] = false;
$event_attendee = $attendee;
}
}
if (!$event_attendee) {
$this->logger->warn('MeetingResponse on an event where the user is not an attendee. UID: ' . $event['uid']);
throw new Syncroton_Exception_Status_MeetingResponse(Syncroton_Exception_Status_MeetingResponse::MEETING_ERROR);
}
}
/**
* Returns filter query array according to specified ActiveSync FilterType
*
* @param int $filter_type Filter type
*
* @param array Filter query
*/
protected function filter($filter_type = 0)
{
$filter = array(array('type', '=', $this->modelName));
switch ($filter_type) {
case Syncroton_Command_Sync::FILTER_2_WEEKS_BACK:
$mod = '-2 weeks';
break;
case Syncroton_Command_Sync::FILTER_1_MONTH_BACK:
$mod = '-1 month';
break;
case Syncroton_Command_Sync::FILTER_3_MONTHS_BACK:
$mod = '-3 months';
break;
case Syncroton_Command_Sync::FILTER_6_MONTHS_BACK:
$mod = '-6 months';
break;
}
if (!empty($mod)) {
$dt = new DateTime('now', new DateTimeZone('UTC'));
$dt->modify($mod);
$filter[] = array('dtend', '>', $dt);
}
return $filter;
}
/**
* Set MeetingStatus according to event data
*/
protected function meeting_status_from_kolab($collection, $event, &$result)
{
// 0 - The event is an appointment, which has no attendees.
// 1 - The event is a meeting and the user is the meeting organizer.
// 3 - This event is a meeting, and the user is not the meeting organizer.
// 5 - The meeting has been canceled and the user was the meeting organizer.
// 7 - The meeting has been canceled. The user was not the meeting organizer.
$status = 0;
if (!empty($event['attendees'])) {
// Find out if the user is an organizer
// TODO: Delegation/aliases support
$user_emails = $this->user_emails();
$is_organizer = false;
if ($event['organizer'] && $event['organizer']['email']) {
$is_organizer = in_array_nocase($event['organizer']['email'], $user_emails);
}
if ($event['status'] == 'CANCELLED') {
$status = $is_organizer ? 5 : 7;
}
else {
$status = $is_organizer ? 1 : 3;
}
}
$result['meetingStatus'] = $status;
}
/**
* Converts libkolab alarms spec. into a number of minutes
*/
protected function from_kolab_alarm($event)
{
if (isset($event['valarms'])) {
foreach ($event['valarms'] as $alarm) {
if (in_array($alarm['action'], array('DISPLAY', 'AUDIO'))) {
$value = $alarm['trigger'];
break;
}
}
}
- if ($value && $value instanceof DateTime) {
- if ($event['start'] && ($interval = $event['start']->diff($value))) {
+ if (!empty($value) && $value instanceof DateTime) {
+ if (!empty($event['start']) && ($interval = $event['start']->diff($value))) {
if ($interval->invert && !$interval->m && !$interval->y) {
return intval(round($interval->s/60) + $interval->i + $interval->h * 60 + $interval->d * 60 * 24);
}
}
}
- else if ($value && preg_match('/^([-+]*)[PT]*([0-9]+)([WDHMS])$/', $value, $matches)) {
+ else if (!empty($value) && preg_match('/^([-+]*)[PT]*([0-9]+)([WDHMS])$/', $value, $matches)) {
$value = intval($matches[2]);
if ($value && $matches[1] != '-') {
return null;
}
switch ($matches[3]) {
case 'S': $value = intval(round($value/60)); break;
case 'H': $value *= 60; break;
case 'D': $value *= 24 * 60; break;
case 'W': $value *= 7 * 24 * 60; break;
}
return $value;
}
}
/**
* Converts ActiveSync reminder into libkolab alarms spec.
*/
protected function to_kolab_alarm($value, $event)
{
if ($value === null || $value === '') {
- return (array) $event['valarms'];
+ return isset($event['valarms']) ? (array) $event['valarms'] : array();
}
$valarms = array();
$unsupported = array();
if (!empty($event['valarms'])) {
foreach ($event['valarms'] as $alarm) {
if (!$current && in_array($alarm['action'], array('DISPLAY', 'AUDIO'))) {
$current = $alarm;
}
else {
$unsupported[] = $alarm;
}
}
}
$valarms[] = array(
- 'action' => $current['action'] ?: 'DISPLAY',
- 'description' => $current['description'] ?: '',
+ 'action' => !empty($current['action']) ? $current['action'] : 'DISPLAY',
+ 'description' => !empty($current['description']) ? $current['description'] : '',
'trigger' => sprintf('-PT%dM', $value),
);
if (!empty($unsupported)) {
$valarms = array_merge($valarms, $unsupported);
}
return $valarms;
}
/**
* Sanity checks on event input
*
* @param Syncroton_Model_IEntry &$entry Entry object
*
* @throws Syncroton_Exception_Status_Sync
*/
protected function check_event(Syncroton_Model_IEntry &$entry)
{
// https://msdn.microsoft.com/en-us/library/jj194434(v=exchg.80).aspx
$now = new DateTime('now');
$rounded = new DateTime('now');
$min = (int) $rounded->format('i');
$add = $min > 30 ? (60 - $min) : (30 - $min);
$rounded->add(new DateInterval('PT' . $add . 'M'));
if (empty($entry->startTime) && empty($entry->endTime)) {
// use current time rounded to 30 minutes
$end = clone $rounded;
$end->add(new DateInterval($entry->allDayEvent ? 'P1D' : 'PT30M'));
$entry->startTime = $rounded;
$entry->endTime = $end;
}
else if (empty($entry->startTime)) {
if ($entry->endTime < $now || $entry->endTime < $rounded) {
throw new Syncroton_Exception_Status_Sync(Syncroton_Exception_Status_Sync::INVALID_ITEM);
}
$entry->startTime = $rounded;
}
else if (empty($entry->endTime)) {
if ($entry->startTime < $now) {
throw new Syncroton_Exception_Status_Sync(Syncroton_Exception_Status_Sync::INVALID_ITEM);
}
$rounded->add(new DateInterval($entry->allDayEvent ? 'P1D' : 'PT30M'));
$entry->endTime = $rounded;
}
}
/**
* Check if the new event version has any significant changes
*/
protected function has_significant_changes($event, $old)
{
// Calendar namespace fields
foreach (array('allday', 'start', 'end', 'location', 'recurrence') as $key) {
if ($event[$key] != $old[$key]) {
// Comparing recurrence is tricky as there can be differences in default
// value handling. Let's try to handle most common cases
if ($key == 'recurrence' && $this->fixed_recurrence($event) == $this->fixed_recurrence($old)) {
continue;
}
return true;
}
}
if (count($event['attendees']) != count($old['attendees'])) {
return true;
}
foreach ($event['attendees'] as $idx => $attendee) {
$old_attendee = $old['attendees'][$idx];
if ($old_attendee['email'] != $attendee['email']
|| ($attendee['role'] != 'ORGANIZER'
&& $attendee['status'] != $old_attendee['status']
&& $attendee['status'] == 'NEEDS-ACTION')
) {
return true;
}
}
return false;
}
/**
* Unify recurrence spec. for comparison
*/
protected function fixed_recurrence($event)
{
$rec = (array) $event['recurrence'];
// Add BYDAY if not exists
if ($rec['FREQ'] == 'WEEKLY' && empty($rec['BYDAY'])) {
$days = array('SU', 'MO', 'TU', 'WE', 'TH', 'FR', 'SA');
$day = $event['start']->format('w');
$rec['BYDAY'] = $days[$day];
}
if (!$rec['INTERVAL']) {
$rec['INTERVAL'] = 1;
}
ksort($rec);
return $rec;
}
/**
* Check if the event update request is a fake (for Outlook)
*/
protected function isDummyOutlookUpdate($data, $entry, &$result)
{
$is_dummy = false;
// Outlook 2013 sends a dummy update just after MeetingResponse has been processed,
// this update resets attendee status set in the MeetingResponse request.
// We ignore attendees data in such updates, they should not happen according to
// https://msdn.microsoft.com/en-us/library/office/hh428685(v=exchg.140).aspx
// but they will contain some data as alarms and free/busy status so we don't
// ignore them completely
if (!empty($entry) && !empty($data->attendees) && stripos($this->device->devicetype, 'outlook') !== false) {
// Some of these requests use just dummy Timezone
$dummy_tz = str_repeat('A', 230) . '==';
if ($data->timezone == $dummy_tz) {
$is_dummy = true;
}
// But some of them do not, so we have check if that is a first
// update immediately (up to 5 seconds) after MeetingResponse request
if (!$is_dummy && ($dtstamp = $this->getKolabDataItem($entry, self::KEY_RESPONSE_DTSTAMP))) {
$dtstamp = new DateTime($dtstamp);
$now = new DateTime('now', new DateTimeZone('UTC'));
$is_dummy = $now->getTimestamp() - $dtstamp->getTimestamp() <= 5;
}
$this->unsetKolabDataItem($result, self::KEY_RESPONSE_DTSTAMP);
}
return $is_dummy;
}
}
diff --git a/lib/kolab_sync_message.php b/lib/kolab_sync_message.php
index 448d31c..5068738 100644
--- a/lib/kolab_sync_message.php
+++ b/lib/kolab_sync_message.php
@@ -1,494 +1,494 @@
<?php
/**
+--------------------------------------------------------------------------+
| Kolab Sync (ActiveSync for Kolab) |
| |
| Copyright (C) 2011-2012, 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/> |
+--------------------------------------------------------------------------+
| Author: Aleksander Machniak <machniak@kolabsys.com> |
+--------------------------------------------------------------------------+
*/
class kolab_sync_message
{
protected $headers = array();
protected $body;
protected $ctype;
protected $ctype_params = array();
/**
* Constructor
*
* @param string|resource $source MIME message source
*/
function __construct($source)
{
$this->parse_mime($source);
}
/**
* Returns message headers
*
* @return array Message headers
*/
public function headers()
{
return $this->headers;
}
public function source()
{
$headers = array();
// Build the message back
foreach ($this->headers as $header => $header_value) {
$headers[$header] = $header . ': ' . $header_value;
}
return trim(implode("\r\n", $headers)) . "\r\n\r\n" . ltrim($this->body);
// @TODO: work with file streams
}
/**
* Appends text at the end of the message body
*
* @todo: HTML support
*
* @param string $text Text to append
* @param string $charset Text charset
*/
public function append($text, $charset = null)
{
if ($this->ctype == 'text/plain') {
// decode body
$body = $this->decode($this->body, $this->headers['Content-Transfer-Encoding']);
$body = rcube_charset::convert($body, $this->ctype_params['charset'], $charset);
// append text
$body .= $text;
// encode and save
$body = rcube_charset::convert($body, $charset, $this->ctype_params['charset']);
$this->body = $this->encode($body, $this->headers['Content-Transfer-Encoding']);
}
}
/**
* Adds attachment to the message
*
* @param string $body Attachment body (not encoded)
* @param string $params Attachment parameters (Mail_mimePart format)
*/
public function add_attachment($body, $params = array())
{
// convert the message into multipart/mixed
if ($this->ctype != 'multipart/mixed') {
$boundary = '_' . md5(rand() . microtime());
$this->body = "--$boundary\r\n"
."Content-Type: " . $this->headers['Content-Type']."\r\n"
."Content-Transfer-Encoding: " . $this->headers['Content-Transfer-Encoding']."\r\n"
."\r\n" . trim($this->body) . "\r\n"
."--$boundary\r\n";
$this->ctype = 'multipart/mixed';
$this->ctype_params = array('boundary' => $boundary);
unset($this->headers['Content-Transfer-Encoding']);
$this->save_content_type($this->ctype, $this->ctype_params);
}
// make sure MIME-Version header is set, it's required by some servers
if (empty($this->headers['MIME-Version'])) {
$this->headers['MIME-Version'] = '1.0';
}
$boundary = $this->ctype_params['boundary'];
$part = new Mail_mimePart($body, $params);
$body = $part->encode();
foreach ($body['headers'] as $name => $value) {
$body['headers'][$name] = $name . ': ' . $value;
}
$this->body = rtrim($this->body);
$this->body = preg_replace('/--$/', '', $this->body);
// add the attachment to the end of the message
$this->body .= "\r\n"
.implode("\r\n", $body['headers']) . "\r\n\r\n"
.$body['body'] . "\r\n--$boundary--\r\n";
}
/**
* Sets the value of specified message header
*
* @param string $name Header name
* @param string $value Header value
*/
public function set_header($name, $value)
{
$name = self::normalize_header_name($name);
if ($name != 'Content-Type') {
$this->headers[$name] = $value;
}
}
/**
* Send the given message using the configured method.
*
* @param array $smtp_error SMTP error array (reference)
* @param array $smtp_opts SMTP options (e.g. DSN request)
*
* @return boolean Send status.
*/
public function send(&$smtp_error = null, $smtp_opts = null)
{
$rcube = rcube::get_instance();
$headers = $this->headers;
$mailto = $headers['To'];
$headers['User-Agent'] .= $rcube->app_name . ' ' . kolab_sync::VERSION;
if ($agent = $rcube->config->get('useragent')) {
$headers['User-Agent'] .= '/' . $agent;
}
if (empty($headers['From'])) {
$headers['From'] = $this->get_identity();
}
// make sure there's sender name in From:
else if ($rcube->config->get('activesync_fix_from')
&& preg_match('/^<?((\S+|("[^"]+"))@\S+)>?$/', trim($headers['From']), $m)
) {
$identities = kolab_sync::get_instance()->user->list_identities();
$email = $m[1];
foreach ((array) $identities as $ident) {
if ($ident['email'] == $email) {
if ($ident['name']) {
$headers['From'] = format_email_recipient($email, $ident['name']);
}
break;
}
}
}
if (empty($headers['Message-ID'])) {
$headers['Message-ID'] = $rcube->gen_message_id();
}
// remove empty headers
$headers = array_filter($headers);
$smtp_headers = $headers;
// generate list of recipients
$recipients = array();
if (!empty($headers['To']))
$recipients[] = $headers['To'];
if (!empty($headers['Cc']))
$recipients[] = $headers['Cc'];
if (!empty($headers['Bcc']))
$recipients[] = $headers['Bcc'];
if (empty($headers['To']) && empty($headers['Cc'])) {
$headers['To'] = 'undisclosed-recipients:;';
}
// remove Bcc header
unset($smtp_headers['Bcc']);
// send message
if (!is_object($rcube->smtp)) {
$rcube->smtp_init(true);
}
$sent = $rcube->smtp->send_mail($headers['From'], $recipients, $smtp_headers, $this->body, $smtp_opts);
$smtp_response = $rcube->smtp->get_response();
$smtp_error = $rcube->smtp->get_error();
// log error
if (!$sent) {
rcube::raise_error(array('code' => 800, 'type' => 'smtp',
'line' => __LINE__, 'file' => __FILE__,
'message' => "SMTP error: ".join("\n", $smtp_response)), true, false);
}
if ($sent) {
$rcube->plugins->exec_hook('message_sent', array('headers' => $headers, 'body' => $this->body));
// remove MDN headers after sending
unset($headers['Return-Receipt-To'], $headers['Disposition-Notification-To']);
if ($rcube->config->get('smtp_log')) {
// get all recipient addresses
$mailto = rcube_mime::decode_address_list(implode(',', $recipients), null, false, null, true);
rcube::write_log('sendmail', sprintf("User %s [%s]; Message %s for %s; %s",
$rcube->get_user_name(),
rcube_utils::remote_addr(),
$headers['Message-ID'],
implode(', ', $mailto),
!empty($smtp_response) ? implode('; ', $smtp_response) : '')
);
}
}
$this->headers = $headers;
return $sent;
}
/**
* Parses the message source and fixes 8bit data for ActiveSync.
* This way any not UTF8 characters will be encoded before
* sending to the device.
*
* @param string $message Message source
*
* @return string Fixed message source
*/
public static function recode_message($message)
{
// @TODO: work with stream, to workaround memory issues with big messages
if (is_resource($message)) {
$message = stream_get_contents($message);
}
list($headers, $message) = preg_split('/\r?\n\r?\n/', $message, 2, PREG_SPLIT_NO_EMPTY);
$hdrs = self::parse_headers($headers);
// multipart message
if (preg_match('/boundary="?([a-z0-9-\'\(\)+_\,\.\/:=\? ]+)"?/i', $hdrs['Content-Type'], $matches)) {
$boundary = '--' . $matches[1];
$message = explode($boundary, $message);
for ($x=1, $parts = count($message) - 1; $x<$parts; $x++) {
$message[$x] = "\r\n" . self::recode_message(ltrim($message[$x]));
}
return $headers . "\r\n\r\n" . implode($boundary , $message);
}
// single part
- $enc = strtolower($hdrs['Content-Transfer-Encoding']);
+ $enc = !empty($hdrs['Content-Transfer-Encoding']) ? strtolower($hdrs['Content-Transfer-Encoding']) : null;
// do nothing if already encoded
if ($enc != 'quoted-printable' && $enc != 'base64') {
// recode body if any non-printable-ascii characters found
if (preg_match('/[^\x20-\x7E\x0A\x0D\x09]/', $message)) {
$hdrs['Content-Transfer-Encoding'] = 'base64';
foreach ($hdrs as $header => $header_value) {
$hdrs[$header] = $header . ': ' . $header_value;
}
$headers = trim(implode("\r\n", $hdrs));
$message = rtrim(chunk_split(base64_encode(rtrim($message)), 76, "\r\n")) . "\r\n";
}
}
return $headers . "\r\n\r\n" . $message;
}
/**
* Creates a fake plain text message source with predefined headers and body
*
* @param string $headers Message headers
* @param string $body Plain text body
*
* @return string Message source
*/
public static function fake_message($headers, $body = '')
{
$hdrs = self::parse_headers($headers);
$result = '';
$hdrs['Content-Type'] = 'text/plain; charset=UTF-8';
$hdrs['Content-Transfer-Encoding'] = 'quoted-printable';
foreach ($hdrs as $header => $header_value) {
$result .= $header . ': ' . $header_value . "\r\n";
}
return $result . "\r\n" . self::encode($body, 'quoted-printable');
}
/**
* MIME message parser
*
* @param string|resource $message MIME message source
* @param bool $decode_body Enables body decoding
*
* @return array Message headers array and message body
*/
protected function parse_mime($message)
{
// @TODO: work with stream, to workaround memory issues with big messages
if (is_resource($message)) {
$message = stream_get_contents($message);
}
list($headers, $message) = preg_split('/\r?\n\r?\n/', $message, 2, PREG_SPLIT_NO_EMPTY);
$headers = self::parse_headers($headers);
// parse Content-Type header
$ctype_parts = preg_split('/[; ]+/', $headers['Content-Type']);
$this->ctype = strtolower(array_shift($ctype_parts));
foreach ($ctype_parts as $part) {
if (preg_match('/^([a-z-_]+)\s*=\s*(.+)$/i', trim($part), $m)) {
$this->ctype_params[strtolower($m[1])] = trim($m[2], '"');
}
}
if (!empty($headers['Content-Transfer-Encoding'])) {
$headers['Content-Transfer-Encoding'] = strtolower($headers['Content-Transfer-Encoding']);
}
$this->headers = $headers;
$this->body = $message;
}
/**
* Parse message source with headers
*/
protected static function parse_headers($headers)
{
// Parse headers
$headers = str_replace("\r\n", "\n", $headers);
$headers = explode("\n", trim($headers));
$ln = 0;
$lines = array();
foreach ($headers as $line) {
if (ord($line[0]) <= 32) {
$lines[$ln] .= (empty($lines[$ln]) ? '' : "\r\n") . $line;
}
else {
$lines[++$ln] = trim($line);
}
}
// Unify char-case of header names
$headers = array();
foreach ($lines as $line) {
list($field, $string) = explode(':', $line, 2);
if ($field = self::normalize_header_name($field)) {
$headers[$field] = trim($string);
}
}
return $headers;
}
/**
* Normalize (fix) header names
*/
protected static function normalize_header_name($name)
{
$headers_map = array(
'subject' => 'Subject',
'from' => 'From',
'to' => 'To',
'cc' => 'Cc',
'bcc' => 'Bcc',
'message-id' => 'Message-ID',
'references' => 'References',
'content-type' => 'Content-Type',
'content-transfer-encoding' => 'Content-Transfer-Encoding',
);
$name_lc = strtolower($name);
return isset($headers_map[$name_lc]) ? $headers_map[$name_lc] : $name;
}
/**
* Encodes message/part body
*
* @param string $body Message/part body
* @param string $encoding Content encoding
*
* @return string Encoded body
*/
protected function encode($body, $encoding)
{
switch ($encoding) {
case 'base64':
$body = base64_encode($body);
$body = chunk_split($body, 76, "\r\n");
break;
case 'quoted-printable':
$body = quoted_printable_encode($body);
break;
}
return $body;
}
/**
* Decodes message/part body
*
* @param string $body Message/part body
* @param string $encoding Content encoding
*
* @return string Decoded body
*/
protected function decode($body, $encoding)
{
$body = str_replace("\r\n", "\n", $body);
switch ($encoding) {
case 'base64':
$body = base64_decode($body);
break;
case 'quoted-printable':
$body = quoted_printable_decode($body);
break;
}
return $body;
}
/**
* Returns email address string from default identity of the current user
*/
protected function get_identity()
{
$user = kolab_sync::get_instance()->user;
if ($identity = $user->get_identity()) {
return format_email_recipient(format_email($identity['email']), $identity['name']);
}
}
protected function save_content_type($ctype, $params = array())
{
$this->ctype = $ctype;
$this->ctype_params = $params;
$this->headers['Content-Type'] = $ctype;
if (!empty($params)) {
foreach ($params as $name => $value) {
$this->headers['Content-Type'] .= sprintf('; %s="%s"', $name, $value);
}
}
}
}
diff --git a/lib/kolab_sync_timezone_converter.php b/lib/kolab_sync_timezone_converter.php
index fe2e9d7..800be59 100644
--- a/lib/kolab_sync_timezone_converter.php
+++ b/lib/kolab_sync_timezone_converter.php
@@ -1,647 +1,650 @@
<?php
/**
+--------------------------------------------------------------------------+
| Kolab Sync (ActiveSync for Kolab) |
| |
| Copyright (C) 2011-2017, Kolab Systems AG <contact@kolabsys.com> |
| Copyright (C) 2008-2012, Metaways Infosystems GmbH |
| |
| 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/> |
+--------------------------------------------------------------------------+
| Author: Aleksander Machniak <machniak@kolabsys.com> |
| Author: Jonas Fischer <j.fischer@metaways.de> |
+--------------------------------------------------------------------------+
*/
/**
* Activesync timezone converter
*/
class kolab_sync_timezone_converter
{
/**
* holds the instance of the singleton
*
* @var kolab_sync_timezone_onverter
*/
private static $_instance = NULL;
protected $_startDate = array();
/**
* If set then the timezone guessing results will be cached.
* This is strongly recommended for performance reasons.
*
* @var rcube_cache
*/
protected $cache = null;
/**
* array of offsets known by ActiceSync clients, but unknown by php
* @var array
*/
protected $_knownTimezones = array(
'0AIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==' => array(
'Pacific/Kwajalein' => 'MHT'
)
);
/**
* don't use the constructor. Use the singleton.
*
* @param $_logger
*/
private function __construct()
{
}
/**
* don't clone. Use the singleton.
*/
private function __clone()
{
}
/**
* the singleton pattern
*
* @return kolab_sync_timezone_converter
*/
public static function getInstance()
{
if (self::$_instance === NULL) {
self::$_instance = new kolab_sync_timezone_converter();
}
return self::$_instance;
}
/**
* Returns a timezone with an offset matching the time difference
* of $dt from $referenceDt.
*
* If set and matching the offset, kolab_format::$timezone is preferred.
*
* @param DateTime $dt The date time value for which we
* calculate the offset.
* @param DateTime $referenceDt The reference value, for instance in UTC.
*
* @return DateTimeZone|null
*/
public function getOffsetTimezone($dt, $referenceDt)
{
$interval = $referenceDt->diff($dt);
$tz = new DateTimeZone($interval->format('%R%H%I')); //e.g. +0200
$utcOffset = $tz->getOffset($dt);
//Prefer the configured timezone if it matches the offset.
if (kolab_format::$timezone) {
if (kolab_format::$timezone->getOffset($dt) == $utcOffset) {
return kolab_format::$timezone;
}
}
//Look for any timezone with a matching offset.
foreach (DateTimeZone::listIdentifiers() as $timezoneIdentifier) {
$timezone = new DateTimeZone($timezoneIdentifier);
if ($timezone->getOffset($dt) == $utcOffset) {
return $timezone;
}
}
return null;
}
/**
* Returns a list of timezones that match to the {@param $_offsets}
*
* If {@see $_expectedTimezone} is set then the method will terminate as soon
* as the expected timezone has matched and the expected timezone will be the
* first entry to the returned array.
*
* @param string|array $_offsets
*
* @return array
*/
public function getListOfTimezones($_offsets)
{
if (is_string($_offsets) && isset($this->_knownTimezones[$_offsets])) {
$timezones = $this->_knownTimezones[$_offsets];
}
else {
if (is_string($_offsets)) {
// unpack timezone info to array
$_offsets = $this->_unpackTimezoneInfo($_offsets);
}
if (!$this->_validateOffsets($_offsets)) {
return array();
}
$this->_setDefaultStartDateIfEmpty($_offsets);
$cacheId = $this->_getCacheId('timezones', $_offsets);
$timezones = $this->_loadFromCache($cacheId);
if (!is_array($timezones)) {
$timezones = array();
foreach (DateTimeZone::listIdentifiers() as $timezoneIdentifier) {
$timezone = new DateTimeZone($timezoneIdentifier);
if (false !== ($matchingTransition = $this->_checkTimezone($timezone, $_offsets))) {
$timezones[$timezoneIdentifier] = $matchingTransition['abbr'];
}
}
$this->_saveInCache($timezones, $cacheId);
}
}
return $timezones;
}
/**
* Returns PHP timezone that matches to the {@param $_offsets}
*
* If {@see $_expectedTimezone} is set then the method will return this timezone if it matches.
*
* @param string|array $_offsets Activesync timezone definition
* @param string $_expectedTomezone Expected timezone name
*
* @return string Expected timezone name
*/
public function getTimezone($_offsets, $_expectedTimezone = null)
{
$timezones = $this->getListOfTimezones($_offsets);
if ($_expectedTimezone && isset($timezones[$_expectedTimezone])) {
return $_expectedTimezone;
}
else {
return key($timezones);
}
}
/**
* Return packed string for given {@param $_timezone}
*
* @param string $_timezone Timezone identifier
* @param string|int $_startDate Start date
*
* @return string Packed timezone offsets
*/
public function encodeTimezone($_timezone, $_startDate = null)
{
foreach ($this->_knownTimezones as $packedString => $knownTimezone) {
if (array_key_exists($_timezone, $knownTimezone)) {
return $packedString;
}
}
$offsets = $this->getOffsetsForTimezone($_timezone, $_startDate);
return $this->_packTimezoneInfo($offsets);
}
/**
* Get offsets for given timezone
*
* @param string $_timezone Timezone identifier
* @param string|int $_startDate Start date
*
* @return array Timezone offsets
*/
public function getOffsetsForTimezone($_timezone, $_startDate = null)
{
$this->_setStartDate($_startDate);
$cacheId = $this->_getCacheId('offsets', array($_timezone));
if (false === ($offsets = $this->_loadFromCache($cacheId))) {
$offsets = $this->_getOffsetsTemplate();
try {
$timezone = new DateTimeZone($_timezone);
}
catch (Exception $e) {
return null;
}
list($standardTransition, $daylightTransition) = $this->_getTransitionsForTimezoneAndYear($timezone, $this->_startDate['year']);
if ($standardTransition) {
$offsets['bias'] = $standardTransition['offset']/60*-1;
if ($daylightTransition) {
$offsets = $this->_generateOffsetsForTransition($offsets, $standardTransition, 'standard', $timezone);
$offsets = $this->_generateOffsetsForTransition($offsets, $daylightTransition, 'daylight', $timezone);
//@todo how do we get the standardBias (is usually 0)?
//$offsets['standardBias'] = ...
$offsets['daylightBias'] = ($daylightTransition['offset'] - $standardTransition['offset'])/60*-1;
$offsets['standardHour'] -= $offsets['daylightBias'] / 60;
$offsets['daylightHour'] += $offsets['daylightBias'] / 60;
}
}
$this->_saveInCache($offsets, $cacheId);
}
return $offsets;
}
/**
* Get offsets for timezone transition
*
* @param array $_offsets Timezone offsets
* @param array $_transition Timezone transition information
* @param string $_type Transition type: 'standard' or 'daylight'
* @param DateTimeZone $_timezone Timezone of the transition
*
* @return array
*/
protected function _generateOffsetsForTransition(array $_offsets, array $_transition, $_type, $_timezone)
{
$transitionDate = new DateTime($_transition['time'], $_timezone);
if ($_transition['offset']) {
$transitionDate->modify($_transition['offset'] . ' seconds');
}
$_offsets[$_type . 'Month'] = (int) $transitionDate->format('n');
$_offsets[$_type . 'DayOfWeek'] = (int) $transitionDate->format('w');
$_offsets[$_type . 'Minute'] = (int) $transitionDate->format('i');
$_offsets[$_type . 'Hour'] = (int) $transitionDate->format('G');
for ($i=5; $i>0; $i--) {
if ($this->_isNthOcurrenceOfWeekdayInMonth($transitionDate, $i)) {
$_offsets[$_type . 'Day'] = $i;
break;
};
}
return $_offsets;
}
/**
* Test if the weekday of the given {@param $_timestamp} is the {@param $_occurence}th occurence of this weekday within its month.
*
* @param DateTime $_datetime
* @param int $_occurence [1 to 5, where 5 indicates the final occurrence during the month if that day of the week does not occur 5 times]
*
* @return bool
*/
protected function _isNthOcurrenceOfWeekdayInMonth($_datetime, $_occurence)
{
if ($_occurence <= 1) {
return true;
}
$orig = $_datetime->format('n');
if ($_occurence == 5) {
$modified = clone($_datetime);
$modified->modify('1 week');
$mod = $modified->format('n');
// modified date is a next month
return $mod > $orig || ($mod == 1 && $orig == 12);
}
$modified = clone($_datetime);
$modified->modify(sprintf('-%d weeks', $_occurence - 1));
$mod = $modified->format('n');
if ($mod != $orig) {
return false;
}
$modified = clone($_datetime);
$modified->modify(sprintf('-%d weeks', $_occurence));
$mod = $modified->format('n');
// modified month is earlier than original
return $mod < $orig || ($mod == 12 && $orig == 1);
}
/**
* Check if the given {@param $_standardTransition} and {@param $_daylightTransition}
* match to the object property {@see $_offsets}
*
* @param array $standardTransition
* @param array $daylightTransition
*
* @return bool
*/
protected function _checkTransition($_standardTransition, $_daylightTransition, $_offsets, $tz)
{
if (empty($_standardTransition) || empty($_offsets)) {
return false;
}
$standardOffset = ($_offsets['bias'] + $_offsets['standardBias']) * 60 * -1;
// check each condition in a single if statement and break the chain when one condition is not met - for performance reasons
if ($standardOffset == $_standardTransition['offset'] ) {
if (empty($_offsets['daylightMonth']) && (empty($_daylightTransition) || empty($_daylightTransition['isdst']))) {
// No DST
return true;
}
$daylightOffset = ($_offsets['bias'] + $_offsets['daylightBias']) * 60 * -1;
// the milestone is sending a positive value for daylightBias while it should send a negative value
$daylightOffsetMilestone = ($_offsets['bias'] + ($_offsets['daylightBias'] * -1) ) * 60 * -1;
- if ($daylightOffset == $_daylightTransition['offset'] || $daylightOffsetMilestone == $_daylightTransition['offset']) {
+ if (
+ !empty($_daylightTransition)
+ && ($daylightOffset == $_daylightTransition['offset'] || $daylightOffsetMilestone == $_daylightTransition['offset'])
+ ) {
$standardDate = new DateTime($_standardTransition['time'], $tz);
$daylightDate = new DateTime($_daylightTransition['time'], $tz);
if ($standardDate->format('n') == $_offsets['standardMonth'] &&
$daylightDate->format('n') == $_offsets['daylightMonth'] &&
$standardDate->format('w') == $_offsets['standardDayOfWeek'] &&
$daylightDate->format('w') == $_offsets['daylightDayOfWeek']
) {
return $this->_isNthOcurrenceOfWeekdayInMonth($daylightDate, $_offsets['daylightDay']) &&
$this->_isNthOcurrenceOfWeekdayInMonth($standardDate, $_offsets['standardDay']);
}
}
}
return false;
}
/**
* decode timezone info from activesync
*
* @param string $_packedTimezoneInfo the packed timezone info
* @return array
*/
protected function _unpackTimezoneInfo($_packedTimezoneInfo)
{
$timezoneUnpackString = 'lbias/a64standardName/vstandardYear/vstandardMonth/vstandardDayOfWeek/vstandardDay/vstandardHour/vstandardMinute/vstandardSecond/vstandardMilliseconds/lstandardBias/a64daylightName/vdaylightYear/vdaylightMonth/vdaylightDayOfWeek/vdaylightDay/vdaylightHour/vdaylightMinute/vdaylightSecond/vdaylightMilliseconds/ldaylightBias';
$timezoneInfo = unpack($timezoneUnpackString, base64_decode($_packedTimezoneInfo));
return $timezoneInfo;
}
/**
* encode timezone info to activesync
*
* @param array $_timezoneInfo
* @return string
*/
protected function _packTimezoneInfo($_timezoneInfo)
{
if (!is_array($_timezoneInfo)) {
return null;
}
$packed = pack(
"la64vvvvvvvvla64vvvvvvvvl",
$_timezoneInfo['bias'],
$_timezoneInfo['standardName'],
$_timezoneInfo['standardYear'],
$_timezoneInfo['standardMonth'],
$_timezoneInfo['standardDayOfWeek'],
$_timezoneInfo['standardDay'],
$_timezoneInfo['standardHour'],
$_timezoneInfo['standardMinute'],
$_timezoneInfo['standardSecond'],
$_timezoneInfo['standardMilliseconds'],
$_timezoneInfo['standardBias'],
$_timezoneInfo['daylightName'],
$_timezoneInfo['daylightYear'],
$_timezoneInfo['daylightMonth'],
$_timezoneInfo['daylightDayOfWeek'],
$_timezoneInfo['daylightDay'],
$_timezoneInfo['daylightHour'],
$_timezoneInfo['daylightMinute'],
$_timezoneInfo['daylightSecond'],
$_timezoneInfo['daylightMilliseconds'],
$_timezoneInfo['daylightBias']
);
return base64_encode($packed);
}
/**
* Returns complete offsets array with all fields empty
*
* Used e.g. when reverse-generating ActiveSync Timezone Offset Information
* based on a given Timezone, {@see getOffsetsForTimezone}
*
* @return unknown_type
*/
protected function _getOffsetsTemplate()
{
return array(
'bias' => 0,
'standardName' => '',
'standardYear' => 0,
'standardMonth' => 0,
'standardDayOfWeek' => 0,
'standardDay' => 0,
'standardHour' => 0,
'standardMinute' => 0,
'standardSecond' => 0,
'standardMilliseconds' => 0,
'standardBias' => 0,
'daylightName' => '',
'daylightYear' => 0,
'daylightMonth' => 0,
'daylightDayOfWeek' => 0,
'daylightDay' => 0,
'daylightHour' => 0,
'daylightMinute' => 0,
'daylightSecond' => 0,
'daylightMilliseconds' => 0,
'daylightBias' => 0
);
}
/**
* Validate and set offsets
*
* @param array $value
*
* @return bool Validation result
*/
protected function _validateOffsets($value)
{
// validate $value
if ((!empty($value['standardMonth']) || !empty($value['standardDay']) || !empty($value['daylightMonth']) || !empty($value['daylightDay'])) &&
(empty($value['standardMonth']) || empty($value['standardDay']) || empty($value['daylightMonth']) || empty($value['daylightDay']))
) {
// It is not possible not set standard offsets without setting daylight offsets and vice versa
return false;
}
return true;
}
/**
* Parse and set object property {@see $_startDate}
*
* @param string|int $_startDate
* @return void
*/
protected function _setStartDate($_startDate)
{
if (empty($_startDate)) {
$this->_setDefaultStartDateIfEmpty();
return;
}
$startDateParsed = array();
if (is_string($_startDate)) {
$startDateParsed['string'] = $_startDate;
$startDateParsed['ts'] = strtotime($_startDate);
}
else if (is_int($_startDate)) {
$startDateParsed['ts'] = $_startDate;
$startDateParsed['string'] = strftime('%F', $_startDate);
}
else {
$this->_setDefaultStartDateIfEmpty();
return;
}
$startDateParsed['object'] = new DateTime($startDateParsed['string']);
$startDateParsed = array_merge($startDateParsed, getdate($startDateParsed['ts']));
$this->_startDate = $startDateParsed;
}
/**
* Set default value for object property {@see $_startdate} if it is not set yet.
* Tries to guess the correct startDate depending on object property {@see $_offsets} and
* falls back to current date.
*
* @param array $_offsets [offsets may be avaluated for a given start year]
* @return void
*/
protected function _setDefaultStartDateIfEmpty($_offsets = null)
{
if (!empty($this->_startDate)) {
return;
}
if (!empty($_offsets['standardYear'])) {
$this->_setStartDate($_offsets['standardYear'].'-01-01');
}
else {
$this->_setStartDate(time());
}
}
/**
* Check if the given {@param $_timezone} matches the {@see $_offsets}
* and also evaluate the daylight saving time transitions for this timezone if necessary.
*
* @param DateTimeZone $timezone
* @param array $offsets
*
* @return array|bool
*/
protected function _checkTimezone(DateTimeZone $timezone, $offsets)
{
list($standardTransition, $daylightTransition) = $this->_getTransitionsForTimezoneAndYear($timezone, $this->_startDate['year']);
if ($this->_checkTransition($standardTransition, $daylightTransition, $offsets, $timezone)) {
return $standardTransition;
}
return false;
}
/**
* Returns the standard and daylight transitions for the given {@param $_timezone}
* and {@param $_year}.
*
* @param DateTimeZone $_timezone
* @param int $_year
*
* @return array
*/
protected function _getTransitionsForTimezoneAndYear(DateTimeZone $_timezone, $_year)
{
$standardTransition = null;
$daylightTransition = null;
$start = mktime(0, 0, 0, 12, 1, $_year - 1);
$end = mktime(24, 0, 0, 12, 31, $_year);
$transitions = $_timezone->getTransitions($start, $end);
if ($transitions === false) {
return array();
}
foreach ($transitions as $index => $transition) {
if (strftime('%Y', $transition['ts']) == $_year) {
if (isset($transitions[$index+1]) && strftime('%Y', $transitions[$index]['ts']) == strftime('%Y', $transitions[$index+1]['ts'])) {
$daylightTransition = $transition['isdst'] ? $transition : $transitions[$index+1];
$standardTransition = $transition['isdst'] ? $transitions[$index+1] : $transition;
}
else {
$daylightTransition = $transition['isdst'] ? $transition : null;
$standardTransition = $transition['isdst'] ? null : $transition;
}
break;
}
else if ($index == count($transitions) -1) {
$standardTransition = $transition;
}
}
return array($standardTransition, $daylightTransition);
}
protected function _getCacheId($_prefix, $_offsets)
{
return $_prefix . md5(serialize($_offsets));
}
protected function _loadFromCache($key)
{
- if ($cache = $this->getCache) {
+ if ($cache = $this->getCache()) {
return $cache->get($key);
}
return false;
}
protected function _saveInCache($value, $key)
{
- if ($cache = $this->getCache) {
+ if ($cache = $this->getCache()) {
$cache->set($key, $value);
}
}
/**
* Getter for the cache engine object
*/
protected function getCache()
{
if ($this->cache === null) {
$rcube = rcube::get_instance();
$cache = $rcube->get_cache_shared('activesync');
$this->cache = $cache ? $cache : false;
}
return $this->cache;
}
}
File Metadata
Details
Attached
Mime Type
text/x-diff
Expires
Sun, Jan 19, 2:11 AM (16 h, 1 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
120056
Default Alt Text
(81 KB)
Attached To
Mode
R4 syncroton
Attached
Detach File
Event Timeline
Log In to Comment