1: <?php
2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17:
18: class Horde_ActiveSync_Timezone
19: {
20: 21: 22: 23: 24: 25:
26: protected $_startDate;
27:
28: 29: 30: 31: 32: 33: 34: 35: 36: 37: 38: 39: 40: 41: 42: 43: 44: 45: 46: 47: 48: 49: 50: 51: 52: 53: 54: 55: 56: 57: 58: 59: 60: 61: 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: 76: 77: 78: 79: 80: 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: 94: 95: 96: 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: 142: 143: 144: 145: 146: 147: 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: 187: 188: 189: 190: 191: 192: 193:
194: static protected function _generateOffsetsForTransition(array $offsets, array $transition, $type)
195: {
196:
197:
198:
199:
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: 220: 221: 222: 223: 224: 225: 226: 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: 240: 241: 242: 243: 244: 245: 246: 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: 276: 277: 278: 279: 280: 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: 299: 300: 301: 302: 303: 304: 305: 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: 319: 320: 321: 322: 323: 324: 325: 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:
336:
337: if ($standardOffset == $std['offset']) {
338: if ((empty($offsets['dstmonth']) && (empty($dst) || empty($dst['isdst']))) ||
339: (empty($dst) && !empty($offsets['dstmonth']))) {
340:
341: return true;
342: }
343: $daylightOffset = ($offsets['bias'] + $offsets['dstbias']) * 60 * -1;
344:
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: 367: 368: 369: 370: 371: 372: 373: 374: 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: