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