1<?php
2
3namespace Sabre\VObject;
4
5/**
6 * Time zone name translation.
7 *
8 * This file translates well-known time zone names into "Olson database" time zone names.
9 *
10 * @copyright Copyright (C) fruux GmbH (https://fruux.com/)
11 * @author Frank Edelhaeuser (fedel@users.sourceforge.net)
12 * @author Evert Pot (http://evertpot.com/)
13 * @license http://sabre.io/license/ Modified BSD License
14 */
15class TimeZoneUtil {
16
17    static $map = null;
18
19    /**
20     * List of microsoft exchange timezone ids.
21     *
22     * Source: http://msdn.microsoft.com/en-us/library/aa563018(loband).aspx
23     */
24    static $microsoftExchangeMap = [
25        0  => 'UTC',
26        31 => 'Africa/Casablanca',
27
28        // Insanely, id #2 is used for both Europe/Lisbon, and Europe/Sarajevo.
29        // I'm not even kidding.. We handle this special case in the
30        // getTimeZone method.
31        2  => 'Europe/Lisbon',
32        1  => 'Europe/London',
33        4  => 'Europe/Berlin',
34        6  => 'Europe/Prague',
35        3  => 'Europe/Paris',
36        69 => 'Africa/Luanda', // This was a best guess
37        7  => 'Europe/Athens',
38        5  => 'Europe/Bucharest',
39        49 => 'Africa/Cairo',
40        50 => 'Africa/Harare',
41        59 => 'Europe/Helsinki',
42        27 => 'Asia/Jerusalem',
43        26 => 'Asia/Baghdad',
44        74 => 'Asia/Kuwait',
45        51 => 'Europe/Moscow',
46        56 => 'Africa/Nairobi',
47        25 => 'Asia/Tehran',
48        24 => 'Asia/Muscat', // Best guess
49        54 => 'Asia/Baku',
50        48 => 'Asia/Kabul',
51        58 => 'Asia/Yekaterinburg',
52        47 => 'Asia/Karachi',
53        23 => 'Asia/Calcutta',
54        62 => 'Asia/Kathmandu',
55        46 => 'Asia/Almaty',
56        71 => 'Asia/Dhaka',
57        66 => 'Asia/Colombo',
58        61 => 'Asia/Rangoon',
59        22 => 'Asia/Bangkok',
60        64 => 'Asia/Krasnoyarsk',
61        45 => 'Asia/Shanghai',
62        63 => 'Asia/Irkutsk',
63        21 => 'Asia/Singapore',
64        73 => 'Australia/Perth',
65        75 => 'Asia/Taipei',
66        20 => 'Asia/Tokyo',
67        72 => 'Asia/Seoul',
68        70 => 'Asia/Yakutsk',
69        19 => 'Australia/Adelaide',
70        44 => 'Australia/Darwin',
71        18 => 'Australia/Brisbane',
72        76 => 'Australia/Sydney',
73        43 => 'Pacific/Guam',
74        42 => 'Australia/Hobart',
75        68 => 'Asia/Vladivostok',
76        41 => 'Asia/Magadan',
77        17 => 'Pacific/Auckland',
78        40 => 'Pacific/Fiji',
79        67 => 'Pacific/Tongatapu',
80        29 => 'Atlantic/Azores',
81        53 => 'Atlantic/Cape_Verde',
82        30 => 'America/Noronha',
83         8 => 'America/Sao_Paulo', // Best guess
84        32 => 'America/Argentina/Buenos_Aires',
85        60 => 'America/Godthab',
86        28 => 'America/St_Johns',
87         9 => 'America/Halifax',
88        33 => 'America/Caracas',
89        65 => 'America/Santiago',
90        35 => 'America/Bogota',
91        10 => 'America/New_York',
92        34 => 'America/Indiana/Indianapolis',
93        55 => 'America/Guatemala',
94        11 => 'America/Chicago',
95        37 => 'America/Mexico_City',
96        36 => 'America/Edmonton',
97        38 => 'America/Phoenix',
98        12 => 'America/Denver', // Best guess
99        13 => 'America/Los_Angeles', // Best guess
100        14 => 'America/Anchorage',
101        15 => 'Pacific/Honolulu',
102        16 => 'Pacific/Midway',
103        39 => 'Pacific/Kwajalein',
104    ];
105
106    /**
107     * This method will try to find out the correct timezone for an iCalendar
108     * date-time value.
109     *
110     * You must pass the contents of the TZID parameter, as well as the full
111     * calendar.
112     *
113     * If the lookup fails, this method will return the default PHP timezone
114     * (as configured using date_default_timezone_set, or the date.timezone ini
115     * setting).
116     *
117     * Alternatively, if $failIfUncertain is set to true, it will throw an
118     * exception if we cannot accurately determine the timezone.
119     *
120     * @param string $tzid
121     * @param Sabre\VObject\Component $vcalendar
122     *
123     * @return DateTimeZone
124     */
125    static function getTimeZone($tzid, Component $vcalendar = null, $failIfUncertain = false) {
126
127        // First we will just see if the tzid is a support timezone identifier.
128        //
129        // The only exception is if the timezone starts with (. This is to
130        // handle cases where certain microsoft products generate timezone
131        // identifiers that for instance look like:
132        //
133        // (GMT+01.00) Sarajevo/Warsaw/Zagreb
134        //
135        // Since PHP 5.5.10, the first bit will be used as the timezone and
136        // this method will return just GMT+01:00. This is wrong, because it
137        // doesn't take DST into account.
138        if ($tzid[0] !== '(') {
139
140            // PHP has a bug that logs PHP warnings even it shouldn't:
141            // https://bugs.php.net/bug.php?id=67881
142            //
143            // That's why we're checking if we'll be able to successfull instantiate
144            // \DateTimeZone() before doing so. Otherwise we could simply instantiate
145            // and catch the exception.
146            $tzIdentifiers = \DateTimeZone::listIdentifiers();
147
148            try {
149                if (
150                    (in_array($tzid, $tzIdentifiers)) ||
151                    (preg_match('/^GMT(\+|-)([0-9]{4})$/', $tzid, $matches)) ||
152                    (in_array($tzid, self::getIdentifiersBC()))
153                ) {
154                    return new \DateTimeZone($tzid);
155                }
156            } catch (\Exception $e) {
157            }
158
159        }
160
161        self::loadTzMaps();
162
163        // Next, we check if the tzid is somewhere in our tzid map.
164        if (isset(self::$map[$tzid])) {
165            return new \DateTimeZone(self::$map[$tzid]);
166        }
167
168        // Some Microsoft products prefix the offset first, so let's strip that off
169        // and see if it is our tzid map.  We don't want to check for this first just
170        // in case there are overrides in our tzid map.
171        if (preg_match('/^\((UTC|GMT)(\+|\-)[\d]{2}\:[\d]{2}\) (.*)/', $tzid, $matches)) {
172            $tzidAlternate = $matches[3];
173            if (isset(self::$map[$tzidAlternate])) {
174                return new \DateTimeZone(self::$map[$tzidAlternate]);
175            }
176        }
177
178        // Maybe the author was hyper-lazy and just included an offset. We
179        // support it, but we aren't happy about it.
180        if (preg_match('/^GMT(\+|-)([0-9]{4})$/', $tzid, $matches)) {
181
182            // Note that the path in the source will never be taken from PHP 5.5.10
183            // onwards. PHP 5.5.10 supports the "GMT+0100" style of format, so it
184            // already gets returned early in this function. Once we drop support
185            // for versions under PHP 5.5.10, this bit can be taken out of the
186            // source.
187            // @codeCoverageIgnoreStart
188            return new \DateTimeZone('Etc/GMT' . $matches[1] . ltrim(substr($matches[2], 0, 2), '0'));
189            // @codeCoverageIgnoreEnd
190        }
191
192        if ($vcalendar) {
193
194            // If that didn't work, we will scan VTIMEZONE objects
195            foreach ($vcalendar->select('VTIMEZONE') as $vtimezone) {
196
197                if ((string)$vtimezone->TZID === $tzid) {
198
199                    // Some clients add 'X-LIC-LOCATION' with the olson name.
200                    if (isset($vtimezone->{'X-LIC-LOCATION'})) {
201
202                        $lic = (string)$vtimezone->{'X-LIC-LOCATION'};
203
204                        // Libical generators may specify strings like
205                        // "SystemV/EST5EDT". For those we must remove the
206                        // SystemV part.
207                        if (substr($lic, 0, 8) === 'SystemV/') {
208                            $lic = substr($lic, 8);
209                        }
210
211                        return self::getTimeZone($lic, null, $failIfUncertain);
212
213                    }
214                    // Microsoft may add a magic number, which we also have an
215                    // answer for.
216                    if (isset($vtimezone->{'X-MICROSOFT-CDO-TZID'})) {
217                        $cdoId = (int)$vtimezone->{'X-MICROSOFT-CDO-TZID'}->getValue();
218
219                        // 2 can mean both Europe/Lisbon and Europe/Sarajevo.
220                        if ($cdoId === 2 && strpos((string)$vtimezone->TZID, 'Sarajevo') !== false) {
221                            return new \DateTimeZone('Europe/Sarajevo');
222                        }
223
224                        if (isset(self::$microsoftExchangeMap[$cdoId])) {
225                            return new \DateTimeZone(self::$microsoftExchangeMap[$cdoId]);
226                        }
227                    }
228
229                }
230
231            }
232
233        }
234
235        if ($failIfUncertain) {
236            throw new \InvalidArgumentException('We were unable to determine the correct PHP timezone for tzid: ' . $tzid);
237        }
238
239        // If we got all the way here, we default to UTC.
240        return new \DateTimeZone(date_default_timezone_get());
241
242    }
243
244    /**
245     * This method will load in all the tz mapping information, if it's not yet
246     * done.
247     */
248    static function loadTzMaps() {
249
250        if (!is_null(self::$map)) return;
251
252        self::$map = array_merge(
253            include __DIR__ . '/timezonedata/windowszones.php',
254            include __DIR__ . '/timezonedata/lotuszones.php',
255            include __DIR__ . '/timezonedata/exchangezones.php',
256            include __DIR__ . '/timezonedata/php-workaround.php'
257        );
258
259    }
260
261    /**
262     * This method returns an array of timezone identifiers, that are supported
263     * by DateTimeZone(), but not returned by DateTimeZone::listIdentifiers().
264     *
265     * We're not using DateTimeZone::listIdentifiers(DateTimeZone::ALL_WITH_BC) because:
266     * - It's not supported by some PHP versions as well as HHVM.
267     * - It also returns identifiers, that are invalid values for new DateTimeZone() on some PHP versions.
268     * (See timezonedata/php-bc.php and timezonedata php-workaround.php)
269     *
270     * @return array
271     */
272    static function getIdentifiersBC() {
273        return include __DIR__ . '/timezonedata/php-bc.php';
274    }
275
276}
277