Overview

Packages

  • ActiveSync
  • None

Classes

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