aboutsummaryrefslogtreecommitdiffstats
path: root/includes
diff options
context:
space:
mode:
authorjenkins-bot <jenkins-bot@gerrit.wikimedia.org>2021-05-07 18:43:08 +0000
committerGerrit Code Review <gerrit@wikimedia.org>2021-05-07 18:43:08 +0000
commit41d7fa917f06621e141bd660349dce17723446c5 (patch)
tree67faf65103351745c9248cf01c6f4ad5ccfeb531 /includes
parentb6744c24fbec8abd0d910ec8150abe250aa2e31c (diff)
parent65c955746c5953e8747e27ce69fe1666581be82c (diff)
downloadmediawikicore-41d7fa917f06621e141bd660349dce17723446c5.tar.gz
mediawikicore-41d7fa917f06621e141bd660349dce17723446c5.zip
Merge "Split TimeCorrection parser into separate class"
Diffstat (limited to 'includes')
-rw-r--r--includes/MWTimestamp.php62
-rw-r--r--includes/preferences/DefaultPreferencesFactory.php2
-rw-r--r--includes/preferences/TimezoneFilter.php50
-rw-r--r--includes/user/UserOptionsManager.php13
-rw-r--r--includes/user/UserTimeCorrection.php252
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();
+ }
+}