* Added ConditionalHeaderUtil, a conditional request helper class meant for composition into Handler. I evaluated the composer package micheh/psr7-cache for this role but I decided that I prefer DIY code rather than some rather ugly glue. * Check conditional request headers prior to entry into Handler::execute(). Contrary to what was previously documented, use the results of getLastModified() and getETag() to set headers in the response. This is convenient and can be overridden in the Handler if desired by overriding a one-line function. * Instead of locking up header parsing inside ConditionalHeaderUtil as was done in micheh/psr7-cache, make a start on a new reusable header parsing framework, with recursive descent parsers for HTTP-date and IfNoneMatch. Change-Id: I260809081cad7701df8620ab03834158670d4230
311 lines
6.7 KiB
PHP
311 lines
6.7 KiB
PHP
<?php
|
|
|
|
namespace MediaWiki\Rest\HeaderParser;
|
|
|
|
/**
|
|
* This is a parser for "HTTP-date" as defined by RFC 7231.
|
|
*
|
|
* Normally in MediaWiki, dates in HTTP headers are converted using
|
|
* ConvertibleTimestamp or strtotime(), and this is encouraged by RFC 7231:
|
|
*
|
|
* "Recipients of timestamp values are encouraged to be robust in parsing
|
|
* timestamps unless otherwise restricted by the field definition."
|
|
*
|
|
* In the case of If-Modified-Since, we are in fact otherwise restricted, since
|
|
* RFC 7232 says:
|
|
*
|
|
* "A recipient MUST ignore the If-Modified-Since header field if the
|
|
* received field-value is not a valid HTTP-date"
|
|
*
|
|
* So it is not correct to use strtotime() or ConvertibleTimestamp to parse
|
|
* If-Modified-Since.
|
|
*/
|
|
class HttpDate extends HeaderParserBase {
|
|
private static $dayNames = [
|
|
'Mon' => true,
|
|
'Tue' => true,
|
|
'Wed' => true,
|
|
'Thu' => true,
|
|
'Fri' => true,
|
|
'Sat' => true,
|
|
'Sun' => true
|
|
];
|
|
|
|
private static $monthsByName = [
|
|
'Jan' => 1,
|
|
'Feb' => 2,
|
|
'Mar' => 3,
|
|
'Apr' => 4,
|
|
'May' => 5,
|
|
'Jun' => 6,
|
|
'Jul' => 7,
|
|
'Aug' => 8,
|
|
'Sep' => 9,
|
|
'Oct' => 10,
|
|
'Nov' => 11,
|
|
'Dec' => 12,
|
|
];
|
|
|
|
private static $dayNamesLong = [
|
|
'Monday',
|
|
'Tuesday',
|
|
'Wednesday',
|
|
'Thursday',
|
|
'Friday',
|
|
'Saturday',
|
|
'Sunday',
|
|
];
|
|
|
|
private $dayName;
|
|
private $day;
|
|
private $month;
|
|
private $year;
|
|
private $hour;
|
|
private $minute;
|
|
private $second;
|
|
|
|
/**
|
|
* Parse an HTTP-date string
|
|
*
|
|
* @param string $dateString
|
|
* @return int|null The UNIX timestamp, or null if the date was invalid
|
|
*/
|
|
public static function parse( $dateString ) {
|
|
$parser = new self( $dateString );
|
|
if ( $parser->execute() ) {
|
|
return $parser->getUnixTime();
|
|
} else {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* A convenience function to convert a UNIX timestamp to the preferred
|
|
* IMF-fixdate format for HTTP header output.
|
|
*
|
|
* @param int $unixTime
|
|
* @return false|string
|
|
*/
|
|
public static function format( $unixTime ) {
|
|
return gmdate( 'D, d M Y H:i:s \G\M\T', $unixTime );
|
|
}
|
|
|
|
/**
|
|
* Private constructor. Use the public static functions for public access.
|
|
*
|
|
* @param string $input
|
|
*/
|
|
private function __construct( $input ) {
|
|
$this->setInput( $input );
|
|
}
|
|
|
|
/**
|
|
* Parse the input string
|
|
*
|
|
* @return bool True for success
|
|
*/
|
|
private function execute() {
|
|
$this->pos = 0;
|
|
try {
|
|
$this->consumeFixdate();
|
|
$this->assertEnd();
|
|
return true;
|
|
} catch ( HeaderParserError $e ) {
|
|
}
|
|
$this->pos = 0;
|
|
try {
|
|
$this->consumeRfc850Date();
|
|
$this->assertEnd();
|
|
return true;
|
|
} catch ( HeaderParserError $e ) {
|
|
}
|
|
$this->pos = 0;
|
|
try {
|
|
$this->consumeAsctimeDate();
|
|
$this->assertEnd();
|
|
return true;
|
|
} catch ( HeaderParserError $e ) {
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Execute the IMF-fixdate rule, or throw an exception
|
|
*
|
|
* @throws HeaderParserError
|
|
*/
|
|
private function consumeFixdate() {
|
|
$this->consumeDayName();
|
|
$this->consumeString( ', ' );
|
|
$this->consumeDate1();
|
|
$this->consumeString( ' ' );
|
|
$this->consumeTimeOfDay();
|
|
$this->consumeString( ' GMT' );
|
|
}
|
|
|
|
/**
|
|
* Execute the day-name rule, and capture the result.
|
|
*
|
|
* @throws HeaderParserError
|
|
*/
|
|
private function consumeDayName() {
|
|
$next3 = substr( $this->input, $this->pos, 3 );
|
|
if ( isset( self::$dayNames[$next3] ) ) {
|
|
$this->dayName = $next3;
|
|
$this->pos += 3;
|
|
} else {
|
|
$this->error( 'expected day-name' );
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Execute the date1 rule
|
|
*
|
|
* @throws HeaderParserError
|
|
*/
|
|
private function consumeDate1() {
|
|
$this->consumeDay();
|
|
$this->consumeString( ' ' );
|
|
$this->consumeMonth();
|
|
$this->consumeString( ' ' );
|
|
$this->consumeYear();
|
|
}
|
|
|
|
/**
|
|
* Execute the day rule, and capture the result.
|
|
*
|
|
* @throws HeaderParserError
|
|
*/
|
|
private function consumeDay() {
|
|
$this->day = $this->consumeFixedDigits( 2 );
|
|
}
|
|
|
|
/**
|
|
* Execute the month rule, and capture the result
|
|
*
|
|
* @throws HeaderParserError
|
|
*/
|
|
private function consumeMonth() {
|
|
$next3 = substr( $this->input, $this->pos, 3 );
|
|
if ( isset( self::$monthsByName[$next3] ) ) {
|
|
$this->month = self::$monthsByName[$next3];
|
|
$this->pos += 3;
|
|
} else {
|
|
$this->error( 'expected month' );
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Execute the year rule, and capture the result
|
|
*
|
|
* @throws HeaderParserError
|
|
*/
|
|
private function consumeYear() {
|
|
$this->year = $this->consumeFixedDigits( 4 );
|
|
}
|
|
|
|
/**
|
|
* Execute the time-of-day rule
|
|
* @throws HeaderParserError
|
|
*/
|
|
private function consumeTimeOfDay() {
|
|
$this->hour = $this->consumeFixedDigits( 2 );
|
|
$this->consumeString( ':' );
|
|
$this->minute = $this->consumeFixedDigits( 2 );
|
|
$this->consumeString( ':' );
|
|
$this->second = $this->consumeFixedDigits( 2 );
|
|
}
|
|
|
|
/**
|
|
* Execute the rfc850-date rule
|
|
*
|
|
* @throws HeaderParserError
|
|
*/
|
|
private function consumeRfc850Date() {
|
|
$this->consumeDayNameLong();
|
|
$this->consumeString( ', ' );
|
|
$this->consumeDate2();
|
|
$this->consumeString( ' ' );
|
|
$this->consumeTimeOfDay();
|
|
$this->consumeString( ' GMT' );
|
|
}
|
|
|
|
/**
|
|
* Execute the date2 rule.
|
|
*
|
|
* @throws HeaderParserError
|
|
*/
|
|
private function consumeDate2() {
|
|
$this->consumeDay();
|
|
$this->consumeString( '-' );
|
|
$this->consumeMonth();
|
|
$this->consumeString( '-' );
|
|
$year = $this->consumeFixedDigits( 2 );
|
|
// RFC 2626 section 11.2
|
|
$currentYear = gmdate( 'Y' );
|
|
$startOfCentury = round( $currentYear, -2 );
|
|
$this->year = $startOfCentury + intval( $year );
|
|
$pivot = $currentYear + 50;
|
|
if ( $this->year > $pivot ) {
|
|
$this->year -= 100;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Execute the day-name-l rule
|
|
*
|
|
* @throws HeaderParserError
|
|
*/
|
|
private function consumeDayNameLong() {
|
|
foreach ( self::$dayNamesLong as $dayName ) {
|
|
if ( substr_compare( $this->input, $dayName, $this->pos, strlen( $dayName ) ) === 0 ) {
|
|
$this->dayName = substr( $dayName, 0, 3 );
|
|
$this->pos += strlen( $dayName );
|
|
return;
|
|
}
|
|
}
|
|
$this->error( 'expected day-name-l' );
|
|
}
|
|
|
|
/**
|
|
* Execute the asctime-date rule
|
|
*
|
|
* @throws HeaderParserError
|
|
*/
|
|
private function consumeAsctimeDate() {
|
|
$this->consumeDayName();
|
|
$this->consumeString( ' ' );
|
|
$this->consumeDate3();
|
|
$this->consumeString( ' ' );
|
|
$this->consumeTimeOfDay();
|
|
$this->consumeString( ' ' );
|
|
$this->consumeYear();
|
|
}
|
|
|
|
/**
|
|
* Execute the date3 rule
|
|
*
|
|
* @throws HeaderParserError
|
|
*/
|
|
private function consumeDate3() {
|
|
$this->consumeMonth();
|
|
$this->consumeString( ' ' );
|
|
if ( ( $this->input[$this->pos] ?? '' ) === ' ' ) {
|
|
$this->pos++;
|
|
$this->day = '0' . $this->consumeFixedDigits( 1 );
|
|
} else {
|
|
$this->day = $this->consumeFixedDigits( 2 );
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Convert the captured results to a UNIX timestamp.
|
|
* This should only be called after parsing succeeds.
|
|
*
|
|
* @return int
|
|
*/
|
|
private function getUnixTime() {
|
|
return gmmktime( $this->hour, $this->minute, $this->second,
|
|
$this->month, $this->day, $this->year );
|
|
}
|
|
}
|