all-in-one-event-calendar/lib/date/timezone.php
2017-11-09 17:36:04 +01:00

617 lines
28 KiB
PHP

<?php
/**
* Timezones manipulation object.
*
* @author Time.ly Network, Inc.
* @since 2.0
* @package Ai1EC
* @subpackage Ai1EC.Date
*/
class Ai1ec_Date_Timezone extends Ai1ec_Base {
/**
* @var Ai1ec_Cache_Interface In-memory storage for timezone objects.
*/
protected $_cache = null;
/**
* @var array Map of timezone names and their Olson TZ counterparts.
*/
protected $_zones = array(
'+00:00' => 'UTC',
'Z' => 'UTC',
'AUS Central Standard Time' => 'Australia/Darwin',
'AUS Eastern Standard Time' => 'Australia/Sydney',
'Acre' => 'America/Rio_Branco',
'Afghanistan' => 'Asia/Kabul',
'Afghanistan Standard Time' => 'Asia/Kabul',
'Africa_Central' => 'Africa/Maputo',
'Africa_Eastern' => 'Africa/Nairobi',
'Africa_FarWestern' => 'Africa/El_Aaiun',
'Africa_Southern' => 'Africa/Johannesburg',
'Africa_Western' => 'Africa/Lagos',
'Aktyubinsk' => 'Asia/Aqtobe',
'Alaska' => 'America/Juneau',
'Alaska_Hawaii' => 'America/Anchorage',
'Alaskan Standard Time' => 'America/Anchorage',
'Almaty' => 'Asia/Almaty',
'Amazon' => 'America/Manaus',
'America_Central' => 'America/Chicago',
'America_Eastern' => 'America/New_York',
'America_Mountain' => 'America/Denver',
'America_Pacific' => 'America/Los_Angeles',
'Anadyr' => 'Asia/Anadyr',
'Aqtau' => 'Asia/Aqtau',
'Aqtobe' => 'Asia/Aqtobe',
'Arab Standard Time' => 'Asia/Riyadh',
'Arabian' => 'Asia/Riyadh',
'Arabian Standard Time' => 'Asia/Dubai',
'Arabic Standard Time' => 'Asia/Baghdad',
'Argentina' => 'America/Buenos_Aires',
'Argentina Standard Time' => 'America/Buenos_Aires',
'Argentina_Western' => 'America/Mendoza',
'Armenia' => 'Asia/Yerevan',
'Armenian Standard Time' => 'Asia/Yerevan',
'Ashkhabad' => 'Asia/Ashgabat',
'Atlantic' => 'America/Halifax',
'Atlantic Standard Time' => 'America/Halifax',
'Australia_Central' => 'Australia/Adelaide',
'Australia_CentralWestern' => 'Australia/Eucla',
'Australia_Eastern' => 'Australia/Sydney',
'Australia_Western' => 'Australia/Perth',
'Azerbaijan' => 'Asia/Baku',
'Azerbaijan Standard Time' => 'Asia/Baku',
'Azores' => 'Atlantic/Azores',
'Azores Standard Time' => 'Atlantic/Azores',
'Baku' => 'Asia/Baku',
'Bangladesh' => 'Asia/Dhaka',
'Bering' => 'America/Adak',
'Bhutan' => 'Asia/Thimphu',
'Bolivia' => 'America/La_Paz',
'Borneo' => 'Asia/Kuching',
'Brasilia' => 'America/Sao_Paulo',
'British' => 'Europe/London',
'Brunei' => 'Asia/Brunei',
'Canada Central Standard Time' => 'America/Regina',
'Cape Verde Standard Time' => 'Atlantic/Cape_Verde',
'Cape_Verde' => 'Atlantic/Cape_Verde',
'Caucasus Standard Time' => 'Asia/Yerevan',
'Cen. Australia Standard Time' => 'Australia/Adelaide',
'Central America Standard Time' => 'America/Guatemala',
'Central Asia Standard Time' => 'Asia/Dhaka',
'Central Brazilian Standard Time' => 'America/Manaus',
'Central Europe Standard Time' => 'Europe/Budapest',
'Central European Standard Time' => 'Europe/Warsaw',
'Central Pacific Standard Time' => 'Pacific/Guadalcanal',
'Central Standard Time' => 'America/Chicago',
'Central Standard Time (Mexico)' => 'America/Mexico_City',
'Chamorro' => 'Pacific/Saipan',
'Changbai' => 'Asia/Harbin',
'Chatham' => 'Pacific/Chatham',
'Chile' => 'America/Santiago',
'China' => 'Asia/Shanghai',
'China Standard Time' => 'Asia/Shanghai',
'Choibalsan' => 'Asia/Choibalsan',
'Christmas' => 'Indian/Christmas',
'Cocos' => 'Indian/Cocos',
'Colombia' => 'America/Bogota',
'Cook' => 'Pacific/Rarotonga',
'Cuba' => 'America/Havana',
'Dacca' => 'Asia/Dhaka',
'Dateline Standard Time' => 'Etc/GMT+12',
'Davis' => 'Antarctica/Davis',
'Dominican' => 'America/Santo_Domingo',
'DumontDUrville' => 'Antarctica/DumontDUrville',
'Dushanbe' => 'Asia/Dushanbe',
'Dutch_Guiana' => 'America/Paramaribo',
'E. Africa Standard Time' => 'Africa/Nairobi',
'E. Australia Standard Time' => 'Australia/Brisbane',
'E. Europe Standard Time' => 'Europe/Minsk',
'E. South America Standard Time' => 'America/Sao_Paulo',
'East_Timor' => 'Asia/Dili',
'Easter' => 'Pacific/Easter',
'Eastern Standard Time' => 'America/New_York',
'Ecuador' => 'America/Guayaquil',
'Egypt Standard Time' => 'Africa/Cairo',
'Ekaterinburg Standard Time' => 'Asia/Yekaterinburg',
'Europe_Central' => 'Europe/Paris',
'Europe_Eastern' => 'Europe/Bucharest',
'Europe_Western' => 'Atlantic/Canary',
'FLE Standard Time' => 'Europe/Kiev',
'Falkland' => 'Atlantic/Stanley',
'Fiji' => 'Pacific/Fiji',
'Fiji Standard Time' => 'Pacific/Fiji',
'French_Guiana' => 'America/Cayenne',
'French_Southern' => 'Indian/Kerguelen',
'Frunze' => 'Asia/Bishkek',
'GMT' => 'UTC', // seems better than 'Atlantic/Reykjavik'
'GMT Standard Time' => 'Europe/London',
'GTB Standard Time' => 'Europe/Istanbul',
'Galapagos' => 'Pacific/Galapagos',
'Gambier' => 'Pacific/Gambier',
'Georgia' => 'Asia/Tbilisi',
'Georgian Standard Time' => 'Etc/GMT-3',
'Gilbert_Islands' => 'Pacific/Tarawa',
'Goose_Bay' => 'America/Goose_Bay',
'Greenland Standard Time' => 'America/Godthab',
'Greenland_Central' => 'America/Scoresbysund',
'Greenland_Eastern' => 'America/Scoresbysund',
'Greenland_Western' => 'America/Godthab',
'Greenwich Standard Time' => 'Atlantic/Reykjavik',
'Guam' => 'Pacific/Guam',
'Gulf' => 'Asia/Dubai',
'Guyana' => 'America/Guyana',
'Hawaii_Aleutian' => 'Pacific/Honolulu',
'Hawaiian Standard Time' => 'Pacific/Honolulu',
'Hong_Kong' => 'Asia/Hong_Kong',
'Hovd' => 'Asia/Hovd',
'India' => 'Asia/Calcutta',
'India Standard Time' => 'Asia/Calcutta',
'Indian_Ocean' => 'Indian/Chagos',
'Indochina' => 'Asia/Saigon',
'Indonesia_Central' => 'Asia/Makassar',
'Indonesia_Eastern' => 'Asia/Jayapura',
'Indonesia_Western' => 'Asia/Jakarta',
'Iran' => 'Asia/Tehran',
'Iran Standard Time' => 'Asia/Tehran',
'Irish' => 'Europe/Dublin',
'Irkutsk' => 'Asia/Irkutsk',
'Israel' => 'Asia/Jerusalem',
'Israel Standard Time' => 'Asia/Jerusalem',
'Japan' => 'Asia/Tokyo',
'Jordan Standard Time' => 'Asia/Amman',
'Kamchatka' => 'Asia/Kamchatka',
'Karachi' => 'Asia/Karachi',
'Kashgar' => 'Asia/Kashgar',
'Kazakhstan_Eastern' => 'Asia/Almaty',
'Kazakhstan_Western' => 'Asia/Aqtobe',
'Kizilorda' => 'Asia/Qyzylorda',
'Korea' => 'Asia/Seoul',
'Korea Standard Time' => 'Asia/Seoul',
'Kosrae' => 'Pacific/Kosrae',
'Krasnoyarsk' => 'Asia/Krasnoyarsk',
'Kuybyshev' => 'Europe/Samara',
'Kwajalein' => 'Pacific/Kwajalein',
'Kyrgystan' => 'Asia/Bishkek',
'Lanka' => 'Asia/Colombo',
'Liberia' => 'Africa/Monrovia',
'Line_Islands' => 'Pacific/Kiritimati',
'Long_Shu' => 'Asia/Chongqing',
'Lord_Howe' => 'Australia/Lord_Howe',
'Macau' => 'Asia/Macau',
'Magadan' => 'Asia/Magadan',
'Malaya' => 'Asia/Kuala_Lumpur',
'Malaysia' => 'Asia/Kuching',
'Maldives' => 'Indian/Maldives',
'Marquesas' => 'Pacific/Marquesas',
'Marshall_Islands' => 'Pacific/Majuro',
'Mauritius' => 'Indian/Mauritius',
'Mauritius Standard Time' => 'Indian/Mauritius',
'Mawson' => 'Antarctica/Mawson',
'Mexico Standard Time' => 'America/Mexico_City',
'Mexico Standard Time 2' => 'America/Chihuahua',
'Mid-Atlantic Standard Time' => 'Atlantic/South_Georgia',
'Middle East Standard Time' => 'Asia/Beirut',
'Mongolia' => 'Asia/Ulaanbaatar',
'Montevideo Standard Time' => 'America/Montevideo',
'Morocco Standard Time' => 'Africa/Casablanca',
'Moscow' => 'Europe/Moscow',
'Mountain Standard Time' => 'America/Denver',
'Mountain Standard Time (Mexico)' => 'America/Chihuahua',
'Myanmar' => 'Asia/Rangoon',
'Myanmar Standard Time' => 'Asia/Rangoon',
'N. Central Asia Standard Time' => 'Asia/Novosibirsk',
'Namibia Standard Time' => 'Africa/Windhoek',
'Nauru' => 'Pacific/Nauru',
'Nepal' => 'Asia/Katmandu',
'Nepal Standard Time' => 'Asia/Katmandu',
'New Zealand Standard Time' => 'Pacific/Auckland',
'New_Caledonia' => 'Pacific/Noumea',
'New_Zealand' => 'Pacific/Auckland',
'Newfoundland' => 'America/St_Johns',
'Newfoundland Standard Time' => 'America/St_Johns',
'Niue' => 'Pacific/Niue',
'Norfolk' => 'Pacific/Norfolk',
'Noronha' => 'America/Noronha',
'North Asia East Standard Time' => 'Asia/Irkutsk',
'North Asia Standard Time' => 'Asia/Krasnoyarsk',
'North_Mariana' => 'Pacific/Saipan',
'Novosibirsk' => 'Asia/Novosibirsk',
'Omsk' => 'Asia/Omsk',
'Oral' => 'Asia/Oral',
'Pacific SA Standard Time' => 'America/Santiago',
'Pacific Standard Time' => 'America/Los_Angeles',
'Pacific Standard Time (Mexico)' => 'America/Tijuana',
'Pakistan' => 'Asia/Karachi',
'Pakistan Standard Time' => 'Asia/Karachi',
'Palau' => 'Pacific/Palau',
'Papua_New_Guinea' => 'Pacific/Port_Moresby',
'Paraguay' => 'America/Asuncion',
'Peru' => 'America/Lima',
'Philippines' => 'Asia/Manila',
'Phoenix_Islands' => 'Pacific/Enderbury',
'Pierre_Miquelon' => 'America/Miquelon',
'Pitcairn' => 'Pacific/Pitcairn',
'Ponape' => 'Pacific/Ponape',
'Qyzylorda' => 'Asia/Qyzylorda',
'Reunion' => 'Indian/Reunion',
'Romance Standard Time' => 'Europe/Paris',
'Rothera' => 'Antarctica/Rothera',
'Russian Standard Time' => 'Europe/Moscow',
'SA Eastern Standard Time' => 'Etc/GMT+3',
'SA Pacific Standard Time' => 'America/Bogota',
'SA Western Standard Time' => 'America/La_Paz',
'SE Asia Standard Time' => 'Asia/Bangkok',
'Sakhalin' => 'Asia/Sakhalin',
'Samara' => 'Europe/Samara',
'Samarkand' => 'Asia/Samarkand',
'Samoa' => 'Pacific/Apia',
'Samoa Standard Time' => 'Pacific/Apia',
'Seychelles' => 'Indian/Mahe',
'Shevchenko' => 'Asia/Aqtau',
'Singapore' => 'Asia/Singapore',
'Singapore Standard Time' => 'Asia/Singapore',
'Solomon' => 'Pacific/Guadalcanal',
'South Africa Standard Time' => 'Africa/Johannesburg',
'South_Georgia' => 'Atlantic/South_Georgia',
'Sri Lanka Standard Time' => 'Asia/Colombo',
'Suriname' => 'America/Paramaribo',
'Sverdlovsk' => 'Asia/Yekaterinburg',
'Syowa' => 'Antarctica/Syowa',
'Tahiti' => 'Pacific/Tahiti',
'Taipei' => 'Asia/Taipei',
'Taipei Standard Time' => 'Asia/Taipei',
'Tajikistan' => 'Asia/Dushanbe',
'Tashkent' => 'Asia/Tashkent',
'Tasmania Standard Time' => 'Australia/Hobart',
'Tbilisi' => 'Asia/Tbilisi',
'Tokelau' => 'Pacific/Fakaofo',
'Tokyo Standard Time' => 'Asia/Tokyo',
'Tonga' => 'Pacific/Tongatapu',
'Tonga Standard Time' => 'Pacific/Tongatapu',
'Truk' => 'Pacific/Truk',
'Turkey' => 'Europe/Istanbul',
'Turkmenistan' => 'Asia/Ashgabat',
'Tuvalu' => 'Pacific/Funafuti',
'US/Eastern' => 'America/New_York',
'US Eastern Standard Time' => 'Etc/GMT+5',
'US Mountain Standard Time' => 'America/Phoenix',
'Uralsk' => 'Asia/Oral',
'Uruguay' => 'America/Montevideo',
'Urumqi' => 'Asia/Urumqi',
'Uzbekistan' => 'Asia/Tashkent',
'Vanuatu' => 'Pacific/Efate',
'Venezuela' => 'America/Caracas',
'Venezuela Standard Time' => 'America/Caracas',
'Vladivostok' => 'Asia/Vladivostok',
'Vladivostok Standard Time' => 'Asia/Vladivostok',
'Volgograd' => 'Europe/Volgograd',
'Vostok' => 'Antarctica/Vostok',
'W. Australia Standard Time' => 'Australia/Perth',
'W. Central Africa Standard Time' => 'Africa/Lagos',
'W. Europe Standard Time' => 'Europe/Berlin',
'Wake' => 'Pacific/Wake',
'Wallis' => 'Pacific/Wallis',
'West Asia Standard Time' => 'Asia/Tashkent',
'West Pacific Standard Time' => 'Pacific/Port_Moresby',
'Yakutsk' => 'Asia/Yakutsk',
'Yakutsk Standard Time' => 'Asia/Yakutsk',
'Yekaterinburg' => 'Asia/Yekaterinburg',
'Yerevan' => 'Asia/Yerevan',
'Yukon' => 'America/Yakutat',
);
/**
* @var array Map of timezones acceptable by DateTimeZone but not strtotime.
*/
protected $_invalid_legacy = array(
'US/Eastern' => true,
);
/**
* @var array|bool List of system identifiers or false if none available.
*/
protected $_identifiers = false;
/**
* Initialize local cache and identifiers.
*
* @param Ai1ec_Registry_Object $registry Registry to use.
*
* @return void
*/
public function __construct( Ai1ec_Registry_Object $registry ) {
parent::__construct( $registry );
$this->_cache = $this->_registry->get( 'cache.memory' );
$this->_init_identifiers();
}
/**
* Get default timezone to use in input/output.
*
* Approach is as follows:
* - check user profile for timezone preference;
* - if user has no preference - check site for timezone selection;
* - if site has no selection - raise notice and use 'UTC'.
*
* @return string Olson timezone string identifier.
*/
public function get_default_timezone() {
static $default_timezone = null;
if ( null === $default_timezone ) {
$candidates = array();
$candidates[] = (string)$this->_registry->get( 'model.meta-user' )
->get_current( 'ai1ec_timezone' );
$candidates[] = (string)$this->_registry->get( 'model.option' )
->get( 'timezone_string' );
$candidates[] = (string)$this->_registry->get( 'model.option' )
->get( 'gmt_offset' );
$candidates = array_filter( $candidates, 'strlen' );
foreach ( $candidates as $timezone ) {
$timezone = $this->get_name( $timezone );
if ( false !== $timezone ) {
$default_timezone = $timezone;
break;
}
}
if ( null === $default_timezone ) {
$default_timezone = 'UTC';
$this->_registry->get( 'notification.admin' )->store(
sprintf(
Ai1ec_I18n::__(
'Please select site timezone in %s <em>Timezone</em> dropdown menu.'
),
'<a href="' . ai1ec_admin_url( 'options-general.php' ) .
'">' . Ai1ec_I18n::__( 'Settings' ) . '</a>'
),
'error'
);
}
}
return $default_timezone;
}
/**
* Attempt to decode GMT offset to some Olson timezone.
*
* @param float $zone GMT offset.
*
* @return string Valid Olson timezone name (UTC is last resort).
*/
public function decode_gmt_timezone( $zone ) {
$auto_zone = timezone_name_from_abbr( null, $zone * 3600, true );
if ( false !== $auto_zone ) {
return $auto_zone;
}
$auto_zone = timezone_name_from_abbr(
null,
( (int) $zone ) * 3600,
true
);
if ( false !== $auto_zone ) {
return $auto_zone;
}
$this->_registry->get( 'notification.admin' )->store(
sprintf(
Ai1ec_I18n::__(
'Timezone "UTC%+d" is not recognized. Please %suse valid%s timezone name, until then events will be created in UTC timezone.'
),
$zone,
'<a href="' . ai1ec_admin_url( 'options-general.php' ) . '">',
'</a>'
),
'error'
);
return 'UTC';
}
/**
* Get valid timezone name from input.
*
* @param string $zone Name to check/parse.
*
* @return string Timezone name to use
*/
public function get_name( $zone ) {
if ( is_numeric( $zone ) ) {
$decoded_zone = $this->decode_gmt_timezone( $zone );
if ( 'UTC' !== $decoded_zone ) {
$message = sprintf(
Ai1ec_I18n::__(
'Selected timezone "UTC%+d" will be treated as %s.'
),
$zone,
$decoded_zone
);
$this->_registry->get( 'notification.admin' )
->store( $message );
}
$zone = $decoded_zone;
}
if ( false === $this->_identifiers ) {
return $zone; // anything should do, as zones are not supported
}
if ( ! isset( $this->_identifiers[$zone] ) ) {
$zone = $this->_olson_lookup( $zone );
$valid_legacy = false;
try {
new DateTimeZone( $zone ); // throw away instantly
$valid_legacy = true;
} catch ( Exception $excpt ) {
$valid_legacy = false;
}
if ( ! $valid_legacy || isset( $this->_invalid_legacy[$zone] ) ) {
return $this->guess_zone( $zone );
}
$this->_identifiers[$zone] = $zone;
unset( $valid_legacy );
}
return $zone;
}
/**
* Quick map look-up to discard zones that have limited recognition.
*
* @param string $zone Name of timezone to lookup.
*
* @return string Timezone name to use. Might be the same as $zone.
*/
protected function _olson_lookup( $zone ) {
if ( isset( $this->_zones[$zone] ) ) {
return $this->_zones[$zone];
}
return $zone;
}
/**
* Check if timezone is set in wp_option
*
*/
public function is_timezone_not_set() {
$timezone = $this->_registry->get( 'model.option' )
->get( 'timezone_string' );
return empty( $timezone );
}
/**
* Render options for select in settings
*
* @return array
*/
public function get_timezones( $only_zones = false ) {
$zones = DateTimeZone::listIdentifiers();
if (
empty( $zones )
) {
return array();
}
if ( ! $only_zones ) {
$manual = __( 'Manual Offset', AI1EC_PLUGIN_NAME );
$options = array();
$options[$manual][] = array(
'text' => __( 'Choose your timezone', AI1EC_PLUGIN_NAME ),
'value' => '',
'args' => array(
'selected' => 'selected'
)
);
}
foreach ( $zones as $zone ) {
$exploded_zone = explode( '/', $zone );
if ( ! isset( $exploded_zone[1] ) && ! $only_zones ) {
$exploded_zone[1] = $exploded_zone[0];
$exploded_zone[0] = $manual;
}
$optgroup = $exploded_zone[0];
unset( $exploded_zone[0] );
$options[$optgroup][] = array(
'text' => implode( '/', $exploded_zone ),
'value' => $zone,
);
}
return $options;
}
/**
* Guess valid timezone identifier from arbitrary input.
*
* @param string $meta_name Arbitrary input.
*
* @return string|bool Parsed timezone name or false if none found.
*/
public function guess_zone( $meta_name ) {
if ( isset( $this->_zones[$meta_name] ) ) {
return $this->_zones[$meta_name];
}
$name_variants = array(
strtr( $meta_name, ' ', '_' ),
strtr( $meta_name, '_', ' ' ),
);
if ( false !== ( $parenthesis_pos = strpos( $meta_name, '(' ) ) ) {
foreach ( $name_variants as $name ) {
$name_variants[] = substr( $name, 0, $parenthesis_pos - 1 );
}
}
foreach ( $name_variants as $name ) {
if ( isset( $this->_zones[$name] ) ) {
// cache to avoid future lookups and return
$this->_zones[$meta_name] = $this->_zones[$name];
return $this->_zones[$name];
}
}
if (
isset( $meta_name{0} ) &&
'(' === $meta_name{0} &&
$closing_pos = strpos( $meta_name, ')' )
) {
$meta_name = trim( substr( $meta_name, $closing_pos + 1 ) );
return $this->guess_zone( $meta_name );
}
if (
false === strpos( $meta_name, ' Standard ' ) &&
false !== ( $time_pos = strpos( $meta_name, ' Time' ) )
) {
$meta_name = substr( $meta_name, 0, $time_pos ) .
' Standard' . substr( $meta_name, $time_pos );
return $this->guess_zone( $meta_name );
}
return false;
}
/**
* Get timezone object instance.
*
* @param string $timezone Name of timezone to get instance for.
*
* @return DateTimeZone Instance of timezone object.
*
* @throws Ai1ec_Date_Timezone_Exception If an error occurs.
*/
public function get( $timezone ) {
if ( 'sys.default' === $timezone ) {
$timezone = $this->get_default_timezone();
}
$name = $this->get_name( $timezone );
if ( ! $name ) {
$name = $this->get_name( $this->get_default_timezone() );
}
$zone = $this->_cache->get( $name, null );
if ( null === $zone ) {
$exception = null;
try {
$zone = new DateTimeZone( $name );
} catch ( Exception $invalid_tz ) {
$exception = $invalid_tz;
}
if ( null !== $exception ) {
throw new Ai1ec_Date_Timezone_Exception( $exception->getMessage() );
}
$this->_cache->set( $name, $zone );
}
return $zone;
}
/**
* Add system identifiers to object registry.
*
* @return bool Success
*/
protected function _init_identifiers() {
$identifiers = DateTimeZone::listIdentifiers();
if ( ! $identifiers ) {
return false;
}
$mapped = array();
foreach ( $identifiers as $zone ) {
$zone = (string)$zone;
$mapped[$zone] = true;
$this->_zones[$zone] = $zone;
}
unset( $identifiers, $zone );
$this->_identifiers = $mapped;
return true;
}
}