MediaWiki master
ConditionalHeaderUtil.php
Go to the documentation of this file.
1<?php
2
3namespace MediaWiki\Rest;
4
7use RuntimeException;
8use Wikimedia\Timestamp\ConvertibleTimestamp;
9use Wikimedia\Timestamp\TimestampFormat as TS;
10
12 private bool $varnishETagHack = true;
14 private $eTag;
16 private $lastModified;
18 private $hasRepresentation;
19
20 private IfNoneMatch $eTagParser;
21
22 private ?array $eTagParts = null;
23
24 public function __construct() {
25 $this->eTagParser = new IfNoneMatch;
26 }
27
44 public function setValidators(
45 $eTag,
46 $lastModified,
47 $hasRepresentation = null
48 ) {
49 $this->eTag = $eTag;
50 $this->lastModified = $lastModified;
51 $this->hasRepresentation = $hasRepresentation;
52 }
53
59 public function setVarnishETagHack( bool $hack ) {
60 $this->varnishETagHack = $hack;
61 }
62
63 private function getETag(): ?string {
64 if ( is_callable( $this->eTag ) ) {
65 // resolve callback
66 $this->eTag = ( $this->eTag )();
67 }
68
69 return $this->eTag;
70 }
71
72 private function getETagParts(): ?array {
73 if ( $this->eTagParts !== null ) {
74 return $this->eTagParts;
75 }
76
77 $eTag = $this->getETag();
78
79 if ( $eTag === null ) {
80 return null;
81 }
82
83 $this->eTagParts = $this->eTagParser->parseETag( $eTag );
84 if ( !$this->eTagParts ) {
85 throw new RuntimeException( 'Invalid ETag returned by handler: `' .
86 $this->eTagParser->getLastError() . '`' );
87 }
88
89 return $this->eTagParts;
90 }
91
92 private function getLastModified(): ?int {
93 if ( is_callable( $this->lastModified ) ) {
94 // resolve callback
95 $this->lastModified = ( $this->lastModified )();
96 }
97
98 if ( is_string( $this->lastModified ) ) {
99 // normalize to int
100 $this->lastModified = (int)ConvertibleTimestamp::convert(
101 TS::UNIX,
102 $this->lastModified
103 );
104 }
105
106 // should be int or null now.
107 return $this->lastModified;
108 }
109
110 private function hasRepresentation(): bool {
111 if ( is_callable( $this->hasRepresentation ) ) {
112 // resolve callback
113 $this->hasRepresentation = ( $this->hasRepresentation )();
114 }
115
116 if ( $this->hasRepresentation === null ) {
117 // apply fallback
118 $this->hasRepresentation = $this->getETag() !== null
119 || $this->getLastModified() !== null;
120 }
121
122 return $this->hasRepresentation;
123 }
124
133 $getOrHead = in_array( $request->getMethod(), [ 'GET', 'HEAD' ] );
134 if ( $request->hasHeader( 'If-Match' ) ) {
135 $im = $request->getHeader( 'If-Match' );
136 $match = false;
137 foreach ( $this->eTagParser->parseHeaderList( $im ) as $tag ) {
138 if ( ( $tag['whole'] === '*' && $this->hasRepresentation() ) ||
139 $this->strongCompare( $this->getETagParts(), $tag )
140 ) {
141 $match = true;
142 break;
143 }
144 }
145 if ( !$match ) {
146 return 412;
147 }
148 } elseif ( $request->hasHeader( 'If-Unmodified-Since' ) ) {
149 $requestDate = HttpDate::parse( $request->getHeader( 'If-Unmodified-Since' )[0] );
150 $lastModified = $this->getLastModified();
151 if ( $requestDate !== null
152 && ( $lastModified === null || $lastModified > $requestDate )
153 ) {
154 return 412;
155 }
156 }
157 if ( $request->hasHeader( 'If-None-Match' ) ) {
158 $inm = $request->getHeader( 'If-None-Match' );
159 foreach ( $this->eTagParser->parseHeaderList( $inm ) as $tag ) {
160 if ( ( $tag['whole'] === '*' && $this->hasRepresentation() ) ||
161 $this->weakCompare( $this->getETagParts(), $tag )
162 ) {
163 return $getOrHead ? 304 : 412;
164 }
165 }
166 } elseif ( $getOrHead && $request->hasHeader( 'If-Modified-Since' ) ) {
167 $requestDate = HttpDate::parse( $request->getHeader( 'If-Modified-Since' )[0] );
168 $lastModified = $this->getLastModified();
169 if ( $requestDate !== null && $lastModified !== null
170 && $lastModified <= $requestDate
171 ) {
172 return 304;
173 }
174 }
175 // RFC 7232 states that If-Range should be evaluated here. However, the
176 // purpose of If-Range is to cause the Range request header to be
177 // conditionally ignored, not to immediately send a response, so it
178 // doesn't fit here. RFC 7232 only requires that If-Range be checked
179 // after the other conditional header fields, a requirement that is
180 // satisfied if it is processed in Handler::execute().
181 return null;
182 }
183
191 public function applyResponseHeaders( ResponseInterface $response ) {
192 if ( $response->getStatusCode() >= 400
193 || $response->getStatusCode() === 301
194 || $response->getStatusCode() === 307
195 ) {
196 // Don't add Last-Modified and ETag for errors, including 412.
197 // Note that 304 responses are required to have these headers set.
198 // See IETF RFC 7232 section 4.
199 return;
200 }
201
202 $lastModified = $this->getLastModified();
203 if ( $lastModified !== null && !$response->hasHeader( 'Last-Modified' ) ) {
204 $response->setHeader( 'Last-Modified', HttpDate::format( $lastModified ) );
205 }
206
207 $eTag = $this->getETag();
208 if ( $eTag !== null && !$response->hasHeader( 'ETag' ) ) {
209 $response->setHeader( 'ETag', $eTag );
210 }
211 }
212
220 private function weakCompare( $resourceETag, $headerETag ) {
221 if ( $resourceETag === null || $headerETag === null ) {
222 return false;
223 }
224 return $resourceETag['contents'] === $headerETag['contents'];
225 }
226
240 private function strongCompare( $resourceETag, $headerETag ) {
241 if ( $resourceETag === null || $headerETag === null ) {
242 return false;
243 }
244
245 return !$resourceETag['weak']
246 && ( $this->varnishETagHack || !$headerETag['weak'] )
247 && $resourceETag['contents'] === $headerETag['contents'];
248 }
249
250}
if(!defined('MW_SETUP_CALLBACK'))
Definition WebStart.php:71
checkPreconditions(RequestInterface $request)
Check conditional request headers in the order required by RFC 7232 section 6.
setValidators( $eTag, $lastModified, $hasRepresentation=null)
Initialize the object with information about the requested resource.
setVarnishETagHack(bool $hack)
If the Varnish ETag hack is disabled by calling this method, strong ETag comparison will follow RFC 7...
applyResponseHeaders(ResponseInterface $response)
Set Last-Modified and ETag headers in the response according to the cached values set by setValidator...
This is a parser for "HTTP-date" as defined by RFC 7231.
Definition HttpDate.php:23
A class to assist with the parsing of If-None-Match, If-Match and ETag headers.
A request interface similar to PSR-7's ServerRequestInterface.
An interface similar to PSR-7's ResponseInterface, the primary difference being that it is mutable.
setHeader( $name, $value)
Set or replace the specified header.
getStatusCode()
Gets the response status code.
hasHeader(string $name)
Checks if a header exists by the given case-insensitive name.