Skip to content

Commit

Permalink
Merge pull request #548 from heiglandreas/allowMoreTimezoneGuesser
Browse files Browse the repository at this point in the history
Allow easier extension of the timezone guessing
  • Loading branch information
phil-davis committed Nov 15, 2021
2 parents a7460c5 + ff902c6 commit 9157772
Show file tree
Hide file tree
Showing 9 changed files with 503 additions and 132 deletions.
261 changes: 134 additions & 127 deletions lib/TimeZoneUtil.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,16 @@

namespace Sabre\VObject;

use DateTimeZone;
use InvalidArgumentException;
use Sabre\VObject\TimezoneGuesser\FindFromOffset;
use Sabre\VObject\TimezoneGuesser\FindFromTimezoneIdentifier;
use Sabre\VObject\TimezoneGuesser\FindFromTimezoneMap;
use Sabre\VObject\TimezoneGuesser\GuessFromLicEntry;
use Sabre\VObject\TimezoneGuesser\GuessFromMsTzId;
use Sabre\VObject\TimezoneGuesser\TimezoneFinder;
use Sabre\VObject\TimezoneGuesser\TimezoneGuesser;

/**
* Time zone name translation.
*
Expand All @@ -14,17 +24,136 @@
*/
class TimeZoneUtil
{
/** @var self */
private static $instance = null;

/** @var TimezoneGuesser[] */
private $timezoneGuessers = [];

/** @var TimezoneFinder[] */
private $timezoneFinders = [];

private function __construct()
{
$this->addGuesser('lic', new GuessFromLicEntry());
$this->addGuesser('msTzId', new GuessFromMsTzId());
$this->addFinder('tzid', new FindFromTimezoneIdentifier());
$this->addFinder('tzmap', new FindFromTimezoneMap());
$this->addFinder('offset', new FindFromOffset());
}

private static function getInstance(): self
{
if (null === self::$instance) {
self::$instance = new self();
}

return self::$instance;
}

private function addGuesser(string $key, TimezoneGuesser $guesser): void
{
$this->timezoneGuessers[$key] = $guesser;
}

private function addFinder(string $key, TimezoneFinder $finder): void
{
$this->timezoneFinders[$key] = $finder;
}

/**
* This method will try to find out the correct timezone for an iCalendar
* date-time value.
*
* You must pass the contents of the TZID parameter, as well as the full
* calendar.
*
* If the lookup fails, this method will return the default PHP timezone
* (as configured using date_default_timezone_set, or the date.timezone ini
* setting).
*
* Alternatively, if $failIfUncertain is set to true, it will throw an
* exception if we cannot accurately determine the timezone.
*/
private function findTimeZone(string $tzid, Component $vcalendar = null, bool $failIfUncertain = false): DateTimeZone
{
foreach ($this->timezoneFinders as $timezoneFinder) {
$timezone = $timezoneFinder->find($tzid, $failIfUncertain);
if (!$timezone instanceof DateTimeZone) {
continue;
}

return $timezone;
}

if ($vcalendar) {
// If that didn't work, we will scan VTIMEZONE objects
foreach ($vcalendar->select('VTIMEZONE') as $vtimezone) {
if ((string) $vtimezone->TZID === $tzid) {
foreach ($this->timezoneGuessers as $timezoneGuesser) {
$timezone = $timezoneGuesser->guess($vtimezone, $failIfUncertain);
if (!$timezone instanceof DateTimeZone) {
continue;
}

return $timezone;
}
}
}
}

if ($failIfUncertain) {
throw new InvalidArgumentException('We were unable to determine the correct PHP timezone for tzid: '.$tzid);
}

// If we got all the way here, we default to whatever has been set as the PHP default timezone.
return new DateTimeZone(date_default_timezone_get());
}

public static function addTimezoneGuesser(string $key, TimezoneGuesser $guesser): void
{
self::getInstance()->addGuesser($key, $guesser);
}

public static function addTimezoneFinder(string $key, TimezoneFinder $finder): void
{
self::getInstance()->addFinder($key, $finder);
}

/**
* @param string $tzid
* @param false $failIfUncertain
*
* @return DateTimeZone
*/
public static function getTimeZone($tzid, Component $vcalendar = null, $failIfUncertain = false)
{
return self::getInstance()->findTimeZone($tzid, $vcalendar, $failIfUncertain);
}

public static function clean(): void
{
self::$instance = null;
}

// Keeping things for backwards compatibility
/**
* @var array|null
*
* @deprecated
*/
public static $map = null;

/**
* List of microsoft exchange timezone ids.
*
* Source: http://msdn.microsoft.com/en-us/library/aa563018(loband).aspx
*
* @deprecated
*/
public static $microsoftExchangeMap = [
0 => 'UTC',
31 => 'Africa/Casablanca',

// Insanely, id #2 is used for both Europe/Lisbon, and Europe/Sarajevo.
// I'm not even kidding.. We handle this special case in the
// getTimeZone method.
Expand Down Expand Up @@ -103,135 +232,11 @@ class TimeZoneUtil
39 => 'Pacific/Kwajalein',
];

/**
* This method will try to find out the correct timezone for an iCalendar
* date-time value.
*
* You must pass the contents of the TZID parameter, as well as the full
* calendar.
*
* If the lookup fails, this method will return the default PHP timezone
* (as configured using date_default_timezone_set, or the date.timezone ini
* setting).
*
* Alternatively, if $failIfUncertain is set to true, it will throw an
* exception if we cannot accurately determine the timezone.
*
* @param string $tzid
* @param Sabre\VObject\Component $vcalendar
*
* @return \DateTimeZone
*/
public static function getTimeZone($tzid, Component $vcalendar = null, $failIfUncertain = false)
{
// First we will just see if the tzid is a support timezone identifier.
//
// The only exception is if the timezone starts with (. This is to
// handle cases where certain microsoft products generate timezone
// identifiers that for instance look like:
//
// (GMT+01.00) Sarajevo/Warsaw/Zagreb
//
// Since PHP 5.5.10, the first bit will be used as the timezone and
// this method will return just GMT+01:00. This is wrong, because it
// doesn't take DST into account.
if ('(' !== $tzid[0]) {
// PHP has a bug that logs PHP warnings even it shouldn't:
// https://bugs.php.net/bug.php?id=67881
//
// That's why we're checking if we'll be able to successfully instantiate
// \DateTimeZone() before doing so. Otherwise we could simply instantiate
// and catch the exception.
$tzIdentifiers = \DateTimeZone::listIdentifiers();

try {
if (
(in_array($tzid, $tzIdentifiers)) ||
(preg_match('/^GMT(\+|-)([0-9]{4})$/', $tzid, $matches)) ||
(in_array($tzid, self::getIdentifiersBC()))
) {
return new \DateTimeZone($tzid);
}
} catch (\Exception $e) {
}
}

self::loadTzMaps();

// Next, we check if the tzid is somewhere in our tzid map.
if (isset(self::$map[$tzid])) {
return new \DateTimeZone(self::$map[$tzid]);
}

// Some Microsoft products prefix the offset first, so let's strip that off
// and see if it is our tzid map. We don't want to check for this first just
// in case there are overrides in our tzid map.
if (preg_match('/^\((UTC|GMT)(\+|\-)[\d]{2}\:[\d]{2}\) (.*)/', $tzid, $matches)) {
$tzidAlternate = $matches[3];
if (isset(self::$map[$tzidAlternate])) {
return new \DateTimeZone(self::$map[$tzidAlternate]);
}
}

// Maybe the author was hyper-lazy and just included an offset. We
// support it, but we aren't happy about it.
if (preg_match('/^GMT(\+|-)([0-9]{4})$/', $tzid, $matches)) {
// Note that the path in the source will never be taken from PHP 5.5.10
// onwards. PHP 5.5.10 supports the "GMT+0100" style of format, so it
// already gets returned early in this function. Once we drop support
// for versions under PHP 5.5.10, this bit can be taken out of the
// source.
// @codeCoverageIgnoreStart
return new \DateTimeZone('Etc/GMT'.$matches[1].ltrim(substr($matches[2], 0, 2), '0'));
// @codeCoverageIgnoreEnd
}

if ($vcalendar) {
// If that didn't work, we will scan VTIMEZONE objects
foreach ($vcalendar->select('VTIMEZONE') as $vtimezone) {
if ((string) $vtimezone->TZID === $tzid) {
// Some clients add 'X-LIC-LOCATION' with the olson name.
if (isset($vtimezone->{'X-LIC-LOCATION'})) {
$lic = (string) $vtimezone->{'X-LIC-LOCATION'};

// Libical generators may specify strings like
// "SystemV/EST5EDT". For those we must remove the
// SystemV part.
if ('SystemV/' === substr($lic, 0, 8)) {
$lic = substr($lic, 8);
}

return self::getTimeZone($lic, null, $failIfUncertain);
}
// Microsoft may add a magic number, which we also have an
// answer for.
if (isset($vtimezone->{'X-MICROSOFT-CDO-TZID'})) {
$cdoId = (int) $vtimezone->{'X-MICROSOFT-CDO-TZID'}->getValue();

// 2 can mean both Europe/Lisbon and Europe/Sarajevo.
if (2 === $cdoId && false !== strpos((string) $vtimezone->TZID, 'Sarajevo')) {
return new \DateTimeZone('Europe/Sarajevo');
}

if (isset(self::$microsoftExchangeMap[$cdoId])) {
return new \DateTimeZone(self::$microsoftExchangeMap[$cdoId]);
}
}
}
}
}

if ($failIfUncertain) {
throw new \InvalidArgumentException('We were unable to determine the correct PHP timezone for tzid: '.$tzid);
}

// If we got all the way here, we default to UTC.
return new \DateTimeZone(date_default_timezone_get());
}

/**
* This method will load in all the tz mapping information, if it's not yet
* done.
*
* @deprecated
*/
public static function loadTzMaps()
{
Expand All @@ -257,6 +262,8 @@ public static function loadTzMaps()
* (See timezonedata/php-bc.php and timezonedata php-workaround.php)
*
* @return array
*
* @deprecated
*/
public static function getIdentifiersBC()
{
Expand Down
31 changes: 31 additions & 0 deletions lib/TimezoneGuesser/FindFromOffset.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<?php

declare(strict_types=1);

namespace Sabre\VObject\TimezoneGuesser;

use DateTimeZone;

/**
* Some clients add 'X-LIC-LOCATION' with the olson name.
*/
class FindFromOffset implements TimezoneFinder
{
public function find(string $tzid, bool $failIfUncertain = false): ?DateTimeZone
{
// Maybe the author was hyper-lazy and just included an offset. We
// support it, but we aren't happy about it.
if (preg_match('/^GMT(\+|-)([0-9]{4})$/', $tzid, $matches)) {
// Note that the path in the source will never be taken from PHP 5.5.10
// onwards. PHP 5.5.10 supports the "GMT+0100" style of format, so it
// already gets returned early in this function. Once we drop support
// for versions under PHP 5.5.10, this bit can be taken out of the
// source.
// @codeCoverageIgnoreStart
return new DateTimeZone('Etc/GMT'.$matches[1].ltrim(substr($matches[2], 0, 2), '0'));
// @codeCoverageIgnoreEnd
}

return null;
}
}
Loading

0 comments on commit 9157772

Please sign in to comment.