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 public 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 public 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 public 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 // PHP has a bug that logs PHP warnings even it shouldn't: 140 // https://bugs.php.net/bug.php?id=67881 141 // 142 // That's why we're checking if we'll be able to successfully instantiate 143 // \DateTimeZone() before doing so. Otherwise we could simply instantiate 144 // and catch the exception. 145 $tzIdentifiers = \DateTimeZone::listIdentifiers(); 146 147 try { 148 if ( 149 (in_array($tzid, $tzIdentifiers)) || 150 (preg_match('/^GMT(\+|-)([0-9]{4})$/', $tzid, $matches)) || 151 (in_array($tzid, self::getIdentifiersBC())) 152 ) { 153 return new \DateTimeZone($tzid); 154 } 155 } catch (\Exception $e) { 156 } 157 } 158 159 self::loadTzMaps(); 160 161 // Next, we check if the tzid is somewhere in our tzid map. 162 if (isset(self::$map[$tzid])) { 163 return new \DateTimeZone(self::$map[$tzid]); 164 } 165 166 // Some Microsoft products prefix the offset first, so let's strip that off 167 // and see if it is our tzid map. We don't want to check for this first just 168 // in case there are overrides in our tzid map. 169 if (preg_match('/^\((UTC|GMT)(\+|\-)[\d]{2}\:[\d]{2}\) (.*)/', $tzid, $matches)) { 170 $tzidAlternate = $matches[3]; 171 if (isset(self::$map[$tzidAlternate])) { 172 return new \DateTimeZone(self::$map[$tzidAlternate]); 173 } 174 } 175 176 // Maybe the author was hyper-lazy and just included an offset. We 177 // support it, but we aren't happy about it. 178 if (preg_match('/^GMT(\+|-)([0-9]{4})$/', $tzid, $matches)) { 179 // Note that the path in the source will never be taken from PHP 5.5.10 180 // onwards. PHP 5.5.10 supports the "GMT+0100" style of format, so it 181 // already gets returned early in this function. Once we drop support 182 // for versions under PHP 5.5.10, this bit can be taken out of the 183 // source. 184 // @codeCoverageIgnoreStart 185 return new \DateTimeZone('Etc/GMT'.$matches[1].ltrim(substr($matches[2], 0, 2), '0')); 186 // @codeCoverageIgnoreEnd 187 } 188 189 if ($vcalendar) { 190 // If that didn't work, we will scan VTIMEZONE objects 191 foreach ($vcalendar->select('VTIMEZONE') as $vtimezone) { 192 if ((string) $vtimezone->TZID === $tzid) { 193 // Some clients add 'X-LIC-LOCATION' with the olson name. 194 if (isset($vtimezone->{'X-LIC-LOCATION'})) { 195 $lic = (string) $vtimezone->{'X-LIC-LOCATION'}; 196 197 // Libical generators may specify strings like 198 // "SystemV/EST5EDT". For those we must remove the 199 // SystemV part. 200 if ('SystemV/' === substr($lic, 0, 8)) { 201 $lic = substr($lic, 8); 202 } 203 204 return self::getTimeZone($lic, null, $failIfUncertain); 205 } 206 // Microsoft may add a magic number, which we also have an 207 // answer for. 208 if (isset($vtimezone->{'X-MICROSOFT-CDO-TZID'})) { 209 $cdoId = (int) $vtimezone->{'X-MICROSOFT-CDO-TZID'}->getValue(); 210 211 // 2 can mean both Europe/Lisbon and Europe/Sarajevo. 212 if (2 === $cdoId && false !== strpos((string) $vtimezone->TZID, 'Sarajevo')) { 213 return new \DateTimeZone('Europe/Sarajevo'); 214 } 215 216 if (isset(self::$microsoftExchangeMap[$cdoId])) { 217 return new \DateTimeZone(self::$microsoftExchangeMap[$cdoId]); 218 } 219 } 220 } 221 } 222 } 223 224 if ($failIfUncertain) { 225 throw new \InvalidArgumentException('We were unable to determine the correct PHP timezone for tzid: '.$tzid); 226 } 227 228 // If we got all the way here, we default to UTC. 229 return new \DateTimeZone(date_default_timezone_get()); 230 } 231 232 /** 233 * This method will load in all the tz mapping information, if it's not yet 234 * done. 235 */ 236 public static function loadTzMaps() 237 { 238 if (!is_null(self::$map)) { 239 return; 240 } 241 242 self::$map = array_merge( 243 include __DIR__.'/timezonedata/windowszones.php', 244 include __DIR__.'/timezonedata/lotuszones.php', 245 include __DIR__.'/timezonedata/exchangezones.php', 246 include __DIR__.'/timezonedata/php-workaround.php' 247 ); 248 } 249 250 /** 251 * This method returns an array of timezone identifiers, that are supported 252 * by DateTimeZone(), but not returned by DateTimeZone::listIdentifiers(). 253 * 254 * We're not using DateTimeZone::listIdentifiers(DateTimeZone::ALL_WITH_BC) because: 255 * - It's not supported by some PHP versions as well as HHVM. 256 * - It also returns identifiers, that are invalid values for new DateTimeZone() on some PHP versions. 257 * (See timezonedata/php-bc.php and timezonedata php-workaround.php) 258 * 259 * @return array 260 */ 261 public static function getIdentifiersBC() 262 { 263 return include __DIR__.'/timezonedata/php-bc.php'; 264 } 265} 266