Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 3.11.x will end 14 Nov 2022 (12 months plus 6 months extension).
  • Bug fixes for security issues in 3.11.x will end 13 Nov 2023 (18 months plus 12 months extension).
  • PHP version: minimum PHP 7.3.0 Note: minimum PHP version has increased since Moodle 3.10. PHP 7.4.x is supported too.

Differences Between: [Versions 311 and 401] [Versions 311 and 402] [Versions 311 and 403] [Versions 39 and 311]

   1  <?php
   2  // This file is part of Moodle - http://moodle.org/
   3  //
   4  // Moodle is free software: you can redistribute it and/or modify
   5  // it under the terms of the GNU General Public License as published by
   6  // the Free Software Foundation, either version 3 of the License, or
   7  // (at your option) any later version.
   8  //
   9  // Moodle is distributed in the hope that it will be useful,
  10  // but WITHOUT ANY WARRANTY; without even the implied warranty of
  11  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  12  // GNU General Public License for more details.
  13  //
  14  // You should have received a copy of the GNU General Public License
  15  // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
  16  
  17  /**
  18   * Core date and time related code.
  19   *
  20   * @package   core
  21   * @copyright 2015 Totara Learning Solutions Ltd {@link http://www.totaralms.com/}
  22   * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  23   * @author    Petr Skoda <petr.skoda@totaralms.com>
  24   */
  25  
  26  /**
  27   * Core date and time related code.
  28   *
  29   * @since Moodle 2.9
  30   * @package   core
  31   * @copyright 2015 Totara Learning Solutions Ltd {@link http://www.totaralms.com/}
  32   * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  33   * @author    Petr Skoda <petr.skoda@totaralms.com>
  34   */
  35  class core_date {
  36      /** @var array list of recommended zones */
  37      protected static $goodzones = null;
  38  
  39      /** @var array list of BC zones supported by PHP */
  40      protected static $bczones = null;
  41  
  42      /** @var array mapping of timezones not supported by PHP */
  43      protected static $badzones = null;
  44  
  45      /** @var string the default PHP timezone right after config.php */
  46      protected static $defaultphptimezone = null;
  47  
  48      /**
  49       * Returns a localised list of timezones.
  50       * @param string $currentvalue
  51       * @param bool $include99 should the server timezone info be included?
  52       * @return array
  53       */
  54      public static function get_list_of_timezones($currentvalue = null, $include99 = false) {
  55          self::init_zones();
  56  
  57          // Localise first.
  58          $timezones = array();
  59          foreach (self::$goodzones as $tzkey => $ignored) {
  60              $timezones[$tzkey] = self::get_localised_timezone($tzkey);
  61          }
  62          core_collator::asort($timezones);
  63  
  64          // Add '99' if requested.
  65          if ($include99 or $currentvalue == 99) {
  66              $timezones['99'] = self::get_localised_timezone('99');
  67          }
  68  
  69          if (!isset($currentvalue) or isset($timezones[$currentvalue])) {
  70              return $timezones;
  71          }
  72  
  73          if (is_numeric($currentvalue)) {
  74              // UTC offset.
  75              if ($currentvalue == 0) {
  76                  $a = 'UTC';
  77              } else {
  78                  $modifier = ($currentvalue > 0) ? '+' : '';
  79                  $a = 'UTC' . $modifier . number_format($currentvalue, 1);
  80              }
  81              $timezones[$currentvalue] = get_string('timezoneinvalid', 'core_admin', $a);
  82          } else {
  83              // Some string we don't recognise.
  84              $timezones[$currentvalue] = get_string('timezoneinvalid', 'core_admin', $currentvalue);
  85          }
  86  
  87          return $timezones;
  88      }
  89  
  90      /**
  91       * Returns localised timezone name.
  92       * @param string $tz
  93       * @return string
  94       */
  95      public static function get_localised_timezone($tz) {
  96          if ($tz == 99) {
  97              $tz = self::get_server_timezone();
  98              $tz = self::get_localised_timezone($tz);
  99              return get_string('timezoneserver', 'core_admin', $tz);
 100          }
 101  
 102          if (get_string_manager()->string_exists(strtolower($tz), 'core_timezones')) {
 103              $tz = get_string(strtolower($tz), 'core_timezones');
 104          } else if ($tz === 'GMT' or $tz === 'Etc/GMT' or $tz === 'Etc/UTC') {
 105              $tz = 'UTC';
 106          } else if (preg_match('|^Etc/GMT([+-])([0-9]+)$|', $tz, $matches)) {
 107              $sign = $matches[1] === '+' ? '-' : '+';
 108              $tz = 'UTC' . $sign . $matches[2];
 109          }
 110  
 111          return $tz;
 112      }
 113  
 114      /**
 115       * Normalise the timezone name. If timezone not supported
 116       * this method falls back to server timezone (if valid)
 117       * or default PHP timezone.
 118       *
 119       * @param int|string|float|DateTimeZone $tz
 120       * @return string timezone compatible with PHP
 121       */
 122      public static function normalise_timezone($tz) {
 123          global $CFG;
 124  
 125          if ($tz instanceof DateTimeZone) {
 126              return $tz->getName();
 127          }
 128  
 129          self::init_zones();
 130          $tz = (string)$tz;
 131  
 132          if (isset(self::$goodzones[$tz]) or isset(self::$bczones[$tz])) {
 133              return $tz;
 134          }
 135  
 136          $fixed = false;
 137          if (isset(self::$badzones[$tz])) {
 138              // Convert to known zone.
 139              $tz = self::$badzones[$tz];
 140              $fixed = true;
 141          } else if (is_numeric($tz)) {
 142              // Half hour numeric offsets were already tested, try rounding to integers here.
 143              $roundedtz = (string)(int)$tz;
 144              if (isset(self::$badzones[$roundedtz])) {
 145                  $tz = self::$badzones[$roundedtz];
 146                  $fixed = true;
 147              }
 148          }
 149  
 150          if ($fixed and isset(self::$goodzones[$tz]) or isset(self::$bczones[$tz])) {
 151              return $tz;
 152          }
 153  
 154          // Is server timezone usable?
 155          if (isset($CFG->timezone) and !is_numeric($CFG->timezone)) {
 156              $result = @timezone_open($CFG->timezone); // Hide notices if invalid.
 157              if ($result !== false) {
 158                  return $result->getName();
 159              }
 160          }
 161  
 162          // Bad luck, use the php.ini default or value set in config.php.
 163          return self::get_default_php_timezone();
 164      }
 165  
 166      /**
 167       * Returns server timezone.
 168       * @return string normalised timezone name compatible with PHP
 169       **/
 170      public static function get_server_timezone() {
 171          global $CFG;
 172  
 173          if (!isset($CFG->timezone) or $CFG->timezone == 99 or $CFG->timezone === '') {
 174              return self::get_default_php_timezone();
 175          }
 176  
 177          return self::normalise_timezone($CFG->timezone);
 178      }
 179  
 180      /**
 181       * Returns server timezone.
 182       * @return DateTimeZone
 183       **/
 184      public static function get_server_timezone_object() {
 185          $tz = self::get_server_timezone();
 186          return new DateTimeZone($tz);
 187      }
 188  
 189      /**
 190       * Set PHP default timezone to $CFG->timezone.
 191       */
 192      public static function set_default_server_timezone() {
 193          global $CFG;
 194  
 195          if (!isset($CFG->timezone) or $CFG->timezone == 99 or $CFG->timezone === '') {
 196              date_default_timezone_set(self::get_default_php_timezone());
 197              return;
 198          }
 199  
 200          $current = date_default_timezone_get();
 201          if ($current === $CFG->timezone) {
 202              // Nothing to do.
 203              return;
 204          }
 205  
 206          if (!isset(self::$goodzones)) {
 207              // For better performance try do do this without full tz init,
 208              // because this is called from lib/setup.php file on each page.
 209              $result = @timezone_open($CFG->timezone); // Ignore error if setting invalid.
 210              if ($result !== false) {
 211                  date_default_timezone_set($result->getName());
 212                  return;
 213              }
 214          }
 215  
 216          // Slow way is the last option.
 217          date_default_timezone_set(self::get_server_timezone());
 218      }
 219  
 220      /**
 221       * Returns user timezone.
 222       *
 223       * Ideally the parameter should be a real user record,
 224       * unfortunately the legacy code is using 99 for both server
 225       * and default value.
 226       *
 227       * Example of using legacy API:
 228       *    // Date for other user via legacy API.
 229       *    $datestr = userdate($time, core_date::get_user_timezone($user));
 230       *
 231       * The coding style rules in Moodle are moronic,
 232       * why cannot the parameter names have underscores in them?
 233       *
 234       * @param mixed $userorforcedtz user object or legacy forced timezone string or tz object
 235       * @return string normalised timezone name compatible with PHP
 236       */
 237      public static function get_user_timezone($userorforcedtz = null) {
 238          global $USER, $CFG;
 239  
 240          if ($userorforcedtz instanceof DateTimeZone) {
 241              return $userorforcedtz->getName();
 242          }
 243  
 244          if (isset($userorforcedtz) and !is_object($userorforcedtz) and $userorforcedtz != 99) {
 245              // Legacy code is forcing timezone in legacy API.
 246              return self::normalise_timezone($userorforcedtz);
 247          }
 248  
 249          if (isset($CFG->forcetimezone) and $CFG->forcetimezone != 99) {
 250              // Override any user timezone.
 251              return self::normalise_timezone($CFG->forcetimezone);
 252          }
 253  
 254          if ($userorforcedtz === null) {
 255              $tz = isset($USER->timezone) ? $USER->timezone : 99;
 256  
 257          } else if (is_object($userorforcedtz)) {
 258              $tz = isset($userorforcedtz->timezone) ? $userorforcedtz->timezone : 99;
 259  
 260          } else {
 261              if ($userorforcedtz == 99) {
 262                  $tz = isset($USER->timezone) ? $USER->timezone : 99;
 263              } else {
 264                  $tz = $userorforcedtz;
 265              }
 266          }
 267  
 268          if ($tz == 99) {
 269              return self::get_server_timezone();
 270          }
 271  
 272          return self::normalise_timezone($tz);
 273      }
 274  
 275      /**
 276       * Return user timezone object.
 277       *
 278       * @param mixed $userorforcedtz
 279       * @return DateTimeZone
 280       */
 281      public static function get_user_timezone_object($userorforcedtz = null) {
 282          $tz = self::get_user_timezone($userorforcedtz);
 283          return new DateTimeZone($tz);
 284      }
 285  
 286      /**
 287       * Return default timezone set in php.ini or config.php.
 288       * @return string normalised timezone compatible with PHP
 289       */
 290      public static function get_default_php_timezone() {
 291          if (!isset(self::$defaultphptimezone)) {
 292              // This should not happen.
 293              self::store_default_php_timezone();
 294          }
 295  
 296          return self::$defaultphptimezone;
 297      }
 298  
 299      /**
 300       * To be called from lib/setup.php only!
 301       */
 302      public static function store_default_php_timezone() {
 303          if ((defined('PHPUNIT_TEST') and PHPUNIT_TEST)
 304              or defined('BEHAT_SITE_RUNNING') or defined('BEHAT_TEST') or defined('BEHAT_UTIL')) {
 305              // We want all test sites to be consistent by default.
 306              self::$defaultphptimezone = 'Australia/Perth';
 307              return;
 308          }
 309          if (!isset(self::$defaultphptimezone)) {
 310              self::$defaultphptimezone = date_default_timezone_get();
 311          }
 312      }
 313  
 314      /**
 315       * Do not use directly - use $this->setTimezone('xx', $tz) instead in your test case.
 316       * @param string $tz valid timezone name
 317       */
 318      public static function phpunit_override_default_php_timezone($tz) {
 319          if (!defined('PHPUNIT_TEST')) {
 320              throw new coding_exception('core_date::phpunit_override_default_php_timezone() must be used only from unit tests');
 321          }
 322          $result = timezone_open($tz); // This triggers error if $tz invalid.
 323          if ($result !== false) {
 324              self::$defaultphptimezone = $tz;
 325          } else {
 326              self::$defaultphptimezone = 'Australia/Perth';
 327          }
 328      }
 329  
 330      /**
 331       * To be called from phpunit reset only, after restoring $CFG.
 332       */
 333      public static function phpunit_reset() {
 334          global $CFG;
 335          if (!defined('PHPUNIT_TEST')) {
 336              throw new coding_exception('core_date::phpunit_reset() must be used only from unit tests');
 337          }
 338          self::store_default_php_timezone();
 339          date_default_timezone_set($CFG->timezone);
 340      }
 341  
 342      /**
 343       * Initialise timezone arrays, call before use.
 344       */
 345      protected static function init_zones() {
 346          if (isset(self::$goodzones)) {
 347              return;
 348          }
 349  
 350          $zones = DateTimeZone::listIdentifiers();
 351          self::$goodzones = array_fill_keys($zones, true);
 352  
 353          $zones = DateTimeZone::listIdentifiers(DateTimeZone::ALL_WITH_BC);
 354          self::$bczones = array();
 355          foreach ($zones as $zone) {
 356              if (isset(self::$goodzones[$zone])) {
 357                  continue;
 358              }
 359              self::$bczones[$zone] = true;
 360          }
 361  
 362          self::$badzones = array(
 363              // Windows time zones.
 364              'Dateline Standard Time' => 'Etc/GMT+12',
 365              'Hawaiian Standard Time' => 'Pacific/Honolulu',
 366              'Alaskan Standard Time' => 'America/Anchorage',
 367              'Pacific Standard Time (Mexico)' => 'America/Tijuana',
 368              'Pacific Standard Time' => 'America/Los_Angeles',
 369              'US Mountain Standard Time' => 'America/Phoenix',
 370              'Mountain Standard Time (Mexico)' => 'America/Chihuahua',
 371              'Mountain Standard Time' => 'America/Denver',
 372              'Central America Standard Time' => 'America/Guatemala',
 373              'Central Standard Time' => 'America/Chicago',
 374              'Central Standard Time (Mexico)' => 'America/Mexico_City',
 375              'Canada Central Standard Time' => 'America/Regina',
 376              'SA Pacific Standard Time' => 'America/Bogota',
 377              'S.A. Pacific Standard Time' => 'America/Bogota',
 378              'Eastern Standard Time' => 'America/New_York',
 379              'US Eastern Standard Time' => 'America/Indiana/Indianapolis',
 380              'U.S. Eastern Standard Time' => 'America/Indiana/Indianapolis',
 381              'Venezuela Standard Time' => 'America/Caracas',
 382              'Paraguay Standard Time' => 'America/Asuncion',
 383              'Atlantic Standard Time' => 'America/Halifax',
 384              'Central Brazilian Standard Time' => 'America/Cuiaba',
 385              'SA Western Standard Time' => 'America/La_Paz',
 386              'S.A. Western Standard Time' => 'America/La_Paz',
 387              'Pacific SA Standard Time' => 'America/Santiago',
 388              'Pacific S.A. Standard Time' => 'America/Santiago',
 389              'Newfoundland Standard Time' => 'America/St_Johns',
 390              'Newfoundland and Labrador Standard Time' => 'America/St_Johns',
 391              'E. South America Standard Time' => 'America/Sao_Paulo',
 392              'Argentina Standard Time' => 'America/Argentina/Buenos_Aires',
 393              'SA Eastern Standard Time' => 'America/Cayenne',
 394              'S.A. Eastern Standard Time' => 'America/Cayenne',
 395              'Greenland Standard Time' => 'America/Godthab',
 396              'Montevideo Standard Time' => 'America/Montevideo',
 397              'Bahia Standard Time' => 'America/Bahia',
 398              'Azores Standard Time' => 'Atlantic/Azores',
 399              'Cape Verde Standard Time' => 'Atlantic/Cape_Verde',
 400              'Morocco Standard Time' => 'Africa/Casablanca',
 401              'GMT Standard Time' => 'Europe/London',
 402              'Greenwich Standard Time' => 'Atlantic/Reykjavik',
 403              'W. Europe Standard Time' => 'Europe/Berlin',
 404              'Central Europe Standard Time' => 'Europe/Budapest',
 405              'Romance Standard Time' => 'Europe/Paris',
 406              'Central European Standard Time' => 'Europe/Warsaw',
 407              'W. Central Africa Standard Time' => 'Africa/Lagos',
 408              'Namibia Standard Time' => 'Africa/Windhoek',
 409              'Jordan Standard Time' => 'Asia/Amman',
 410              'GTB Standard Time' => 'Europe/Bucharest',
 411              'Middle East Standard Time' => 'Asia/Beirut',
 412              'Egypt Standard Time' => 'Africa/Cairo',
 413              'Syria Standard Time' => 'Asia/Damascus',
 414              'South Africa Standard Time' => 'Africa/Johannesburg',
 415              'FLE Standard Time' => 'Europe/Kiev',
 416              'Turkey Standard Time' => 'Europe/Istanbul',
 417              'Israel Standard Time' => 'Asia/Jerusalem',
 418              'Kaliningrad Standard Time' => 'Europe/Kaliningrad',
 419              'Libya Standard Time' => 'Africa/Tripoli',
 420              'Arabic Standard Time' => 'Asia/Baghdad',
 421              'Arab Standard Time' => 'Asia/Riyadh',
 422              'Belarus Standard Time' => 'Europe/Minsk',
 423              'Russian Standard Time' => 'Europe/Moscow',
 424              'E. Africa Standard Time' => 'Africa/Nairobi',
 425              'Iran Standard Time' => 'Asia/Tehran',
 426              'Arabian Standard Time' => 'Asia/Dubai',
 427              'Azerbaijan Standard Time' => 'Asia/Baku',
 428              'Russia Time Zone 3' => 'Europe/Samara',
 429              'Mauritius Standard Time' => 'Indian/Mauritius',
 430              'Georgian Standard Time' => 'Asia/Tbilisi',
 431              'Caucasus Standard Time' => 'Asia/Yerevan',
 432              'Afghanistan Standard Time' => 'Asia/Kabul',
 433              'West Asia Standard Time' => 'Asia/Tashkent',
 434              'Ekaterinburg Standard Time' => 'Asia/Yekaterinburg',
 435              'Pakistan Standard Time' => 'Asia/Karachi',
 436              'India Standard Time' => 'Asia/Kolkata', // PHP and Windows differ in spelling.
 437              'Sri Lanka Standard Time' => 'Asia/Colombo',
 438              'Nepal Standard Time' => 'Asia/Kathmandu',
 439              'Central Asia Standard Time' => 'Asia/Almaty',
 440              'Bangladesh Standard Time' => 'Asia/Dhaka',
 441              'N. Central Asia Standard Time' => 'Asia/Novosibirsk',
 442              'Myanmar Standard Time' => 'Asia/Yangon',
 443              'SE Asia Standard Time' => 'Asia/Bangkok',
 444              'S.E. Asia Standard Time' => 'Asia/Bangkok',
 445              'North Asia Standard Time' => 'Asia/Krasnoyarsk',
 446              'China Standard Time' => 'Asia/Shanghai',
 447              'North Asia East Standard Time' => 'Asia/Irkutsk',
 448              'Singapore Standard Time' => 'Asia/Singapore',
 449              'W. Australia Standard Time' => 'Australia/Perth',
 450              'Taipei Standard Time' => 'Asia/Taipei',
 451              'Ulaanbaatar Standard Time' => 'Asia/Ulaanbaatar',
 452              'Tokyo Standard Time' => 'Asia/Tokyo',
 453              'Korea Standard Time' => 'Asia/Seoul',
 454              'Yakutsk Standard Time' => 'Asia/Yakutsk',
 455              'Cen. Australia Standard Time' => 'Australia/Adelaide',
 456              'AUS Central Standard Time' => 'Australia/Darwin',
 457              'A.U.S. Central Standard Time' => 'Australia/Darwin',
 458              'E. Australia Standard Time' => 'Australia/Brisbane',
 459              'AUS Eastern Standard Time' => 'Australia/Sydney',
 460              'A.U.S. Eastern Standard Time' => 'Australia/Sydney',
 461              'West Pacific Standard Time' => 'Pacific/Port_Moresby',
 462              'Tasmania Standard Time' => 'Australia/Hobart',
 463              'Magadan Standard Time' => 'Asia/Magadan',
 464              'Vladivostok Standard Time' => 'Asia/Vladivostok',
 465              'Russia Time Zone 10' => 'Asia/Srednekolymsk',
 466              'Central Pacific Standard Time' => 'Pacific/Guadalcanal',
 467              'Russia Time Zone 11' => 'Asia/Kamchatka',
 468              'New Zealand Standard Time' => 'Pacific/Auckland',
 469              'Fiji Standard Time' => 'Pacific/Fiji',
 470              'Fiji Islands Standard Time' => 'Pacific/Fiji',
 471              'Tonga Standard Time' => 'Pacific/Tongatapu',
 472              'Samoa Standard Time' => 'Pacific/Apia',
 473              'Line Islands Standard Time' => 'Pacific/Kiritimati',
 474              'Mexico Standard Time 2' => 'America/Chihuahua',
 475              'Mexico Standard Time' => 'America/Mexico_City',
 476              'U.S. Mountain Standard Time' => 'America/Phoenix',
 477              'Mid-Atlantic Standard Time' => 'Atlantic/South_Georgia',
 478              'E. Europe Standard Time' => 'Europe/Minsk',
 479              'Transitional Islamic State of Afghanistan Standard Time' => 'Asia/Kabul',
 480              'Armenian Standard Time' => 'Asia/Yerevan',
 481              'Kamchatka Standard Time' => 'Asia/Kamchatka',
 482  
 483              // A lot more bad legacy (deprecated) time zones.
 484              'Australia/ACT' => 'Australia/Sydney',
 485              'Australia/LHI' => 'Australia/Lord_Howe',
 486              'Australia/North' => 'Australia/Darwin',
 487              'Australia/NSW' => 'Australia/Sydney',
 488              'Australia/Queensland' => 'Australia/Brisbane',
 489              'Australia/South' => 'Australia/Adelaide',
 490              'Australia/Tasmania' => 'Australia/Hobart',
 491              'Australia/Victoria' => 'Australia/Melbourne',
 492              'Australia/West' => 'Australia/Perth',
 493              'Brazil/Acre' => 'America/Rio_Branco',
 494              'Brazil/DeNoronha' => 'America/Noronha',
 495              'Brazil/East' => 'America/Sao_Paulo',
 496              'Brazil/West' => 'America/Manaus',
 497              'Canada/Atlantic' => 'America/Halifax',
 498              'Canada/Central' => 'America/Winnipeg',
 499              'Canada/Eastern' => 'America/Toronto',
 500              'Canada/Mountain' => 'America/Edmonton',
 501              'Canada/Newfoundland' => 'America/St_Johns',
 502              'Canada/Pacific' => 'America/Vancouver',
 503              'Canada/Saskatchewan' => 'America/Regina',
 504              'Canada/Yukon' => 'America/Whitehorse',
 505              'CDT' => 'America/Chicago',
 506              'CET' => 'Europe/Berlin',
 507              'Central European Time' => 'Europe/Berlin',
 508              'Central Time' => 'America/Chicago',
 509              'Chile/Continental' => 'America/Santiago',
 510              'Chile/EasterIsland' => 'Pacific/Easter',
 511              'China Time' => 'Asia/Shanghai',
 512              'CST' => 'America/Chicago',
 513              'CST6CDT' => 'America/Chicago',
 514              'Cuba' => 'America/Havana',
 515              'EDT' => 'America/New_York',
 516              'EET' => 'Europe/Kiev',
 517              'Egypt' => 'Africa/Cairo',
 518              'Eire' => 'Europe/Dublin',
 519              'EST' => 'America/Cancun',
 520              'EST5EDT' => 'America/New_York',
 521              'Eastern Time' => 'America/New_York',
 522              'Etc/Greenwich' => 'Etc/GMT',
 523              'Etc/UCT' => 'Etc/UTC',
 524              'Etc/Universal' => 'Etc/UTC',
 525              'Etc/Zulu' => 'Etc/UTC',
 526              'FET' => 'Europe/Minsk',
 527              'GB' => 'Europe/London',
 528              'GB-Eire' => 'Europe/London',
 529              'Greenwich' => 'Etc/GMT',
 530              'Hongkong' => 'Asia/Hong_Kong',
 531              'HST' => 'Pacific/Honolulu',
 532              'Iceland' => 'Atlantic/Reykjavik',
 533              'India Time' => 'Asia/Kolkata',
 534              'Iran' => 'Asia/Tehran',
 535              'Israel' => 'Asia/Jerusalem',
 536              'IST' => 'Asia/Kolkata',
 537              'Jamaica' => 'America/Jamaica',
 538              'Japan' => 'Asia/Tokyo',
 539              'Japan Standard Time' => 'Asia/Tokyo',
 540              'Japan Time' => 'Asia/Tokyo',
 541              'JST' => 'Asia/Tokyo',
 542              'Kwajalein' => 'Pacific/Kwajalein',
 543              'Libya' => 'Africa/Tripoli',
 544              'MDT' => 'America/Denver',
 545              'MET' => 'Europe/Paris',
 546              'Mexico/BajaNorte' => 'America/Tijuana',
 547              'Mexico/BajaSur' => 'America/Mazatlan',
 548              'Mexico/General' => 'America/Mexico_City',
 549              'MST' => 'America/Phoenix',
 550              'MST7MDT' => 'America/Denver',
 551              'Navajo' => 'America/Denver',
 552              'NZ' => 'Pacific/Auckland',
 553              'NZ-CHAT' => 'Pacific/Chatham',
 554              'Pacific Time' => 'America/Los_Angeles',
 555              'PDT' => 'America/Los_Angeles',
 556              'Poland' => 'Europe/Warsaw',
 557              'Portugal' => 'Europe/Lisbon',
 558              'PRC' => 'Asia/Shanghai',
 559              'PST' => 'America/Los_Angeles',
 560              'PST8PDT' => 'America/Los_Angeles',
 561              'ROC' => 'Asia/Taipei',
 562              'ROK' => 'Asia/Seoul',
 563              'Singapore' => 'Asia/Singapore',
 564              'Turkey' => 'Europe/Istanbul',
 565              'UCT' => 'Etc/UTC',
 566              'Universal' => 'Etc/UTC',
 567              'US/Alaska' => 'America/Anchorage',
 568              'US/Aleutian' => 'America/Adak',
 569              'US/Arizona' => 'America/Phoenix',
 570              'US/Central' => 'America/Chicago',
 571              'US/East-Indiana' => 'America/Indiana/Indianapolis',
 572              'US/Eastern' => 'America/New_York',
 573              'US/Hawaii' => 'Pacific/Honolulu',
 574              'US/Indiana-Starke' => 'America/Indiana/Knox',
 575              'US/Michigan' => 'America/Detroit',
 576              'US/Mountain' => 'America/Denver',
 577              'US/Pacific' => 'America/Los_Angeles',
 578              'US/Pacific-New' => 'America/Los_Angeles',
 579              'US/Samoa' => 'Pacific/Pago_Pago',
 580              'W-SU' => 'Europe/Moscow',
 581              'WET' => 'Europe/London',
 582              'Zulu' => 'Etc/UTC',
 583  
 584              // Some UTC variations.
 585              'UTC-01' => 'Etc/GMT+1',
 586              'UTC-02' => 'Etc/GMT+2',
 587              'UTC-03' => 'Etc/GMT+3',
 588              'UTC-04' => 'Etc/GMT+4',
 589              'UTC-05' => 'Etc/GMT+5',
 590              'UTC-06' => 'Etc/GMT+6',
 591              'UTC-07' => 'Etc/GMT+7',
 592              'UTC-08' => 'Etc/GMT+8',
 593              'UTC-09' => 'Etc/GMT+9',
 594  
 595              // Some weird GMTs.
 596              'Etc/GMT+0' => 'Etc/GMT',
 597              'Etc/GMT-0' => 'Etc/GMT',
 598              'Etc/GMT0' => 'Etc/GMT',
 599  
 600              // Link old timezone names with their new names.
 601              'Africa/Asmera' => 'Africa/Asmara',
 602              'Africa/Timbuktu' => 'Africa/Abidjan',
 603              'America/Argentina/ComodRivadavia' => 'America/Argentina/Catamarca',
 604              'America/Atka' => 'America/Adak',
 605              'America/Buenos_Aires' => 'America/Argentina/Buenos_Aires',
 606              'America/Catamarca' => 'America/Argentina/Catamarca',
 607              'America/Coral_Harbour' => 'America/Atikokan',
 608              'America/Cordoba' => 'America/Argentina/Cordoba',
 609              'America/Ensenada' => 'America/Tijuana',
 610              'America/Fort_Wayne' => 'America/Indiana/Indianapolis',
 611              'America/Indianapolis' => 'America/Indiana/Indianapolis',
 612              'America/Jujuy' => 'America/Argentina/Jujuy',
 613              'America/Knox_IN' => 'America/Indiana/Knox',
 614              'America/Louisville' => 'America/Kentucky/Louisville',
 615              'America/Mendoza' => 'America/Argentina/Mendoza',
 616              'America/Montreal' => 'America/Toronto',
 617              'America/Porto_Acre' => 'America/Rio_Branco',
 618              'America/Rosario' => 'America/Argentina/Cordoba',
 619              'America/Santa_Isabel' => 'America/Tijuana',
 620              'America/Shiprock' => 'America/Denver',
 621              'America/Virgin' => 'America/Port_of_Spain',
 622              'Antarctica/South_Pole' => 'Pacific/Auckland',
 623              'Asia/Ashkhabad' => 'Asia/Ashgabat',
 624              'Asia/Calcutta' => 'Asia/Kolkata',
 625              'Asia/Chongqing' => 'Asia/Shanghai',
 626              'Asia/Chungking' => 'Asia/Shanghai',
 627              'Asia/Dacca' => 'Asia/Dhaka',
 628              'Asia/Harbin' => 'Asia/Shanghai',
 629              'Asia/Istanbul' => 'Europe/Istanbul',
 630              'Asia/Kashgar' => 'Asia/Urumqi',
 631              'Asia/Katmandu' => 'Asia/Kathmandu',
 632              'Asia/Macao' => 'Asia/Macau',
 633              'Asia/Rangoon' => 'Asia/Yangon',
 634              'Asia/Saigon' => 'Asia/Ho_Chi_Minh',
 635              'Asia/Tel_Aviv' => 'Asia/Jerusalem',
 636              'Asia/Thimbu' => 'Asia/Thimphu',
 637              'Asia/Ujung_Pandang' => 'Asia/Makassar',
 638              'Asia/Ulan_Bator' => 'Asia/Ulaanbaatar',
 639              'Atlantic/Faeroe' => 'Atlantic/Faroe',
 640              'Atlantic/Jan_Mayen' => 'Europe/Oslo',
 641              'Australia/Canberra' => 'Australia/Sydney',
 642              'Australia/Yancowinna' => 'Australia/Broken_Hill',
 643              'Europe/Belfast' => 'Europe/London',
 644              'Europe/Nicosia' => 'Asia/Nicosia',
 645              'Europe/Tiraspol' => 'Europe/Chisinau',
 646              'Pacific/Johnston' => 'Pacific/Honolulu',
 647              'Pacific/Ponape' => 'Pacific/Pohnpei',
 648              'Pacific/Samoa' => 'Pacific/Pago_Pago',
 649              'Pacific/Truk' => 'Pacific/Chuuk',
 650              'Pacific/Yap' => 'Pacific/Chuuk',
 651          );
 652  
 653          // Legacy GMT fallback.
 654          for ($i = -12; $i <= 14; $i++) {
 655              $off = abs($i);
 656              if ($i < 0) {
 657                  $mapto = 'Etc/GMT+' . $off;
 658                  $utc = 'UTC-' . $off;
 659                  $gmt = 'GMT-' . $off;
 660              } else if ($i > 0) {
 661                  $mapto = 'Etc/GMT-' . $off;
 662                  $utc = 'UTC+' . $off;
 663                  $gmt = 'GMT+' . $off;
 664              } else {
 665                  $mapto = 'Etc/GMT';
 666                  $utc = 'UTC';
 667                  $gmt = 'GMT';
 668              }
 669              if (isset(self::$bczones[$mapto])) {
 670                  self::$badzones[$i . ''] = $mapto;
 671                  self::$badzones[$i . '.0'] = $mapto;
 672                  self::$badzones[$utc] = $mapto;
 673                  self::$badzones[$gmt] = $mapto;
 674              }
 675          }
 676  
 677          // Legacy Moodle half an hour offsets - pick any city nearby, ideally without DST.
 678          self::$badzones['4.5'] = 'Asia/Kabul';
 679          self::$badzones['5.5'] = 'Asia/Kolkata';
 680          self::$badzones['6.5'] = 'Asia/Rangoon';
 681          self::$badzones['9.5'] = 'Australia/Darwin';
 682  
 683          // Remove bad zones that are elsewhere.
 684          foreach (self::$bczones as $zone => $unused) {
 685              if (isset(self::$badzones[$zone])) {
 686                  unset(self::$badzones[$zone]);
 687              }
 688          }
 689          foreach (self::$goodzones as $zone => $unused) {
 690              if (isset(self::$badzones[$zone])) {
 691                  unset(self::$badzones[$zone]);
 692              }
 693          }
 694      }
 695  }