diff options
author | jenkins-bot <jenkins-bot@gerrit.wikimedia.org> | 2021-05-07 18:43:08 +0000 |
---|---|---|
committer | Gerrit Code Review <gerrit@wikimedia.org> | 2021-05-07 18:43:08 +0000 |
commit | 41d7fa917f06621e141bd660349dce17723446c5 (patch) | |
tree | 67faf65103351745c9248cf01c6f4ad5ccfeb531 /includes | |
parent | b6744c24fbec8abd0d910ec8150abe250aa2e31c (diff) | |
parent | 65c955746c5953e8747e27ce69fe1666581be82c (diff) | |
download | mediawikicore-41d7fa917f06621e141bd660349dce17723446c5.tar.gz mediawikicore-41d7fa917f06621e141bd660349dce17723446c5.zip |
Merge "Split TimeCorrection parser into separate class"
Diffstat (limited to 'includes')
-rw-r--r-- | includes/MWTimestamp.php | 62 | ||||
-rw-r--r-- | includes/preferences/DefaultPreferencesFactory.php | 2 | ||||
-rw-r--r-- | includes/preferences/TimezoneFilter.php | 50 | ||||
-rw-r--r-- | includes/user/UserOptionsManager.php | 13 | ||||
-rw-r--r-- | includes/user/UserTimeCorrection.php | 252 |
5 files changed, 280 insertions, 99 deletions
diff --git a/includes/MWTimestamp.php b/includes/MWTimestamp.php index 786e86a09913..d92e69fbc30f 100644 --- a/includes/MWTimestamp.php +++ b/includes/MWTimestamp.php @@ -24,6 +24,7 @@ use MediaWiki\MediaWikiServices; use MediaWiki\User\UserIdentity; +use MediaWiki\User\UserTimeCorrection; use Wikimedia\Timestamp\ConvertibleTimestamp; /** @@ -82,62 +83,21 @@ class MWTimestamp extends ConvertibleTimestamp { * @return DateInterval Offset that was applied to the timestamp */ public function offsetForUser( UserIdentity $user ) { - global $wgLocalTZoffset; - $option = MediaWikiServices::getInstance() ->getUserOptionsLookup() ->getOption( $user, 'timecorrection' ); - $data = explode( '|', $option, 3 ); - - // First handle the case of an actual timezone being specified. - if ( $data[0] == 'ZoneInfo' ) { - try { - $tz = new DateTimeZone( $data[2] ); - } catch ( Exception $e ) { - $tz = false; - } - - if ( $tz ) { - $this->timestamp->setTimezone( $tz ); - return new DateInterval( 'P0Y' ); - } - - $data[0] = 'Offset'; + $value = new UserTimeCorrection( + $option, + $this->timestamp, + MediaWikiServices::getInstance()->getMainConfig()->get( 'LocalTZoffset' ) + ); + $tz = $value->getTimeZone(); + if ( $tz ) { + $this->timestamp->setTimezone( $tz ); + return new DateInterval( 'P0Y' ); } - - $diff = 0; - // If $option is in fact a pipe-separated value, check the - // first value. - if ( $data[0] == 'System' ) { - // First value is System, so use the system offset. - if ( $wgLocalTZoffset !== null ) { - $diff = $wgLocalTZoffset; - } - } elseif ( $data[0] == 'Offset' ) { - // First value is Offset, so use the specified offset - $diff = (int)$data[1]; - } else { - // $option actually isn't a pipe separated value, but instead - // a comma separated value. Isn't MediaWiki fun? - $data = explode( ':', $option ); - if ( count( $data ) >= 2 ) { - // Combination hours and minutes. - $diff = abs( (int)$data[0] ) * 60 + (int)$data[1]; - if ( (int)$data[0] < 0 ) { - $diff *= -1; - } - } else { - // Just hours. - $diff = (int)$data[0] * 60; - } - } - - $interval = new DateInterval( 'PT' . abs( $diff ) . 'M' ); - if ( $diff < 1 ) { - $interval->invert = 1; - } - + $interval = $value->getTimeOffsetInterval(); $this->timestamp->add( $interval ); return $interval; } diff --git a/includes/preferences/DefaultPreferencesFactory.php b/includes/preferences/DefaultPreferencesFactory.php index 17f99594e68e..38c412083d8a 100644 --- a/includes/preferences/DefaultPreferencesFactory.php +++ b/includes/preferences/DefaultPreferencesFactory.php @@ -1709,7 +1709,7 @@ class DefaultPreferencesFactory implements PreferencesFactory { $timestamp = MWTimestamp::getLocalInstance(); // Check that the LocalTZoffset is the same as the local time zone offset - if ( $localTZoffset == $timestamp->format( 'Z' ) / 60 ) { + if ( $localTZoffset === $timestamp->format( 'Z' ) / 60 ) { $timezoneName = $timestamp->getTimezone()->getName(); // Localize timezone if ( isset( $timeZoneList[$timezoneName] ) ) { diff --git a/includes/preferences/TimezoneFilter.php b/includes/preferences/TimezoneFilter.php index b73aa53a0d9f..7864bc12a7fc 100644 --- a/includes/preferences/TimezoneFilter.php +++ b/includes/preferences/TimezoneFilter.php @@ -20,8 +20,7 @@ namespace MediaWiki\Preferences; -use DateTimeZone; -use Exception; +use MediaWiki\User\UserTimeCorrection; class TimezoneFilter implements Filter { @@ -36,50 +35,9 @@ class TimezoneFilter implements Filter { * @inheritDoc */ public function filterFromForm( $tz ) { - $data = explode( '|', $tz, 3 ); - switch ( $data[0] ) { - case 'ZoneInfo': - $valid = false; - - if ( count( $data ) === 3 ) { - // Make sure this timezone exists - try { - // @phan-suppress-next-line PhanNoopNew - new DateTimeZone( $data[2] ); - // If the constructor didn't throw, we know it's valid - $valid = true; - } catch ( Exception $e ) { - // Not a valid timezone - } - } - - if ( !$valid ) { - // If the supplied timezone doesn't exist, fall back to the encoded offset - return 'Offset|' . intval( $tz[1] ); - } - return $tz; - case 'System': - return $tz; - default: - $data = explode( ':', $tz, 2 ); - if ( count( $data ) == 2 ) { - $data[0] = intval( $data[0] ); - $data[1] = intval( $data[1] ); - $minDiff = abs( $data[0] ) * 60 + $data[1]; - if ( $data[0] < 0 ) { - $minDiff = -$minDiff; - } - } else { - $minDiff = intval( $data[0] ) * 60; - } - - # Max is +14:00 and min is -12:00, see: - # https://en.wikipedia.org/wiki/Timezone - # 14:00 - $minDiff = min( $minDiff, 840 ); - # -12:00 - $minDiff = max( $minDiff, -720 ); - return 'Offset|' . $minDiff; + if ( $tz === UserTimeCorrection::SYSTEM ) { + return $tz; } + return ( new UserTimeCorrection( $tz ) )->toString(); } } diff --git a/includes/user/UserOptionsManager.php b/includes/user/UserOptionsManager.php index 914aa38b118f..d6352a6c1110 100644 --- a/includes/user/UserOptionsManager.php +++ b/includes/user/UserOptionsManager.php @@ -47,7 +47,8 @@ class UserOptionsManager extends UserOptionsLookup { * @internal For use by ServiceWiring */ public const CONSTRUCTOR_OPTIONS = [ - 'HiddenPrefs' + 'HiddenPrefs', + 'LocalTZoffset', ]; /** @var ServiceOptions */ @@ -551,6 +552,16 @@ class UserOptionsManager extends UserOptionsLookup { // Replace deprecated language codes $options['language'] = LanguageCode::replaceDeprecatedCodes( $options['language'] ); + // Fix up timezone offset (Due to DST it can change from what was stored in the DB) + // ZoneInfo|offset|TimeZoneName + if ( isset( $options['timecorrection'] ) ) { + $options['timecorrection'] = ( new UserTimeCorrection( + $options['timecorrection'], + null, + $this->serviceOptions->get( 'LocalTZoffset' ) + ) )->toString(); + } + // Need to store what we have so far before the hook to prevent // infinite recursion if the hook attempts to reload options $this->originalOptionsCache[$userKey] = $options; diff --git a/includes/user/UserTimeCorrection.php b/includes/user/UserTimeCorrection.php new file mode 100644 index 000000000000..04a432521ae5 --- /dev/null +++ b/includes/user/UserTimeCorrection.php @@ -0,0 +1,252 @@ +<?php + +/** + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * http://www.gnu.org/copyleft/gpl.html + * + * @file + * @author Derk-Jan Hartman <hartman.wiki@gmail.com> + */ + +namespace MediaWiki\User; + +use DateInterval; +use DateTime; +use DateTimeZone; +use Exception; + +/** + * Utility class to parse the TimeCorrection string value. + * + * These values are used to specify the time offset for a user and are stored in + * the database as a user preference and returned by the preferences APIs + * + * The class will correct invalid input and adjusts timezone offsets to applicable dates, + * taking into account DST etc. + * + * @since 1.37 + */ +class UserTimeCorrection { + + /** + * @var string (default) Time correction based on the MediaWiki's system offset from UTC. + * The System offset can be configured with wgLocalTimezone and/or wgLocalTZoffset + */ + public const SYSTEM = 'System'; + + /** @var string Time correction based on a user defined offset from UTC */ + public const OFFSET = 'Offset'; + + /** @var string Time correction based on a user defined timezone */ + public const ZONEINFO = 'ZoneInfo'; + + /* @var DateTime */ + private $date; + + /* @var bool */ + private $valid; + + /* @var string */ + private $correctionType; + + /* @var int Offset in minutes */ + private $offset; + + /* @var DateTimeZone|null */ + private $timeZone; + + /** + * @param string $timeCorrection Original time correction string + * @param DateTime|null $relativeToDate The date used to calculate the time zone offset of. + * This defaults to the current date and time. + * @param int $offset An offset in minutes (default 0) + */ + public function __construct( + string $timeCorrection, + DateTime $relativeToDate = null, + int $offset = 0 + ) { + $this->date = $relativeToDate ?? new DateTime(); + $this->offset = $offset; + $this->valid = false; + $this->parse( $timeCorrection ); + } + + /** + * Get time offset for a user + * + * @return string Offset that was applied to the user + */ + public function getCorrectionType() : string { + return $this->correctionType; + } + + /** + * Get corresponding time offset for this correction + * Note: When correcting dates/times, apply only the offset OR the time zone, not both. + * @return int Offset in minutes + */ + public function getTimeOffset() : int { + return $this->offset; + } + + /** + * Get corresponding time offset for this correction + * Note: When correcting dates/times, apply only the offset OR the time zone, not both. + * @return DateInterval Offset in minutes as a DateInterval + */ + public function getTimeOffsetInterval() : DateInterval { + $offset = abs( $this->offset ); + $interval = new DateInterval( "PT{$offset}M" ); + if ( $this->offset < 1 ) { + $interval->invert = 1; + } + return $interval; + } + + /** + * The time zone if known + * Note: When correcting dates/times, apply only the offset OR the time zone, not both. + * @return DateTimeZone|null + */ + public function getTimeZone() : ?DateTimeZone { + return $this->timeZone; + } + + /** + * Was the original correction specification valid + * @return bool + */ + public function isValid() : bool { + return $this->valid; + } + + /** + * Parse the timecorrection string as stored in the database for a user + * or as entered into the Preferences form field + * + * There can be two forms of these strings: + * 1. A pipe separated tuple of a maximum of 3 fields + * - Field 1 is the type of offset definition + * - Field 2 is the offset in minutes from UTC (optional for System type) + * - Field 3 is a timezone identifier from the tz database (only required for ZoneInfo type) + * - The offset for a ZoneInfo type is unreliable because of DST. + * After retrieving it from the database, it should be recalculated based on the TZ identifier. + * Examples: + * - System + * - System|60 + * - Offset|60 + * - ZoneInfo|60|Europe/Amsterdam + * + * 2. The following form provides an offset in hours and minutes + * This currently should only be used by the preferences input field, + * but historically they were present in the database. + * TODO: write a maintenance script to migrate these old db values + * Examples: + * - 16:00 + * - 10 + * + * @param string $timeCorrection + */ + private function parse( string $timeCorrection ) { + $data = explode( '|', $timeCorrection, 3 ); + + // First handle the case of an actual timezone being specified. + if ( $data[0] === self::ZONEINFO ) { + try { + $this->correctionType = self::ZONEINFO; + $this->timeZone = new DateTimeZone( $data[2] ); + $this->offset = floor( $this->timeZone->getOffset( $this->date ) / 60 ); + $this->valid = true; + return; + } catch ( Exception $e ) { + // Not a valid/known timezone. + // Fall back to any specified offset + } + } + + // If $timeCorrection is in fact a pipe-separated value, check the + // first value. + switch ( $data[0] ) { + case self::OFFSET: + case self::ZONEINFO: + $this->correctionType = self::OFFSET; + // First value is Offset, so use the specified offset + $this->offset = (int)( $data[1] ?? 0 ); + // If this is ZoneInfo, then we didn't recognize the TimeZone + $this->valid = isset( $data[1] ) && $data[0] === self::OFFSET; + return; + case self::SYSTEM: + $this->correctionType = self::SYSTEM; + $this->valid = true; + return; + } + + // $timeCorrection actually isn't a pipe separated value, but instead + // a colon separated value. This is only used by the userinput of the preferences + // but can also still be present in the Db. (but shouldn't be) + $diff = 0; + $data = explode( ':', $timeCorrection, 2 ); + if ( count( $data ) >= 2 ) { + // Combination hours and minutes. + $diff = abs( (int)$data[0] ) * 60 + (int)$data[1]; + if ( (int)$data[0] < 0 ) { + $diff *= -1; + } + } elseif ( ctype_digit( $data[0] ) ) { + // Just hours. + $diff = (int)$data[0] * 60; + } else { + // We really don't know this. Fallback to System + $this->correctionType = self::SYSTEM; + return; + } + + // Max is +14:00 and min is -12:00, see: + // https://en.wikipedia.org/wiki/Timezone + if ( $diff >= -12 * 60 && $diff <= 14 * 60 ) { + $this->valid = true; + } + // 14:00 + $diff = min( $diff, 14 * 60 ); + // -12:00 + $diff = max( $diff, -12 * 60 ); + + $this->correctionType = self::OFFSET; + $this->offset = $diff; + } + + /** + * Note: The string value of this object might not be equal to the original value + * @return string a timecorrection string representing this value + */ + public function toString() : string { + switch ( $this->correctionType ) { + case self::ZONEINFO: + if ( $this->timeZone ) { + return "ZoneInfo|{$this->offset}|{$this->timeZone->getName()}"; + } + // If not, fallback: + case self::SYSTEM: + case self::OFFSET: + default: + return "{$this->correctionType}|{$this->offset}"; + } + } + + public function __toString() { + return $this->toString(); + } +} |