Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 78
0.00% covered (danger)
0.00%
0 / 5
CRAP
0.00% covered (danger)
0.00%
0 / 1
HTMLTimezoneField
0.00% covered (danger)
0.00%
0 / 77
0.00% covered (danger)
0.00%
0 / 5
306
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
12
 getTimezoneOptions
0.00% covered (danger)
0.00%
0 / 24
0.00% covered (danger)
0.00%
0 / 1
30
 getTimeZoneList
0.00% covered (danger)
0.00%
0 / 34
0.00% covered (danger)
0.00%
0 / 1
30
 validate
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
12
 getFieldClasses
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2
3namespace MediaWiki\HTMLForm\Field;
4
5use DateTime;
6use DateTimeZone;
7use InvalidArgumentException;
8use MediaWiki\Context\RequestContext;
9use MediaWiki\MainConfigNames;
10use MediaWiki\MediaWikiServices;
11use MediaWiki\User\UserTimeCorrection;
12use MediaWiki\Utils\MWTimestamp;
13use Wikimedia\Message\ITextFormatter;
14use Wikimedia\Message\MessageValue;
15
16/**
17 * Dropdown widget that allows the user to select a timezone, either by choosing a geographic zone, by using the wiki
18 * default, or by manually specifying an offset. It also has an option to fill the value from the browser settings.
19 * The value of this field is in a format accepted by UserTimeCorrection.
20 */
21class HTMLTimezoneField extends HTMLSelectOrOtherField {
22    private const FIELD_CLASS = 'mw-htmlform-timezone-field';
23
24    /** @var ITextFormatter */
25    private $msgFormatter;
26
27    /**
28     * @stable to call
29     * @inheritDoc
30     * Note that no options should be specified.
31     */
32    public function __construct( $params ) {
33        if ( isset( $params['options'] ) ) {
34            throw new InvalidArgumentException( "Options should not be provided to " . __CLASS__ );
35        }
36        $params['placeholder-message'] ??= 'timezone-useoffset-placeholder';
37        $params['options'] = [];
38        parent::__construct( $params );
39        $lang = $this->mParent ? $this->mParent->getLanguage() : RequestContext::getMain()->getLanguage();
40        $langCode = $lang->getCode();
41        $this->msgFormatter = MediaWikiServices::getInstance()->getMessageFormatterFactory()
42            ->getTextFormatter( $langCode );
43        $this->mOptions = $this->getTimezoneOptions();
44    }
45
46    /**
47     * @return array<string|string[]>
48     */
49    private function getTimezoneOptions(): array {
50        $opt = [];
51
52        $localTZoffset = MediaWikiServices::getInstance()->getMainConfig()->get( MainConfigNames::LocalTZoffset );
53        $timeZoneList = $this->getTimeZoneList();
54
55        $timestamp = MWTimestamp::getLocalInstance();
56        // Check that the LocalTZoffset is the same as the local time zone offset
57        if ( $localTZoffset === (int)$timestamp->format( 'Z' ) / 60 ) {
58            $timezoneName = $timestamp->getTimezone()->getName();
59            // Localize timezone
60            if ( isset( $timeZoneList[$timezoneName] ) ) {
61                $timezoneName = $timeZoneList[$timezoneName]['name'];
62            }
63            $server_tz_msg = $this->msgFormatter->format(
64                MessageValue::new( 'timezoneuseserverdefault', [ $timezoneName ] )
65            );
66        } else {
67            $tzstring = UserTimeCorrection::formatTimezoneOffset( $localTZoffset );
68            $server_tz_msg = $this->msgFormatter->format(
69                MessageValue::new( 'timezoneuseserverdefault', [ $tzstring ] )
70            );
71        }
72        $opt[$server_tz_msg] = "System|$localTZoffset";
73        $opt[$this->msgFormatter->format( MessageValue::new( 'timezoneuseoffset' ) )] = 'other';
74        $opt[$this->msgFormatter->format( MessageValue::new( 'guesstimezone' ) )] = 'guess';
75
76        foreach ( $timeZoneList as $timeZoneInfo ) {
77            $region = $timeZoneInfo['region'];
78            if ( !isset( $opt[$region] ) ) {
79                $opt[$region] = [];
80            }
81            $opt[$region][$timeZoneInfo['name']] = $timeZoneInfo['timecorrection'];
82        }
83        return $opt;
84    }
85
86    /**
87     * Get a list of all time zones
88     * @return string[][] A list of all time zones. The system name of the time zone is used as key and
89     *  the value is an array which contains localized name, the timecorrection value used for
90     *  preferences and the region
91     */
92    private function getTimeZoneList(): array {
93        $identifiers = DateTimeZone::listIdentifiers();
94        '@phan-var array|false $identifiers'; // See phan issue #3162
95        if ( $identifiers === false ) {
96            return [];
97        }
98        sort( $identifiers );
99
100        $tzRegions = [
101            'Africa' => $this->msgFormatter->format( MessageValue::new( 'timezoneregion-africa' ) ),
102            'America' => $this->msgFormatter->format( MessageValue::new( 'timezoneregion-america' ) ),
103            'Antarctica' => $this->msgFormatter->format( MessageValue::new( 'timezoneregion-antarctica' ) ),
104            'Arctic' => $this->msgFormatter->format( MessageValue::new( 'timezoneregion-arctic' ) ),
105            'Asia' => $this->msgFormatter->format( MessageValue::new( 'timezoneregion-asia' ) ),
106            'Atlantic' => $this->msgFormatter->format( MessageValue::new( 'timezoneregion-atlantic' ) ),
107            'Australia' => $this->msgFormatter->format( MessageValue::new( 'timezoneregion-australia' ) ),
108            'Europe' => $this->msgFormatter->format( MessageValue::new( 'timezoneregion-europe' ) ),
109            'Indian' => $this->msgFormatter->format( MessageValue::new( 'timezoneregion-indian' ) ),
110            'Pacific' => $this->msgFormatter->format( MessageValue::new( 'timezoneregion-pacific' ) ),
111        ];
112        asort( $tzRegions );
113
114        $timeZoneList = [];
115
116        $now = new DateTime();
117
118        foreach ( $identifiers as $identifier ) {
119            $parts = explode( '/', $identifier, 2 );
120
121            // DateTimeZone::listIdentifiers() returns a number of
122            // backwards-compatibility entries. This filters them out of the
123            // list presented to the user.
124            if ( count( $parts ) !== 2 || !array_key_exists( $parts[0], $tzRegions ) ) {
125                continue;
126            }
127
128            // Localize region
129            $parts[0] = $tzRegions[$parts[0]];
130
131            $dateTimeZone = new DateTimeZone( $identifier );
132            $minDiff = floor( $dateTimeZone->getOffset( $now ) / 60 );
133
134            $display = str_replace( '_', ' ', $parts[0] . '/' . $parts[1] );
135            $value = "ZoneInfo|$minDiff|$identifier";
136
137            $timeZoneList[$identifier] = [
138                'name' => $display,
139                'timecorrection' => $value,
140                'region' => $parts[0],
141            ];
142        }
143
144        return $timeZoneList;
145    }
146
147    /**
148     * @inheritDoc
149     */
150    public function validate( $value, $alldata ) {
151        $p = parent::validate( $value, $alldata );
152        if ( $p !== true ) {
153            return $p;
154        }
155
156        if ( !( new UserTimeCorrection( $value ) )->isValid() ) {
157            return $this->mParent->msg( 'timezone-invalid' )->escaped();
158        }
159
160        return true;
161    }
162
163    /**
164     * @inheritDoc
165     */
166    protected function getFieldClasses(): array {
167        $classes = parent::getFieldClasses();
168        $classes[] = self::FIELD_CLASS;
169        return $classes;
170    }
171}
172
173/** @deprecated class alias since 1.42 */
174class_alias( HTMLTimezoneField::class, 'HTMLTimezoneField' );