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
13 private $varnishETagHack = true;
15 private $eTag;
17 private $lastModified;
19 private $hasRepresentation;
20
21 private IfNoneMatch $eTagParser;
22
23 private ?array $eTagParts = null;
24
25 public function __construct() {
26 $this->eTagParser = new IfNoneMatch;
27 }
28
45 public function setValidators(
46 $eTag,
47 $lastModified,
48 $hasRepresentation = null
49 ) {
50 $this->eTag = $eTag;
51 $this->lastModified = $lastModified;
52 $this->hasRepresentation = $hasRepresentation;
53 }
54
62 public function setVarnishETagHack( $hack ) {
63 $this->varnishETagHack = $hack;
64 }
65
66 private function getETag(): ?string {
67 if ( is_callable( $this->eTag ) ) {
68 // resolve callback
69 $this->eTag = ( $this->eTag )();
70 }
71
72 return $this->eTag;
73 }
74
75 private function getETagParts(): ?array {
76 if ( $this->eTagParts !== null ) {
77 return $this->eTagParts;
78 }
79
80 $eTag = $this->getETag();
81
82 if ( $eTag === null ) {
83 return null;
84 }
85
86 $this->eTagParts = $this->eTagParser->parseETag( $eTag );
87 if ( !$this->eTagParts ) {
88 throw new RuntimeException( 'Invalid ETag returned by handler: `' .
89 $this->eTagParser->getLastError() . '`' );
90 }
91
92 return $this->eTagParts;
93 }
94
95 private function getLastModified(): ?int {
96 if ( is_callable( $this->lastModified ) ) {
97 // resolve callback
98 $this->lastModified = ( $this->lastModified )();
99 }
100
101 if ( is_string( $this->lastModified ) ) {
102 // normalize to int
103 $this->lastModified = (int)ConvertibleTimestamp::convert(
104 TS::UNIX,
105 $this->lastModified
106 );
107 }
108
109 // should be int or null now.
110 return $this->lastModified;
111 }
112
113 private function hasRepresentation(): bool {
114 if ( is_callable( $this->hasRepresentation ) ) {
115 // resolve callback
116 $this->hasRepresentation = ( $this->hasRepresentation )();
117 }
118
119 if ( $this->hasRepresentation === null ) {
120 // apply fallback
121 $this->hasRepresentation = $this->getETag() !== null
122 || $this->getLastModified() !== null;
123 }
124
125 return $this->hasRepresentation;
126 }
127
135 public function checkPreconditions( RequestInterface $request ) {
136 $getOrHead = in_array( $request->getMethod(), [ 'GET', 'HEAD' ] );
137 if ( $request->hasHeader( 'If-Match' ) ) {
138 $im = $request->getHeader( 'If-Match' );
139 $match = false;
140 foreach ( $this->eTagParser->parseHeaderList( $im ) as $tag ) {
141 if ( ( $tag['whole'] === '*' && $this->hasRepresentation() ) ||
142 $this->strongCompare( $this->getETagParts(), $tag )
143 ) {
144 $match = true;
145 break;
146 }
147 }
148 if ( !$match ) {
149 return 412;
150 }
151 } elseif ( $request->hasHeader( 'If-Unmodified-Since' ) ) {
152 $requestDate = HttpDate::parse( $request->getHeader( 'If-Unmodified-Since' )[0] );
153 $lastModified = $this->getLastModified();
154 if ( $requestDate !== null
155 && ( $lastModified === null || $lastModified > $requestDate )
156 ) {
157 return 412;
158 }
159 }
160 if ( $request->hasHeader( 'If-None-Match' ) ) {
161 $inm = $request->getHeader( 'If-None-Match' );
162 foreach ( $this->eTagParser->parseHeaderList( $inm ) as $tag ) {
163 if ( ( $tag['whole'] === '*' && $this->hasRepresentation() ) ||
164 $this->weakCompare( $this->getETagParts(), $tag )
165 ) {
166 return $getOrHead ? 304 : 412;
167 }
168 }
169 } elseif ( $getOrHead && $request->hasHeader( 'If-Modified-Since' ) ) {
170 $requestDate = HttpDate::parse( $request->getHeader( 'If-Modified-Since' )[0] );
171 $lastModified = $this->getLastModified();
172 if ( $requestDate !== null && $lastModified !== null
173 && $lastModified <= $requestDate
174 ) {
175 return 304;
176 }
177 }
178 // RFC 7232 states that If-Range should be evaluated here. However, the
179 // purpose of If-Range is to cause the Range request header to be
180 // conditionally ignored, not to immediately send a response, so it
181 // doesn't fit here. RFC 7232 only requires that If-Range be checked
182 // after the other conditional header fields, a requirement that is
183 // satisfied if it is processed in Handler::execute().
184 return null;
185 }
186
194 public function applyResponseHeaders( ResponseInterface $response ) {
195 if ( $response->getStatusCode() >= 400
196 || $response->getStatusCode() === 301
197 || $response->getStatusCode() === 307
198 ) {
199 // Don't add Last-Modified and ETag for errors, including 412.
200 // Note that 304 responses are required to have these headers set.
201 // See IETF RFC 7232 section 4.
202 return;
203 }
204
205 $lastModified = $this->getLastModified();
206 if ( $lastModified !== null && !$response->hasHeader( 'Last-Modified' ) ) {
207 $response->setHeader( 'Last-Modified', HttpDate::format( $lastModified ) );
208 }
209
210 $eTag = $this->getETag();
211 if ( $eTag !== null && !$response->hasHeader( 'ETag' ) ) {
212 $response->setHeader( 'ETag', $eTag );
213 }
214 }
215
223 private function weakCompare( $resourceETag, $headerETag ) {
224 if ( $resourceETag === null || $headerETag === null ) {
225 return false;
226 }
227 return $resourceETag['contents'] === $headerETag['contents'];
228 }
229
243 private function strongCompare( $resourceETag, $headerETag ) {
244 if ( $resourceETag === null || $headerETag === null ) {
245 return false;
246 }
247
248 return !$resourceETag['weak']
249 && ( $this->varnishETagHack || !$headerETag['weak'] )
250 && $resourceETag['contents'] === $headerETag['contents'];
251 }
252
253}
if(!defined('MW_SETUP_CALLBACK'))
Definition WebStart.php:68
setVarnishETagHack( $hack)
If the Varnish ETag hack is disabled by calling this method, strong ETag comparison will follow RFC 7...
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.
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.
getMethod()
Retrieves the HTTP method of the request.
hasHeader( $name)
Checks if a header exists by the given case-insensitive name.
getHeader( $name)
Retrieves a message header value by the given case-insensitive name.
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.
hasHeader( $name)
Checks if a header exists by the given case-insensitive name.
getStatusCode()
Gets the response status code.