Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
20.97% |
13 / 62 |
|
60.00% |
6 / 10 |
CRAP | |
0.00% |
0 / 1 |
UserTimeCorrection | |
20.97% |
13 / 62 |
|
60.00% |
6 / 10 |
474.28 | |
0.00% |
0 / 1 |
__construct | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 | |||
getCorrectionType | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getTimeOffset | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getTimeOffsetInterval | |
80.00% |
4 / 5 |
|
0.00% |
0 / 1 |
2.03 | |||
getTimeZone | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
isValid | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
parse | |
0.00% |
0 / 40 |
|
0.00% |
0 / 1 |
210 | |||
formatTimezoneOffset | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
2 | |||
toString | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
42 | |||
__toString | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 |
1 | <?php |
2 | |
3 | /** |
4 | * This program is free software; you can redistribute it and/or modify |
5 | * it under the terms of the GNU General Public License as published by |
6 | * the Free Software Foundation; either version 2 of the License, or |
7 | * (at your option) any later version. |
8 | * |
9 | * This program is distributed in the hope that it will be useful, |
10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
12 | * GNU General Public License for more details. |
13 | * |
14 | * You should have received a copy of the GNU General Public License along |
15 | * with this program; if not, write to the Free Software Foundation, Inc., |
16 | * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
17 | * http://www.gnu.org/copyleft/gpl.html |
18 | * |
19 | * @file |
20 | * @author Derk-Jan Hartman <hartman.wiki@gmail.com> |
21 | */ |
22 | |
23 | namespace MediaWiki\User; |
24 | |
25 | use DateInterval; |
26 | use DateTime; |
27 | use DateTimeZone; |
28 | use Exception; |
29 | use MediaWiki\Utils\MWTimestamp; |
30 | use Stringable; |
31 | use Wikimedia\RequestTimeout\TimeoutException; |
32 | |
33 | /** |
34 | * Utility class to parse the TimeCorrection string value. |
35 | * |
36 | * These values are used to specify the time offset for a user and are stored in |
37 | * the database as a user preference and returned by the preferences APIs |
38 | * |
39 | * The class will correct invalid input and adjusts timezone offsets to applicable dates, |
40 | * taking into account DST etc. |
41 | * |
42 | * @since 1.37 |
43 | */ |
44 | class UserTimeCorrection implements Stringable { |
45 | |
46 | /** |
47 | * @var string (default) Time correction based on the MediaWiki's system offset from UTC. |
48 | * The System offset can be configured with wgLocalTimezone and/or wgLocalTZoffset |
49 | */ |
50 | public const SYSTEM = 'System'; |
51 | |
52 | /** @var string Time correction based on a user defined offset from UTC */ |
53 | public const OFFSET = 'Offset'; |
54 | |
55 | /** @var string Time correction based on a user defined timezone */ |
56 | public const ZONEINFO = 'ZoneInfo'; |
57 | |
58 | /** @var DateTime */ |
59 | private $date; |
60 | |
61 | /** @var bool */ |
62 | private $valid; |
63 | |
64 | /** @var string */ |
65 | private $correctionType; |
66 | |
67 | /** @var int Offset in minutes */ |
68 | private $offset; |
69 | |
70 | /** @var DateTimeZone|null */ |
71 | private $timeZone; |
72 | |
73 | /** |
74 | * @param string $timeCorrection Original time correction string |
75 | * @param DateTime|null $relativeToDate The date used to calculate the time zone offset of. |
76 | * This defaults to the current date and time. |
77 | * @param int $systemOffset Offset for self::SYSTEM in minutes |
78 | */ |
79 | public function __construct( |
80 | string $timeCorrection, |
81 | ?DateTime $relativeToDate = null, |
82 | int $systemOffset = 0 |
83 | ) { |
84 | $this->date = $relativeToDate ?? new DateTime( '@' . MWTimestamp::time() ); |
85 | $this->valid = false; |
86 | $this->parse( $timeCorrection, $systemOffset ); |
87 | } |
88 | |
89 | /** |
90 | * Get time offset for a user |
91 | * |
92 | * @return string Offset that was applied to the user |
93 | */ |
94 | public function getCorrectionType(): string { |
95 | return $this->correctionType; |
96 | } |
97 | |
98 | /** |
99 | * Get corresponding time offset for this correction |
100 | * Note: When correcting dates/times, apply only the offset OR the time zone, not both. |
101 | * @return int Offset in minutes |
102 | */ |
103 | public function getTimeOffset(): int { |
104 | return $this->offset; |
105 | } |
106 | |
107 | /** |
108 | * Get corresponding time offset for this correction |
109 | * Note: When correcting dates/times, apply only the offset OR the time zone, not both. |
110 | * @return DateInterval Offset in minutes as a DateInterval |
111 | */ |
112 | public function getTimeOffsetInterval(): DateInterval { |
113 | $offset = abs( $this->offset ); |
114 | $interval = new DateInterval( "PT{$offset}M" ); |
115 | if ( $this->offset < 1 ) { |
116 | $interval->invert = 1; |
117 | } |
118 | return $interval; |
119 | } |
120 | |
121 | /** |
122 | * The time zone if known |
123 | * Note: When correcting dates/times, apply only the offset OR the time zone, not both. |
124 | * @return DateTimeZone|null |
125 | */ |
126 | public function getTimeZone(): ?DateTimeZone { |
127 | return $this->timeZone; |
128 | } |
129 | |
130 | /** |
131 | * Was the original correction specification valid |
132 | * @return bool |
133 | */ |
134 | public function isValid(): bool { |
135 | return $this->valid; |
136 | } |
137 | |
138 | /** |
139 | * Parse the timecorrection string as stored in the database for a user |
140 | * or as entered into the Preferences form field |
141 | * |
142 | * There can be two forms of these strings: |
143 | * 1. A pipe separated tuple of a maximum of 3 fields |
144 | * - Field 1 is the type of offset definition |
145 | * - Field 2 is the offset in minutes from UTC (ignored for System type) |
146 | * FIXME Since it's ignored, remove the offset from System everywhere. |
147 | * - Field 3 is a timezone identifier from the tz database (only required for ZoneInfo type) |
148 | * - The offset for a ZoneInfo type is unreliable because of DST. |
149 | * After retrieving it from the database, it should be recalculated based on the TZ identifier. |
150 | * Examples: |
151 | * - System |
152 | * - System|60 |
153 | * - Offset|60 |
154 | * - ZoneInfo|60|Europe/Amsterdam |
155 | * |
156 | * 2. The following form provides an offset in hours and minutes |
157 | * This currently should only be used by the preferences input field, |
158 | * but historically they were present in the database. |
159 | * TODO: write a maintenance script to migrate these old db values |
160 | * Examples: |
161 | * - 16:00 |
162 | * - 10 |
163 | * |
164 | * @param string $timeCorrection |
165 | * @param int $systemOffset |
166 | */ |
167 | private function parse( string $timeCorrection, int $systemOffset ) { |
168 | $data = explode( '|', $timeCorrection, 3 ); |
169 | |
170 | // First handle the case of an actual timezone being specified. |
171 | if ( $data[0] === self::ZONEINFO ) { |
172 | try { |
173 | $this->correctionType = self::ZONEINFO; |
174 | $this->timeZone = new DateTimeZone( $data[2] ); |
175 | $this->offset = (int)floor( $this->timeZone->getOffset( $this->date ) / 60 ); |
176 | $this->valid = true; |
177 | return; |
178 | } catch ( TimeoutException $e ) { |
179 | throw $e; |
180 | } catch ( Exception $e ) { |
181 | // Not a valid/known timezone. |
182 | // Fall back to any specified offset |
183 | } |
184 | } |
185 | |
186 | // If $timeCorrection is in fact a pipe-separated value, check the |
187 | // first value. |
188 | switch ( $data[0] ) { |
189 | case self::OFFSET: |
190 | case self::ZONEINFO: |
191 | $this->correctionType = self::OFFSET; |
192 | // First value is Offset, so use the specified offset |
193 | $this->offset = (int)( $data[1] ?? 0 ); |
194 | // If this is ZoneInfo, then we didn't recognize the TimeZone |
195 | $this->valid = isset( $data[1] ) && $data[0] === self::OFFSET; |
196 | break; |
197 | case self::SYSTEM: |
198 | $this->correctionType = self::SYSTEM; |
199 | $this->offset = $systemOffset; |
200 | $this->valid = true; |
201 | break; |
202 | default: |
203 | // $timeCorrection actually isn't a pipe separated value, but instead |
204 | // a colon separated value. This is only used by the HTMLTimezoneField userinput |
205 | // but can also still be present in the Db. (but shouldn't be) |
206 | $this->correctionType = self::OFFSET; |
207 | $data = explode( ':', $timeCorrection, 2 ); |
208 | if ( count( $data ) >= 2 ) { |
209 | // Combination hours and minutes. |
210 | $this->offset = abs( (int)$data[0] ) * 60 + (int)$data[1]; |
211 | if ( (int)$data[0] < 0 ) { |
212 | $this->offset *= -1; |
213 | } |
214 | $this->valid = true; |
215 | } elseif ( preg_match( '/^[+-]?\d+$/', $data[0] ) ) { |
216 | // Just hours. |
217 | $this->offset = (int)$data[0] * 60; |
218 | $this->valid = true; |
219 | } else { |
220 | // We really don't know this. Fallback to System |
221 | $this->correctionType = self::SYSTEM; |
222 | $this->offset = $systemOffset; |
223 | return; |
224 | } |
225 | break; |
226 | } |
227 | |
228 | // Max is +14:00 and min is -12:00, see: |
229 | // https://en.wikipedia.org/wiki/Timezone |
230 | if ( $this->offset < -12 * 60 || $this->offset > 14 * 60 ) { |
231 | $this->valid = false; |
232 | } |
233 | // 14:00 |
234 | $this->offset = min( $this->offset, 14 * 60 ); |
235 | // -12:00 |
236 | $this->offset = max( $this->offset, -12 * 60 ); |
237 | } |
238 | |
239 | /** |
240 | * Converts a timezone offset in minutes (e.g., "120") to an hh:mm string like "+02:00". |
241 | * @param int $offset |
242 | * @return string |
243 | */ |
244 | public static function formatTimezoneOffset( int $offset ): string { |
245 | $hours = $offset > 0 ? floor( $offset / 60 ) : ceil( $offset / 60 ); |
246 | return sprintf( '%+03d:%02d', $hours, abs( $offset ) % 60 ); |
247 | } |
248 | |
249 | /** |
250 | * Note: The string value of this object might not be equal to the original value |
251 | * @return string a timecorrection string representing this value |
252 | */ |
253 | public function toString(): string { |
254 | switch ( $this->correctionType ) { |
255 | case self::ZONEINFO: |
256 | if ( $this->timeZone ) { |
257 | return "ZoneInfo|{$this->offset}|{$this->timeZone->getName()}"; |
258 | } |
259 | // If not, fallback: |
260 | case self::SYSTEM: |
261 | case self::OFFSET: |
262 | default: |
263 | return "{$this->correctionType}|{$this->offset}"; |
264 | } |
265 | } |
266 | |
267 | public function __toString() { |
268 | return $this->toString(); |
269 | } |
270 | } |