1<?php 2/** 3 * Horde_ActiveSync_Timezone:: 4 * 5 * @license http://www.horde.org/licenses/gpl GPLv2 6 * 7 * @copyright 2009-2020 Horde LLC (http://www.horde.org) 8 * @author Michael J Rubinsky <mrubinsk@horde.org> 9 * @package ActiveSync 10 */ 11/** 12 * Utility functions for dealing with Microsoft ActiveSync's Timezone format. 13 * 14 * Copyright 2009-2020 Horde LLC (http://www.horde.org/) 15 * 16 * See the enclosed file LICENSE for license information. If you 17 * did not receive this file, see http://www.horde.org/licenses/gpl. 18 * 19 * Code dealing with searching for a timezone identifier from an AS timezone 20 * blob inspired by code in the Tine20 Project (http://tine20.org). 21 * 22 * @license http://www.horde.org/licenses/gpl GPLv2 23 * 24 * @copyright 2009-2020 Horde LLC (http://www.horde.org) 25 * @author Michael J Rubinsky <mrubinsk@horde.org> 26 * @package ActiveSync 27 * @deprecated To be removed for Horde 6. Use Horde_Mapi_Timezone::. 28 */ 29class Horde_ActiveSync_Timezone 30{ 31 /** 32 * Date to use as start date when iterating through offsets looking for a 33 * transition. 34 * 35 * @var Horde_Date 36 */ 37 protected $_startDate; 38 39 /** 40 * Convert a timezone from the ActiveSync base64 structure to a TZ offset 41 * hash. 42 * 43 * @param base64 encoded timezone structure defined by MS as: 44 * <pre> 45 * typedef struct TIME_ZONE_INFORMATION { 46 * LONG Bias; 47 * WCHAR StandardName[32]; 48 * SYSTEMTIME StandardDate; 49 * LONG StandardBias; 50 * WCHAR DaylightName[32]; 51 * SYSTEMTIME DaylightDate; 52 * LONG DaylightBias;}; 53 * </pre> 54 * 55 * With the SYSTEMTIME format being: 56 * <pre> 57 * typedef struct _SYSTEMTIME { 58 * WORD wYear; 59 * WORD wMonth; 60 * WORD wDayOfWeek; 61 * WORD wDay; 62 * WORD wHour; 63 * WORD wMinute; 64 * WORD wSecond; 65 * WORD wMilliseconds; 66 * } SYSTEMTIME, *PSYSTEMTIME; 67 * </pre> 68 * 69 * See: http://msdn.microsoft.com/en-us/library/ms724950%28VS.85%29.aspx 70 * and: http://msdn.microsoft.com/en-us/library/ms725481%28VS.85%29.aspx 71 * 72 * @return array Hash of offset information 73 */ 74 public static function getOffsetsFromSyncTZ($data) 75 { 76 if (version_compare(PHP_VERSION, '5.5', '>=')) { 77 $format = 'lbias/Z64stdname/vstdyear/vstdmonth/vstdday/vstdweek/vstdhour/vstdminute/vstdsecond/vstdmillis/' 78 . 'lstdbias/Z64dstname/vdstyear/vdstmonth/vdstday/vdstweek/vdsthour/vdstminute/vdstsecond/vdstmillis/' 79 . 'ldstbias'; 80 } else { 81 $format = 'lbias/a64stdname/vstdyear/vstdmonth/vstdday/vstdweek/vstdhour/vstdminute/vstdsecond/vstdmillis/' 82 . 'lstdbias/a64dstname/vdstyear/vdstmonth/vdstday/vdstweek/vdsthour/vdstminute/vdstsecond/vdstmillis/' 83 . 'ldstbias'; 84 } 85 $tz = unpack($format, base64_decode($data)); 86 $tz['timezone'] = $tz['bias']; 87 $tz['timezonedst'] = $tz['dstbias']; 88 89 if (!Horde_Mapi::isLittleEndian()) { 90 $tz['bias'] = self::_chbo($tz['bias']); 91 $tz['stdbias'] = self::_chbo($tz['stdbias']); 92 $tz['dstbias'] = self::_chbo($tz['dstbias']); 93 } 94 95 return $tz; 96 } 97 98 /** 99 * Build an ActiveSync TZ blob given a TZ Offset hash. 100 * 101 * @param array $offsets A TZ offset hash 102 * 103 * @return string A base64_encoded ActiveSync Timezone structure suitable 104 * for transmitting via wbxml. 105 */ 106 public static function getSyncTZFromOffsets(array $offsets) 107 { 108 if (!Horde_Mapi::isLittleEndian()) { 109 $offsets['bias'] = self::_chbo($offsets['bias']); 110 $offsets['stdbias'] = self::_chbo($offsets['stdbias']); 111 $offsets['dstbias'] = self::_chbo($offsets['dstbias']); 112 } 113 114 $packed = pack('la64vvvvvvvvla64vvvvvvvvl', 115 $offsets['bias'], '', 0, $offsets['stdmonth'], $offsets['stdday'], $offsets['stdweek'], $offsets['stdhour'], $offsets['stdminute'], $offsets['stdsecond'], $offsets['stdmillis'], 116 $offsets['stdbias'], '', 0, $offsets['dstmonth'], $offsets['dstday'], $offsets['dstweek'], $offsets['dsthour'], $offsets['dstminute'], $offsets['dstsecond'], $offsets['dstmillis'], 117 $offsets['dstbias']); 118 119 return base64_encode($packed); 120 } 121 122 /** 123 * Create a offset hash suitable for use in ActiveSync transactions 124 * 125 * @param Horde_Date $date A date object representing the date to base the 126 * the tz data on. 127 * 128 * @return array An offset hash. 129 */ 130 public static function getOffsetsFromDate(Horde_Date $date) 131 { 132 $offsets = array( 133 'bias' => 0, 134 'stdname' => '', 135 'stdyear' => 0, 136 'stdmonth' => 0, 137 'stdday' => 0, 138 'stdweek' => 0, 139 'stdhour' => 0, 140 'stdminute' => 0, 141 'stdsecond' => 0, 142 'stdmillis' => 0, 143 'stdbias' => 0, 144 'dstname' => '', 145 'dstyear' => 0, 146 'dstmonth' => 0, 147 'dstday' => 0, 148 'dstweek' => 0, 149 'dsthour' => 0, 150 'dstminute' => 0, 151 'dstsecond' => 0, 152 'dstmillis' => 0, 153 'dstbias' => 0 154 ); 155 156 $timezone = $date->toDateTime()->getTimezone(); 157 list($std, $dst) = self::_getTransitions($timezone, $date); 158 if ($std) { 159 $offsets['bias'] = $std['offset'] / 60 * -1; 160 if ($dst) { 161 $offsets = self::_generateOffsetsForTransition($offsets, $std, 'std'); 162 $offsets = self::_generateOffsetsForTransition($offsets, $dst, 'dst'); 163 $offsets['stdhour'] += $dst['offset'] / 3600; 164 $offsets['dsthour'] += $std['offset'] / 3600; 165 $offsets['dstbias'] = ($dst['offset'] - $std['offset']) / 60 * -1; 166 } 167 } 168 169 return $offsets; 170 } 171 172 /** 173 * Get the transition data for moving from DST to STD time. 174 * 175 * @param DateTimeZone $timezone The timezone to get the transition for 176 * @param Horde_Date $date The date to start from. Really only the 177 * year we are interested in is needed. 178 * 179 * @return array An array containing the the STD and DST transitions 180 */ 181 protected static function _getTransitions(DateTimeZone $timezone, Horde_Date $date) 182 { 183 184 $std = $dst = array(); 185 $transitions = $timezone->getTransitions( 186 mktime(0, 0, 0, 12, 1, $date->year - 1), 187 mktime(24, 0, 0, 12, 31, $date->year) 188 ); 189 190 foreach ($transitions as $i => $transition) { 191 try { 192 $d = new Horde_Date($transition['time']); 193 $d->setTimezone('UTC'); 194 } catch (Exception $e) { 195 continue; 196 } 197 if (($d->format('Y') == $date->format('Y')) && isset($transitions[$i + 1])) { 198 $next = new Horde_Date($transitions[$i + 1]['ts']); 199 if ($d->format('Y') == $next->format('Y')) { 200 $dst = $transition['isdst'] ? $transition : $transitions[$i + 1]; 201 $std = $transition['isdst'] ? $transitions[$i + 1] : $transition; 202 } else { 203 $dst = $transition['isdst'] ? $transition: null; 204 $std = $transition['isdst'] ? null : $transition; 205 } 206 break; 207 } elseif ($i == count($transitions) - 1) { 208 $std = $transition; 209 } 210 } 211 212 return array($std, $dst); 213 } 214 215 /** 216 * Calculate the offsets for the specified transition 217 * 218 * @param array $offsets A TZ offset hash 219 * @param array $transition A transition hash 220 * @param string $type Transition type - dst or std 221 * 222 * @return array A populated offset hash 223 */ 224 protected static function _generateOffsetsForTransition(array $offsets, array $transition, $type) 225 { 226 // We can't use Horde_Date directly here, since it is unable to 227 // properly convert to UTC from local ON the exact hour of a std -> dst 228 // transition. This is due to a conversion to DateTime in the localtime 229 // zone internally before the timezone change is applied 230 $transitionDate = new DateTime($transition['time']); 231 $transitionDate->setTimezone(new DateTimeZone('UTC')); 232 $transitionDate = new Horde_Date($transitionDate); 233 $offsets[$type . 'month'] = $transitionDate->format('n'); 234 $offsets[$type . 'day'] = $transitionDate->format('w'); 235 $offsets[$type . 'minute'] = (int)$transitionDate->format('i'); 236 $offsets[$type . 'hour'] = (int)$transitionDate->format('H'); 237 for ($i = 5; $i > 0; $i--) { 238 if (self::_isNthOcurrenceOfWeekdayInMonth($transition['ts'], $i)) { 239 $offsets[$type . 'week'] = $i; 240 break; 241 } 242 } 243 244 return $offsets; 245 } 246 247 /** 248 * Attempt to guess the timezone identifier from the $offsets array. 249 * 250 * @param array|string $offsets The timezone to check. Either an array 251 * of offsets or an activesynz tz blob. 252 * @param string $expectedTimezone The expected timezone. If not empty, and 253 * present in the results, will return. 254 * 255 * @return string The timezone identifier 256 */ 257 public function getTimezone($offsets, $expectedTimezone = null) 258 { 259 $timezones = $this->getListOfTimezones($offsets, $expectedTimezone); 260 if (isset($timezones[$expectedTimezone])) { 261 return $expectedTimezone; 262 } else { 263 return current($timezones); 264 } 265 } 266 267 /** 268 * Get the list of timezone identifiers that match the given offsets, having 269 * a preference for $expectedTimezone if it's present in the results. 270 * 271 * @param array|string $offsets Either an offset array, or a AS timezone 272 * structure. 273 * @param string $expectedTimezone The expected timezone. 274 * 275 * @return array An array of timezone identifiers 276 */ 277 public function getListOfTimezones($offsets, $expectedTimezone = null) 278 { 279 if (is_string($offsets)) { 280 $offsets = self::getOffsetsFromSyncTZ($offsets); 281 } 282 $this->_setDefaultStartDate($offsets); 283 $timezones = array(); 284 foreach (DateTimeZone::listIdentifiers() as $timezoneIdentifier) { 285 $timezone = new DateTimeZone($timezoneIdentifier); 286 if (false !== ($matchingTransition = $this->_checkTimezone($timezone, $offsets))) { 287 if ($timezoneIdentifier == $expectedTimezone) { 288 $timezones = array($timezoneIdentifier => $matchingTransition['abbr']); 289 break; 290 } else { 291 $timezones[$timezoneIdentifier] = $matchingTransition['abbr']; 292 } 293 } 294 } 295 296 if (empty($timezones)) { 297 throw new Horde_ActiveSync_Exception('No timezone found for the given offsets'); 298 } 299 300 return $timezones; 301 } 302 303 /** 304 * Set default value for $_startDate. 305 * 306 * Tries to guess the correct startDate depending on object property falls 307 * back to current date. 308 * 309 * @param array $offsets Offsets may be avaluated for a given start year 310 */ 311 protected function _setDefaultStartDate(array $offsets = null) 312 { 313 if (!empty($this->_startDate)) { 314 return; 315 } 316 317 if (!empty($offsets['stdyear'])) { 318 $this->_startDate = new Horde_Date($offsets['stdyear'] . '-01-01'); 319 } else { 320 $start = new Horde_Date(time()); 321 $start->year--; 322 $this->_startDate = $start; 323 } 324 } 325 326 /** 327 * Check if the given timezone matches the offsets and also evaluate the 328 * daylight saving time transitions for this timezone if necessary. 329 * 330 * @param DateTimeZone $timezone The timezone to check. 331 * @param array $offsets The offsets to check. 332 * 333 * @return array|boolean An array of transition data or false if timezone 334 * does not match offset. 335 */ 336 protected function _checkTimezone(DateTimeZone $timezone, array $offsets) 337 { 338 list($std, $dst) = $this->_getTransitions($timezone, $this->_startDate); 339 if ($this->_checkTransition($std, $dst, $offsets)) { 340 return $std; 341 } 342 343 return false; 344 } 345 346 /** 347 * Check if the given standardTransition and daylightTransition match to the 348 * given offsets. 349 * 350 * @param array $std The Standard transition date. 351 * @param array $dst The DST transition date. 352 * @param array $offsets The offsets to check. 353 * 354 * @return boolean 355 */ 356 protected function _checkTransition(array $std, array $dst, array $offsets) 357 { 358 if (empty($std) || empty($offsets)) { 359 return false; 360 } 361 362 $standardOffset = ($offsets['bias'] + $offsets['stdbias']) * 60 * -1; 363 364 // check each condition in a single if statement and break the chain 365 // when one condition is not met - for performance reasons 366 if ($standardOffset == $std['offset']) { 367 if ((empty($offsets['dstmonth']) && (empty($dst) || empty($dst['isdst']))) || 368 (empty($dst) && !empty($offsets['dstmonth']))) { 369 // Offset contains DST, but no dst to compare 370 return true; 371 } 372 $daylightOffset = ($offsets['bias'] + $offsets['dstbias']) * 60 * -1; 373 // the milestone is sending a positive value for daylightBias while it should send a negative value 374 $daylightOffsetMilestone = ($offsets['dstbias'] + ($offsets['dstbias'] * -1) ) * 60 * -1; 375 376 if ($daylightOffset == $dst['offset'] || $daylightOffsetMilestone == $dst['offset']) { 377 $standardParsed = new DateTime($std['time']); 378 $daylightParsed = new DateTime($dst['time']); 379 380 if ($standardParsed->format('n') == $offsets['stdmonth'] && 381 $daylightParsed->format('n') == $offsets['dstmonth'] && 382 $standardParsed->format('w') == $offsets['stdday'] && 383 $daylightParsed->format('w') == $offsets['dstday']) 384 { 385 return self::_isNthOcurrenceOfWeekdayInMonth($dst['ts'], $offsets['dstweek']) && 386 self::_isNthOcurrenceOfWeekdayInMonth($std['ts'], $offsets['stdweek']); 387 } 388 } 389 } 390 391 return false; 392 } 393 394 /** 395 * Test if the weekday of the given timestamp is the nth occurence of this 396 * weekday within its month, where '5' indicates the last occurrence even if 397 * there is less than five occurrences. 398 * 399 * @param integer $timestamp The timestamp to check. 400 * @param integer $occurence 1 to 5, where 5 indicates the final occurrence 401 * during the month if that day of the week does 402 * not occur 5 times 403 * @return boolean 404 */ 405 protected static function _isNthOcurrenceOfWeekdayInMonth($timestamp, $occurence) 406 { 407 $original = new Horde_Date($timestamp); 408 $original->setTimezone('UTC'); 409 if ($occurence == 5) { 410 $modified = $original->add(array('mday' => 7)); 411 return $modified->month > $original->month; 412 } else { 413 $modified = $original->sub(array('mday' => 7 * $occurence)); 414 $modified2 = $original->sub(array('mday' => 7 * ($occurence - 1))); 415 416 return $modified->month < $original->month && 417 $modified2->month == $original->month; 418 } 419 } 420 421 /** 422 * Change the byte order of a number. Used to allow big endian machines to 423 * decode the timezone blobs, which are encoded in little endian order. 424 * 425 * @param integer $num The number to reverse. 426 * 427 * @return integer The number, in the reverse byte order. 428 */ 429 protected static function _chbo($num) { 430 $u = unpack('l', strrev(pack('l', $num))); 431 432 return $u[1]; 433 } 434 435} 436