Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
97.73% |
86 / 88 |
|
88.24% |
15 / 17 |
CRAP | |
0.00% |
0 / 1 |
HttpDate | |
97.73% |
86 / 88 |
|
88.24% |
15 / 17 |
27 | |
0.00% |
0 / 1 |
parse | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
2 | |||
format | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
__construct | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
execute | |
100.00% |
16 / 16 |
|
100.00% |
1 / 1 |
4 | |||
consumeFixdate | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
1 | |||
consumeDayName | |
80.00% |
4 / 5 |
|
0.00% |
0 / 1 |
2.03 | |||
consumeDate1 | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
1 | |||
consumeDay | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
consumeMonth | |
80.00% |
4 / 5 |
|
0.00% |
0 / 1 |
2.03 | |||
consumeYear | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
consumeTimeOfDay | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
1 | |||
consumeRfc850Date | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
1 | |||
consumeDate2 | |
100.00% |
11 / 11 |
|
100.00% |
1 / 1 |
2 | |||
consumeDayNameLong | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
3 | |||
consumeAsctimeDate | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
1 | |||
consumeDate3 | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
2 | |||
getUnixTime | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 |
1 | <?php |
2 | |
3 | namespace MediaWiki\Rest\HeaderParser; |
4 | |
5 | /** |
6 | * This is a parser for "HTTP-date" as defined by RFC 7231. |
7 | * |
8 | * Normally in MediaWiki, dates in HTTP headers are converted using |
9 | * ConvertibleTimestamp or strtotime(), and this is encouraged by RFC 7231: |
10 | * |
11 | * "Recipients of timestamp values are encouraged to be robust in parsing |
12 | * timestamps unless otherwise restricted by the field definition." |
13 | * |
14 | * In the case of If-Modified-Since, we are in fact otherwise restricted, since |
15 | * RFC 7232 says: |
16 | * |
17 | * "A recipient MUST ignore the If-Modified-Since header field if the |
18 | * received field-value is not a valid HTTP-date" |
19 | * |
20 | * So it is not correct to use strtotime() or ConvertibleTimestamp to parse |
21 | * If-Modified-Since. |
22 | */ |
23 | class HttpDate extends HeaderParserBase { |
24 | private const DAY_NAMES = [ |
25 | 'Mon' => true, |
26 | 'Tue' => true, |
27 | 'Wed' => true, |
28 | 'Thu' => true, |
29 | 'Fri' => true, |
30 | 'Sat' => true, |
31 | 'Sun' => true |
32 | ]; |
33 | |
34 | private const MONTHS_BY_NAME = [ |
35 | 'Jan' => 1, |
36 | 'Feb' => 2, |
37 | 'Mar' => 3, |
38 | 'Apr' => 4, |
39 | 'May' => 5, |
40 | 'Jun' => 6, |
41 | 'Jul' => 7, |
42 | 'Aug' => 8, |
43 | 'Sep' => 9, |
44 | 'Oct' => 10, |
45 | 'Nov' => 11, |
46 | 'Dec' => 12, |
47 | ]; |
48 | |
49 | private const DAY_NAMES_LONG = [ |
50 | 'Monday', |
51 | 'Tuesday', |
52 | 'Wednesday', |
53 | 'Thursday', |
54 | 'Friday', |
55 | 'Saturday', |
56 | 'Sunday', |
57 | ]; |
58 | |
59 | /** @var string */ |
60 | private $dayName; |
61 | /** @var string */ |
62 | private $day; |
63 | /** @var int */ |
64 | private $month; |
65 | /** @var int */ |
66 | private $year; |
67 | /** @var string */ |
68 | private $hour; |
69 | /** @var string */ |
70 | private $minute; |
71 | /** @var string */ |
72 | private $second; |
73 | |
74 | /** |
75 | * Parse an HTTP-date string |
76 | * |
77 | * @param string $dateString |
78 | * @return int|null The UNIX timestamp, or null if the date was invalid |
79 | */ |
80 | public static function parse( $dateString ) { |
81 | $parser = new self( $dateString ); |
82 | if ( $parser->execute() ) { |
83 | return $parser->getUnixTime(); |
84 | } else { |
85 | return null; |
86 | } |
87 | } |
88 | |
89 | /** |
90 | * A convenience function to convert a UNIX timestamp to the preferred |
91 | * IMF-fixdate format for HTTP header output. |
92 | * |
93 | * @param int $unixTime |
94 | * @return false|string |
95 | */ |
96 | public static function format( $unixTime ) { |
97 | return gmdate( 'D, d M Y H:i:s \G\M\T', $unixTime ); |
98 | } |
99 | |
100 | /** |
101 | * Private constructor. Use the public static functions for public access. |
102 | * |
103 | * @param string $input |
104 | */ |
105 | private function __construct( $input ) { |
106 | $this->setInput( $input ); |
107 | } |
108 | |
109 | /** |
110 | * Parse the input string |
111 | * |
112 | * @return bool True for success |
113 | */ |
114 | private function execute() { |
115 | $this->pos = 0; |
116 | try { |
117 | $this->consumeFixdate(); |
118 | $this->assertEnd(); |
119 | return true; |
120 | } catch ( HeaderParserError $e ) { |
121 | } |
122 | $this->pos = 0; |
123 | try { |
124 | $this->consumeRfc850Date(); |
125 | $this->assertEnd(); |
126 | return true; |
127 | } catch ( HeaderParserError $e ) { |
128 | } |
129 | $this->pos = 0; |
130 | try { |
131 | $this->consumeAsctimeDate(); |
132 | $this->assertEnd(); |
133 | return true; |
134 | } catch ( HeaderParserError $e ) { |
135 | } |
136 | return false; |
137 | } |
138 | |
139 | /** |
140 | * Execute the IMF-fixdate rule, or throw an exception |
141 | * |
142 | * @throws HeaderParserError |
143 | */ |
144 | private function consumeFixdate() { |
145 | $this->consumeDayName(); |
146 | $this->consumeString( ', ' ); |
147 | $this->consumeDate1(); |
148 | $this->consumeString( ' ' ); |
149 | $this->consumeTimeOfDay(); |
150 | $this->consumeString( ' GMT' ); |
151 | } |
152 | |
153 | /** |
154 | * Execute the day-name rule, and capture the result. |
155 | * |
156 | * @throws HeaderParserError |
157 | */ |
158 | private function consumeDayName() { |
159 | $next3 = substr( $this->input, $this->pos, 3 ); |
160 | if ( isset( self::DAY_NAMES[$next3] ) ) { |
161 | $this->dayName = $next3; |
162 | $this->pos += 3; |
163 | } else { |
164 | $this->error( 'expected day-name' ); |
165 | } |
166 | } |
167 | |
168 | /** |
169 | * Execute the date1 rule |
170 | * |
171 | * @throws HeaderParserError |
172 | */ |
173 | private function consumeDate1() { |
174 | $this->consumeDay(); |
175 | $this->consumeString( ' ' ); |
176 | $this->consumeMonth(); |
177 | $this->consumeString( ' ' ); |
178 | $this->consumeYear(); |
179 | } |
180 | |
181 | /** |
182 | * Execute the day rule, and capture the result. |
183 | * |
184 | * @throws HeaderParserError |
185 | */ |
186 | private function consumeDay() { |
187 | $this->day = $this->consumeFixedDigits( 2 ); |
188 | } |
189 | |
190 | /** |
191 | * Execute the month rule, and capture the result |
192 | * |
193 | * @throws HeaderParserError |
194 | */ |
195 | private function consumeMonth() { |
196 | $next3 = substr( $this->input, $this->pos, 3 ); |
197 | if ( isset( self::MONTHS_BY_NAME[$next3] ) ) { |
198 | $this->month = self::MONTHS_BY_NAME[$next3]; |
199 | $this->pos += 3; |
200 | } else { |
201 | $this->error( 'expected month' ); |
202 | } |
203 | } |
204 | |
205 | /** |
206 | * Execute the year rule, and capture the result |
207 | * |
208 | * @throws HeaderParserError |
209 | */ |
210 | private function consumeYear() { |
211 | $this->year = (int)$this->consumeFixedDigits( 4 ); |
212 | } |
213 | |
214 | /** |
215 | * Execute the time-of-day rule |
216 | * @throws HeaderParserError |
217 | */ |
218 | private function consumeTimeOfDay() { |
219 | $this->hour = $this->consumeFixedDigits( 2 ); |
220 | $this->consumeString( ':' ); |
221 | $this->minute = $this->consumeFixedDigits( 2 ); |
222 | $this->consumeString( ':' ); |
223 | $this->second = $this->consumeFixedDigits( 2 ); |
224 | } |
225 | |
226 | /** |
227 | * Execute the rfc850-date rule |
228 | * |
229 | * @throws HeaderParserError |
230 | */ |
231 | private function consumeRfc850Date() { |
232 | $this->consumeDayNameLong(); |
233 | $this->consumeString( ', ' ); |
234 | $this->consumeDate2(); |
235 | $this->consumeString( ' ' ); |
236 | $this->consumeTimeOfDay(); |
237 | $this->consumeString( ' GMT' ); |
238 | } |
239 | |
240 | /** |
241 | * Execute the date2 rule. |
242 | * |
243 | * @throws HeaderParserError |
244 | */ |
245 | private function consumeDate2() { |
246 | $this->consumeDay(); |
247 | $this->consumeString( '-' ); |
248 | $this->consumeMonth(); |
249 | $this->consumeString( '-' ); |
250 | $year = $this->consumeFixedDigits( 2 ); |
251 | // RFC 2626 section 11.2 |
252 | $currentYear = (int)gmdate( 'Y' ); |
253 | $startOfCentury = (int)round( $currentYear, -2 ); |
254 | $this->year = $startOfCentury + intval( $year ); |
255 | $pivot = $currentYear + 50; |
256 | if ( $this->year > $pivot ) { |
257 | $this->year -= 100; |
258 | } |
259 | } |
260 | |
261 | /** |
262 | * Execute the day-name-l rule |
263 | * |
264 | * @throws HeaderParserError |
265 | */ |
266 | private function consumeDayNameLong() { |
267 | foreach ( self::DAY_NAMES_LONG as $dayName ) { |
268 | if ( substr_compare( $this->input, $dayName, $this->pos, strlen( $dayName ) ) === 0 ) { |
269 | $this->dayName = substr( $dayName, 0, 3 ); |
270 | $this->pos += strlen( $dayName ); |
271 | return; |
272 | } |
273 | } |
274 | $this->error( 'expected day-name-l' ); |
275 | } |
276 | |
277 | /** |
278 | * Execute the asctime-date rule |
279 | * |
280 | * @throws HeaderParserError |
281 | */ |
282 | private function consumeAsctimeDate() { |
283 | $this->consumeDayName(); |
284 | $this->consumeString( ' ' ); |
285 | $this->consumeDate3(); |
286 | $this->consumeString( ' ' ); |
287 | $this->consumeTimeOfDay(); |
288 | $this->consumeString( ' ' ); |
289 | $this->consumeYear(); |
290 | } |
291 | |
292 | /** |
293 | * Execute the date3 rule |
294 | * |
295 | * @throws HeaderParserError |
296 | */ |
297 | private function consumeDate3() { |
298 | $this->consumeMonth(); |
299 | $this->consumeString( ' ' ); |
300 | if ( ( $this->input[$this->pos] ?? '' ) === ' ' ) { |
301 | $this->pos++; |
302 | $this->day = '0' . $this->consumeFixedDigits( 1 ); |
303 | } else { |
304 | $this->day = $this->consumeFixedDigits( 2 ); |
305 | } |
306 | } |
307 | |
308 | /** |
309 | * Convert the captured results to a UNIX timestamp. |
310 | * This should only be called after parsing succeeds. |
311 | * |
312 | * @return int |
313 | */ |
314 | private function getUnixTime() { |
315 | return gmmktime( (int)$this->hour, (int)$this->minute, (int)$this->second, |
316 | $this->month, (int)$this->day, $this->year ); |
317 | } |
318 | } |