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