Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F2527573
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Flag For Later
Award Token
Size
25 KB
Referenced Files
None
Subscribers
None
View Options
diff --git a/lib/kolab_sync_timezone_converter.php b/lib/kolab_sync_timezone_converter.php
index 3f6a25c..710a0b7 100644
--- a/lib/kolab_sync_timezone_converter.php
+++ b/lib/kolab_sync_timezone_converter.php
@@ -1,620 +1,621 @@
<?php
/**
* Tine 2.0
*
* @package ActiveSync
* @license http://www.tine20.org/licenses/agpl-nonus.txt AGPL Version 1 (Non-US)
* NOTE: According to sec. 8 of the AFFERO GENERAL PUBLIC LICENSE (AGPL),
* Version 1, the distribution of the Tine 2.0 ActiveSync module in or to the
* United States of America is excluded from the scope of this license.
* @copyright Copyright (c) 2009 Metaways Infosystems GmbH (http://www.metaways.de)
* @author Jonas Fischer <j.fischer@metaways.de>
*/
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 an array 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);
}
}
// $this->_log(__METHOD__, __LINE__, 'Matching timezones: '.print_r($timezones, true));
// if (empty($timezones)) {
// throw new ActiveSync_TimezoneNotFoundException('No timezone found for the given offsets');
// }
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);
}
}
/**
* Unpacks {@param $_packedTimezoneInfo} using {@see unpackTimezoneInfo} and then
* calls {@see getTimezoneForOffsets} with the unpacked timezone info
*
* @param String $_packedTimezoneInfo
* @return String [timezone abbreviation e.g. CET, MST etc.]
*
*/
// public function getTimezoneForPackedTimezoneInfo($_packedTimezoneInfo)
// {
// $offsets = $this->_unpackTimezoneInfo($_packedTimezoneInfo);
// $matchingTimezones = $this->getTimezoneForOffsets($offsets);
// $maxMatches = 0;
// $matchingAbbr = null;
// foreach ($matchingTimezones as $abbr => $timezones) {
// if (count($timezones) > $maxMatches) {
// $maxMatches = count($timezones);
// $matchingAbbr = $abbr;
// }
// }
// return $matchingAbbr;
// }
/**
* Return packed string for given {@param $_timezone}
* @param String $_timezone
* @param String | int | null $_startDate
* @return String
*/
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
* @param $_startDate
* @return array
*/
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) {
// $this->_log(__METHOD__, __LINE__, ": could not instantiate timezone {$_timezone}: {$e->getMessage()}");
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');
$offsets = $this->_generateOffsetsForTransition($offsets, $daylightTransition, 'daylight');
- $offsets['standardHour'] += $daylightTransition['offset']/3600;
- $offsets['daylightHour'] += $standardTransition['offset']/3600;
//@todo how do we get the standardBias (is usually 0)?
//$offsets['standardBias'] = ...
$offsets['daylightBias'] = ($daylightTransition['offset'] - $standardTransition['offset'])/60*-1;
}
}
$this->_saveInCache($offsets, $cacheId);
}
return $offsets;
}
/**
*
*
* @param array $_offsets
* @param array $_transition
* @param String $_type
* @return array
*/
protected function _generateOffsetsForTransition(Array $_offsets, Array $_transition, $_type)
{
- $transitionDateParsed = getdate($_transition['ts']);
+ $transitionDateParsed = new DateTime($_transition['time']);
- $_offsets[$_type . 'Month'] = $transitionDateParsed['mon'];
- $_offsets[$_type . 'DayOfWeek'] = $transitionDateParsed['wday'];
- $_offsets[$_type . 'Minute'] = $transitionDateParsed['minutes'];
- $_offsets[$_type . 'Hour'] = $transitionDateParsed['hours'];
+ if ($_transition['offset']) {
+ $transitionDateParsed->modify($_transition['offset'] . ' seconds');
+ }
+
+ $transitionDateParsed->modify(($_type == 'daylight' ? '-1 hour' : '+1 hour'));
+
+ $_offsets[$_type . 'Month'] = (int) $transitionDateParsed->format('n');
+ $_offsets[$_type . 'DayOfWeek'] = (int) $transitionDateParsed->format('w');
+ $_offsets[$_type . 'Minute'] = (int) $transitionDateParsed->format('i');
+ $_offsets[$_type . 'Hour'] = (int) $transitionDateParsed->format('G');
for ($i=5; $i>0; $i--) {
if ($this->_isNthOcurrenceOfWeekdayInMonth($_transition['ts'], $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 int $_timestamp
* @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($_timestamp, $_occurence)
{
if ($_occurence <= 1) {
return true;
}
$original = new DateTime('@'.$_timestamp);
$orig = $original->format('n');
if ($_occurence == 5) {
$modified = clone($original);
$modified->modify('1 week');
$mod = $modified->format('n');
// modified date is a next month
return $mod > $orig || ($mod == 1 && $orig == 12);
}
$modified = clone($original);
$modified->modify(sprintf('-%d weeks', $_occurence - 1));
$mod = $modified->format('n');
if ($mod != $orig) {
return false;
}
$modified = clone($original);
$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)
{
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']) {
$standardParsed = getdate($_standardTransition['ts']);
$daylightParsed = getdate($_daylightTransition['ts']);
if ($standardParsed['mon'] == $_offsets['standardMonth'] &&
$daylightParsed['mon'] == $_offsets['daylightMonth'] &&
$standardParsed['wday'] == $_offsets['standardDayOfWeek'] &&
$daylightParsed['wday'] == $_offsets['daylightDayOfWeek']
) {
return $this->_isNthOcurrenceOfWeekdayInMonth($_daylightTransition['ts'], $_offsets['daylightDay']) &&
$this->_isNthOcurrenceOfWeekdayInMonth($_standardTransition['ts'], $_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)) {
// echo 'Matching timezone ' . $timezone->getName();
// echo 'Matching daylight transition ' . print_r($daylightTransition, 1);
// echo 'Matching standard transition ' . print_r($standardTransition, 1);
return $standardTransition;
}
return false;
}
/**
* Returns the standard and daylight transitions for the given {@param $_timezone}
* and {@param $_year}.
*
* @param DateTimeZone $_timezone
* @param $_year
* @return Array
*/
protected function _getTransitionsForTimezoneAndYear(DateTimeZone $_timezone, $_year)
{
$standardTransition = null;
$daylightTransition = null;
- if (version_compare(PHP_VERSION, '5.3.0', '>=')) {
- // Since php version 5.3.0 getTransitions accepts optional start and end parameters.
- $start = mktime(0, 0, 0, 12, 1, $_year - 1);
- $end = mktime(24, 0, 0, 12, 31, $_year);
- $transitions = $_timezone->getTransitions($start, $end);
- } else {
- $transitions = $_timezone->getTransitions();
+ $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();
}
- $index = 0; //we need to access index counter outside of the foreach loop
- $transition = array(); //we need to access the transition counter outside of the foreach loop
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) {
return $cache->get($key);
}
return false;
}
protected function _saveInCache($value, $key)
{
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;
}
}
diff --git a/tests/timezone_converter.php b/tests/timezone_converter.php
index ef020ed..539c155 100644
--- a/tests/timezone_converter.php
+++ b/tests/timezone_converter.php
@@ -1,28 +1,77 @@
<?php
class timezone_converter extends PHPUnit_Framework_TestCase
{
function setUp()
{
}
function test_list_timezones()
{
$converter = timezone_converter_test::getInstance();
$input = 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAoAAAAEAAIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMAAAAFAAEAAAAAAAAAxP///w==';
$output = $converter->getListOfTimezones($input, 'UTC');
$this->assertTrue(is_array($output));
}
+
+ function test_get_offsets_for_timezone()
+ {
+ $converter = timezone_converter_test::getInstance();
+ $datetime = new DateTime('2017-01-01T12:00:00Z');
+
+ $output = $converter->getOffsetsForTimezone('UTC', $datetime);
+
+ $this->assertSame($output['bias'], 0);
+ $this->assertSame($output['standardBias'], 0);
+ $this->assertSame($output['daylightBias'], 0);
+ $this->assertSame($output['standardMonth'], 0);
+ $this->assertSame($output['daylightMonth'], 0);
+
+ $output = $converter->getOffsetsForTimezone('Europe/Warsaw', $datetime);
+
+ $this->assertSame($output['standardBias'], 0);
+ $this->assertSame($output['standardMonth'], 10);
+ $this->assertSame($output['standardDay'], 5);
+ $this->assertSame($output['standardHour'], 3);
+ $this->assertSame($output['daylightBias'], -60);
+ $this->assertSame($output['daylightMonth'], 3);
+ $this->assertSame($output['daylightDay'], 5);
+ $this->assertSame($output['daylightHour'], 2);
+
+ $output = $converter->getOffsetsForTimezone('America/Los_Angeles', $datetime);
+
+ $this->assertSame($output['bias'], 480);
+ $this->assertSame($output['standardBias'], 0);
+ $this->assertSame($output['standardMonth'], 11);
+ $this->assertSame($output['standardDay'], 1);
+ $this->assertSame($output['standardHour'], 2);
+ $this->assertSame($output['daylightBias'], -60);
+ $this->assertSame($output['daylightMonth'], 3);
+ $this->assertSame($output['daylightDay'], 2);
+ $this->assertSame($output['daylightHour'], 2);
+
+ $output = $converter->getOffsetsForTimezone('Atlantic/Azores', $datetime);
+
+ $this->assertSame($output['bias'], 60);
+ $this->assertSame($output['standardBias'], 0);
+ $this->assertSame($output['standardMonth'], 10);
+ $this->assertSame($output['standardDay'], 5);
+ $this->assertSame($output['standardHour'], 1);
+ $this->assertSame($output['daylightBias'], -60);
+ $this->assertSame($output['daylightMonth'], 3);
+ $this->assertSame($output['daylightDay'], 5);
+ $this->assertSame($output['daylightHour'], 0);
+ }
}
class timezone_converter_test extends kolab_sync_timezone_converter
{
// disable cache
function getCache()
{
return null;
}
}
File Metadata
Details
Attached
Mime Type
text/x-diff
Expires
Sat, Jan 31, 1:26 AM (10 h, 47 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
426111
Default Alt Text
(25 KB)
Attached To
Mode
R4 syncroton
Attached
Detach File
Event Timeline
Log In to Comment