Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F1842143
kolab_sync_timezone_converter.php
No One
Temporary
Actions
Download File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Flag For Later
Size
22 KB
Referenced Files
None
Subscribers
None
kolab_sync_timezone_converter.php
View Options
<?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'
)
);
protected
$_legacyTimezones
=
array
(
# This is an outdated timezone that outlook keeps sending because of an outdate timezone database on windows
'Lv///0kAcgBhAG4AIABTAHQAYQBuAGQAYQByAGQAIABUAGkAbQBlAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAkABAADABcAOwA7AOcDAAAAAEkAcgBhAG4AIABEAGEAeQBsAGkAZwBoAHQAIABUAGkAbQBlAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMAAwAEAAAAAAAAAAAAxP///w=='
=>
array
(
'Asia/Tehran'
=>
'+0330'
)
);
/**
* 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
];
}
elseif
(
is_string
(
$_offsets
)
&&
isset
(
$this
->
_legacyTimezones
[
$_offsets
]))
{
$timezones
=
$this
->
_legacyTimezones
[
$_offsets
];
}
else
{
if
(
is_string
(
$_offsets
))
{
// unpack timezone info to array
$_offsets
=
$this
->
_unpackTimezoneInfo
(
$_offsets
);
}
if
(!
$this
->
_validateOffsets
(
$_offsets
))
{
return
array
();
}
$this
->
_setDefaultStartDateIfEmpty
(
$_offsets
);
$timezones
=
array
();
foreach
(
DateTimeZone
::
listIdentifiers
()
as
$timezoneIdentifier
)
{
$timezone
=
new
DateTimeZone
(
$timezoneIdentifier
);
if
(
false
!==
(
$matchingTransition
=
$this
->
_checkTimezone
(
$timezone
,
$_offsets
)))
{
$timezones
[
$timezoneIdentifier
]
=
$matchingTransition
[
'abbr'
];
}
}
}
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
);
$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
;
}
}
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
.
'Week'
]
=
$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
(
!
empty
(
$_daylightTransition
)
&&
(
$daylightOffset
==
$_daylightTransition
[
'offset'
]
||
$daylightOffsetMilestone
==
$_daylightTransition
[
'offset'
])
)
{
// date-time input here contains UTC timezone specifier (+0000),
// we have to convert the date to the requested timezone afterwards.
$standardDate
=
new
DateTime
(
$_standardTransition
[
'time'
]);
$daylightDate
=
new
DateTime
(
$_daylightTransition
[
'time'
]);
$standardDate
->
setTimezone
(
$tz
);
$daylightDate
->
setTimezone
(
$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
[
'daylightWeek'
])
&&
$this
->
_isNthOcurrenceOfWeekdayInMonth
(
$standardDate
,
$_offsets
[
'standardWeek'
]);
}
}
}
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/vstandardWeek/vstandardHour/vstandardMinute/vstandardSecond/vstandardMilliseconds/lstandardBias'
.
'/a64daylightName/vdaylightYear/vdaylightMonth/vdaylightDayOfWeek/vdaylightWeek/vdaylightHour/vdaylightMinute/vdaylightSecond/vdaylightMilliseconds/ldaylightBias'
;
$timezoneInfo
=
unpack
(
$timezoneUnpackString
,
base64_decode
(
$_packedTimezoneInfo
));
if
(
$timezoneInfo
[
'standardHour'
]
==
23
&&
$timezoneInfo
[
'standardMilliseconds'
]
==
999
&&
$timezoneInfo
[
'standardMinute'
]
==
59
&&
$timezoneInfo
[
'standardSecond'
]
==
59
)
{
$timezoneInfo
[
'standardHour'
]
=
24
;
$timezoneInfo
[
'standardMinute'
]
=
0
;
$timezoneInfo
[
'standardSecond'
]
=
0
;
$timezoneInfo
[
'standardMilliseconds'
]
=
0
;
}
return
$timezoneInfo
;
}
/**
* encode timezone info to activesync
*
* @param array $_timezoneInfo
* @return string
*/
protected
function
_packTimezoneInfo
(
$_timezoneInfo
)
{
if
(!
is_array
(
$_timezoneInfo
))
{
return
null
;
}
// According to e.g. https://docs.microsoft.com/en-us/windows/win32/api/minwinbase/ns-minwinbase-systemtime,
// 24 is not allowed in the Hour field, and consequently Outlook can't deal with it.
// This is the same workaround that Outlook applies.
if
(
$_timezoneInfo
[
'standardHour'
]
==
24
)
{
$_timezoneInfo
[
'standardHour'
]
=
23
;
$_timezoneInfo
[
'standardMinute'
]
=
59
;
$_timezoneInfo
[
'standardSecond'
]
=
59
;
$_timezoneInfo
[
'standardMilliseconds'
]
=
999
;
}
$packed
=
pack
(
"la64vvvvvvvvla64vvvvvvvvl"
,
$_timezoneInfo
[
'bias'
],
$_timezoneInfo
[
'standardName'
],
$_timezoneInfo
[
'standardYear'
],
$_timezoneInfo
[
'standardMonth'
],
$_timezoneInfo
[
'standardDayOfWeek'
],
$_timezoneInfo
[
'standardWeek'
],
$_timezoneInfo
[
'standardHour'
],
$_timezoneInfo
[
'standardMinute'
],
$_timezoneInfo
[
'standardSecond'
],
$_timezoneInfo
[
'standardMilliseconds'
],
$_timezoneInfo
[
'standardBias'
],
$_timezoneInfo
[
'daylightName'
],
$_timezoneInfo
[
'daylightYear'
],
$_timezoneInfo
[
'daylightMonth'
],
$_timezoneInfo
[
'daylightDayOfWeek'
],
$_timezoneInfo
[
'daylightWeek'
],
$_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
,
'standardWeek'
=>
0
,
'standardHour'
=>
0
,
'standardMinute'
=>
0
,
'standardSecond'
=>
0
,
'standardMilliseconds'
=>
0
,
'standardBias'
=>
0
,
'daylightName'
=>
''
,
'daylightYear'
=>
0
,
'daylightMonth'
=>
0
,
'daylightDayOfWeek'
=>
0
,
'daylightWeek'
=>
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
[
'standardWeek'
])
||
!
empty
(
$value
[
'daylightMonth'
])
||
!
empty
(
$value
[
'daylightWeek'
]))
&&
(
empty
(
$value
[
'standardMonth'
])
||
empty
(
$value
[
'standardWeek'
])
||
empty
(
$value
[
'daylightMonth'
])
||
empty
(
$value
[
'daylightWeek'
]))
)
{
// 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'
]
=
date
(
'Y-m-d'
,
$_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
(
date
(
'Y'
,
$transition
[
'ts'
])
==
$_year
)
{
if
(
isset
(
$transitions
[
$index
+
1
])
&&
date
(
'Y'
,
$transitions
[
$index
][
'ts'
])
==
date
(
'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
);
}
}
File Metadata
Details
Attached
Mime Type
text/x-php
Expires
Mon, Aug 25, 8:17 PM (1 d, 3 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
257295
Default Alt Text
kolab_sync_timezone_converter.php (22 KB)
Attached To
Mode
R4 syncroton
Attached
Detach File
Event Timeline
Log In to Comment