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 | * This program is free software; you can redistribute it and/or modify |
4 | * it under the terms of the GNU General Public License as published by |
5 | * the Free Software Foundation; either version 2 of the License, or |
6 | * (at your option) any later version. |
7 | * |
8 | * This program is distributed in the hope that it will be useful, |
9 | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
10 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
11 | * GNU General Public License for more details. |
12 | * |
13 | * You should have received a copy of the GNU General Public License along |
14 | * with this program; if not, write to the Free Software Foundation, Inc., |
15 | * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
16 | * http://www.gnu.org/copyleft/gpl.html |
17 | * |
18 | * @file |
19 | */ |
20 | |
21 | namespace MediaWiki\User; |
22 | |
23 | use DateInterval; |
24 | use DateTime; |
25 | use DateTimeZone; |
26 | use Exception; |
27 | use MediaWiki\Utils\MWTimestamp; |
28 | use Stringable; |
29 | use Wikimedia\RequestTimeout\TimeoutException; |
30 | |
31 | /** |
32 | * Utility class to parse the TimeCorrection string value. |
33 | * |
34 | * These values are used to specify the time offset for a user and are stored in |
35 | * the database as a user preference and returned by the preferences APIs |
36 | * |
37 | * The class will correct invalid input and adjusts timezone offsets to applicable dates, |
38 | * taking into account DST etc. |
39 | * |
40 | * @since 1.37 |
41 | * @ingroup User |
42 | * @author Derk-Jan Hartman <hartman.wiki@gmail.com> |
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 | */ |
133 | public function isValid(): bool { |
134 | return $this->valid; |
135 | } |
136 | |
137 | /** |
138 | * Parse the timecorrection string as stored in the database for a user |
139 | * or as entered into the Preferences form field |
140 | * |
141 | * There can be two forms of these strings: |
142 | * 1. A pipe separated tuple of a maximum of 3 fields |
143 | * - Field 1 is the type of offset definition |
144 | * - Field 2 is the offset in minutes from UTC (ignored for System type) |
145 | * FIXME Since it's ignored, remove the offset from System everywhere. |
146 | * - Field 3 is a timezone identifier from the tz database (only required for ZoneInfo type) |
147 | * - The offset for a ZoneInfo type is unreliable because of DST. |
148 | * After retrieving it from the database, it should be recalculated based on the TZ identifier. |
149 | * Examples: |
150 | * - System |
151 | * - System|60 |
152 | * - Offset|60 |
153 | * - ZoneInfo|60|Europe/Amsterdam |
154 | * |
155 | * 2. The following form provides an offset in hours and minutes |
156 | * This currently should only be used by the preferences input field, |
157 | * but historically they were present in the database. |
158 | * TODO: write a maintenance script to migrate these old db values |
159 | * Examples: |
160 | * - 16:00 |
161 | * - 10 |
162 | * |
163 | * @param string $timeCorrection |
164 | * @param int $systemOffset |
165 | */ |
166 | private function parse( string $timeCorrection, int $systemOffset ) { |
167 | $data = explode( '|', $timeCorrection, 3 ); |
168 | |
169 | // First handle the case of an actual timezone being specified. |
170 | if ( $data[0] === self::ZONEINFO ) { |
171 | try { |
172 | $this->correctionType = self::ZONEINFO; |
173 | $this->timeZone = new DateTimeZone( $data[2] ); |
174 | $this->offset = (int)floor( $this->timeZone->getOffset( $this->date ) / 60 ); |
175 | $this->valid = true; |
176 | return; |
177 | } catch ( TimeoutException $e ) { |
178 | throw $e; |
179 | } catch ( Exception $e ) { |
180 | // Not a valid/known timezone. |
181 | // Fall back to any specified offset |
182 | } |
183 | } |
184 | |
185 | // If $timeCorrection is in fact a pipe-separated value, check the |
186 | // first value. |
187 | switch ( $data[0] ) { |
188 | case self::OFFSET: |
189 | case self::ZONEINFO: |
190 | $this->correctionType = self::OFFSET; |
191 | // First value is Offset, so use the specified offset |
192 | $this->offset = (int)( $data[1] ?? 0 ); |
193 | // If this is ZoneInfo, then we didn't recognize the TimeZone |
194 | $this->valid = isset( $data[1] ) && $data[0] === self::OFFSET; |
195 | break; |
196 | case self::SYSTEM: |
197 | $this->correctionType = self::SYSTEM; |
198 | $this->offset = $systemOffset; |
199 | $this->valid = true; |
200 | break; |
201 | default: |
202 | // $timeCorrection actually isn't a pipe separated value, but instead |
203 | // a colon separated value. This is only used by the HTMLTimezoneField userinput |
204 | // but can also still be present in the Db. (but shouldn't be) |
205 | $this->correctionType = self::OFFSET; |
206 | $data = explode( ':', $timeCorrection, 2 ); |
207 | if ( count( $data ) >= 2 ) { |
208 | // Combination hours and minutes. |
209 | $this->offset = abs( (int)$data[0] ) * 60 + (int)$data[1]; |
210 | if ( (int)$data[0] < 0 ) { |
211 | $this->offset *= -1; |
212 | } |
213 | $this->valid = true; |
214 | } elseif ( preg_match( '/^[+-]?\d+$/', $data[0] ) ) { |
215 | // Just hours. |
216 | $this->offset = (int)$data[0] * 60; |
217 | $this->valid = true; |
218 | } else { |
219 | // We really don't know this. Fallback to System |
220 | $this->correctionType = self::SYSTEM; |
221 | $this->offset = $systemOffset; |
222 | return; |
223 | } |
224 | break; |
225 | } |
226 | |
227 | // Max is +14:00 and min is -12:00, see: |
228 | // https://en.wikipedia.org/wiki/Timezone |
229 | if ( $this->offset < -12 * 60 || $this->offset > 14 * 60 ) { |
230 | $this->valid = false; |
231 | } |
232 | // 14:00 |
233 | $this->offset = min( $this->offset, 14 * 60 ); |
234 | // -12:00 |
235 | $this->offset = max( $this->offset, -12 * 60 ); |
236 | } |
237 | |
238 | /** |
239 | * Converts a timezone offset in minutes (e.g., "120") to an hh:mm string like "+02:00". |
240 | * @param int $offset |
241 | * @return string |
242 | */ |
243 | public static function formatTimezoneOffset( int $offset ): string { |
244 | $hours = $offset > 0 ? floor( $offset / 60 ) : ceil( $offset / 60 ); |
245 | return sprintf( '%+03d:%02d', $hours, abs( $offset ) % 60 ); |
246 | } |
247 | |
248 | /** |
249 | * Note: The string value of this object might not be equal to the original value |
250 | * @return string a timecorrection string representing this value |
251 | */ |
252 | public function toString(): string { |
253 | switch ( $this->correctionType ) { |
254 | case self::ZONEINFO: |
255 | if ( $this->timeZone ) { |
256 | return "ZoneInfo|{$this->offset}|{$this->timeZone->getName()}"; |
257 | } |
258 | // If not, fallback: |
259 | case self::SYSTEM: |
260 | case self::OFFSET: |
261 | default: |
262 | return "{$this->correctionType}|{$this->offset}"; |
263 | } |
264 | } |
265 | |
266 | public function __toString() { |
267 | return $this->toString(); |
268 | } |
269 | } |