Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
97.73% covered (success)
97.73%
86 / 88
88.24% covered (warning)
88.24%
15 / 17
CRAP
0.00% covered (danger)
0.00%
0 / 1
HttpDate
97.73% covered (success)
97.73%
86 / 88
88.24% covered (warning)
88.24%
15 / 17
27
0.00% covered (danger)
0.00%
0 / 1
 parse
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 format
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 execute
100.00% covered (success)
100.00%
16 / 16
100.00% covered (success)
100.00%
1 / 1
4
 consumeFixdate
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
1
 consumeDayName
80.00% covered (warning)
80.00%
4 / 5
0.00% covered (danger)
0.00%
0 / 1
2.03
 consumeDate1
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
1
 consumeDay
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 consumeMonth
80.00% covered (warning)
80.00%
4 / 5
0.00% covered (danger)
0.00%
0 / 1
2.03
 consumeYear
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 consumeTimeOfDay
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
1
 consumeRfc850Date
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
1
 consumeDate2
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
2
 consumeDayNameLong
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
3
 consumeAsctimeDate
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
1
 consumeDate3
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
2
 getUnixTime
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2
3namespace 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 */
23class HttpDate extends HeaderParserBase {
24    private static $dayNames = [
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 static $monthsByName = [
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 static $dayNamesLong = [
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::$dayNames[$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::$monthsByName[$next3] ) ) {
198            $this->month = self::$monthsByName[$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::$dayNamesLong 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}