10
10
class ConditionalHeaderUtil {
11
11
/** @var bool */
12
12
private $ varnishETagHack = true ;
13
- /** @var string|null */
13
+ /** @var callback| string|null */
14
14
private $ eTag ;
15
- /** @var int|null */
15
+ /** @var callback|string| int|null */
16
16
private $ lastModified ;
17
- /** @var bool */
17
+ /** @var callback| bool */
18
18
private $ hasRepresentation ;
19
19
20
+ private IfNoneMatch $ eTagParser ;
21
+
22
+ private ?array $ eTagParts = null ;
23
+
24
+ public function __construct () {
25
+ $ this ->eTagParser = new IfNoneMatch ;
26
+ }
27
+
20
28
/**
21
29
* Initialize the object with information about the requested resource.
22
30
*
23
- * @param string|null $eTag The entity-tag (including quotes), or null if
24
- * it is unknown.
25
- * @param string|int|null $lastModified The Last-Modified date in a format
31
+ * @param callable| string|null $eTag The entity-tag (including quotes), or null if
32
+ * it is unknown. Can also be provided as a callback for later evaluation.
33
+ * @param callable| string|int|null $lastModified The Last-Modified date in a format
26
34
* accepted by ConvertibleTimestamp, or null if it is unknown.
27
- * @param bool|null $hasRepresentation Whether the server has a current
35
+ * Can also be provided as a callback for later evaluation.
36
+ * @param callable|bool|null $hasRepresentation Whether the server can serve a
28
37
* representation of the target resource. This should be true if the
29
38
* resource exists, and false if it does not exist. It is used for
30
39
* wildcard validators -- the intended use case is to abort a PUT if the
31
40
* resource does (or does not) exist. If null is passed, we assume that
32
- * the resource exists if an ETag was specified for it.
41
+ * the resource exists if an ETag or last-modified data was specified for it.
42
+ * Can also be provided as a callback for later evaluation.
33
43
*/
34
- public function setValidators ( $ eTag , $ lastModified , $ hasRepresentation ) {
44
+ public function setValidators (
45
+ $ eTag ,
46
+ $ lastModified ,
47
+ $ hasRepresentation = null
48
+ ) {
35
49
$ this ->eTag = $ eTag ;
36
- if ( $ lastModified === null ) {
37
- $ this ->lastModified = null ;
38
- } else {
39
- $ this ->lastModified = (int )ConvertibleTimestamp::convert ( TS_UNIX , $ lastModified );
40
- }
41
- $ this ->hasRepresentation = $ hasRepresentation ?? ( $ eTag !== null );
50
+ $ this ->lastModified = $ lastModified ;
51
+ $ this ->hasRepresentation = $ hasRepresentation ;
42
52
}
43
53
44
54
/**
@@ -52,6 +62,68 @@ public function setVarnishETagHack( $hack ) {
52
62
$ this ->varnishETagHack = $ hack ;
53
63
}
54
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
+
55
127
/**
56
128
* Check conditional request headers in the order required by RFC 7232 section 6.
57
129
*
@@ -60,23 +132,13 @@ public function setVarnishETagHack( $hack ) {
60
132
* continue processing the request.
61
133
*/
62
134
public function checkPreconditions ( RequestInterface $ request ) {
63
- $ parser = new IfNoneMatch ;
64
- if ( $ this ->eTag !== null ) {
65
- $ resourceTag = $ parser ->parseETag ( $ this ->eTag );
66
- if ( !$ resourceTag ) {
67
- throw new RuntimeException ( 'Invalid ETag returned by handler: ` ' .
68
- $ parser ->getLastError () . '` ' );
69
- }
70
- } else {
71
- $ resourceTag = null ;
72
- }
73
135
$ getOrHead = in_array ( $ request ->getMethod (), [ 'GET ' , 'HEAD ' ] );
74
136
if ( $ request ->hasHeader ( 'If-Match ' ) ) {
75
137
$ im = $ request ->getHeader ( 'If-Match ' );
76
138
$ match = false ;
77
- foreach ( $ parser ->parseHeaderList ( $ im ) as $ tag ) {
78
- if ( ( $ tag ['whole ' ] === '* ' && $ this ->hasRepresentation ) ||
79
- $ this ->strongCompare ( $ resourceTag , $ tag )
139
+ foreach ( $ this -> eTagParser ->parseHeaderList ( $ im ) as $ tag ) {
140
+ if ( ( $ tag ['whole ' ] === '* ' && $ this ->hasRepresentation () ) ||
141
+ $ this ->strongCompare ( $ this -> getETagParts () , $ tag )
80
142
) {
81
143
$ match = true ;
82
144
break ;
@@ -87,25 +149,27 @@ public function checkPreconditions( RequestInterface $request ) {
87
149
}
88
150
} elseif ( $ request ->hasHeader ( 'If-Unmodified-Since ' ) ) {
89
151
$ requestDate = HttpDate::parse ( $ request ->getHeader ( 'If-Unmodified-Since ' )[0 ] );
152
+ $ lastModified = $ this ->getLastModified ();
90
153
if ( $ requestDate !== null
91
- && ( $ this -> lastModified === null || $ this -> lastModified > $ requestDate )
154
+ && ( $ lastModified === null || $ lastModified > $ requestDate )
92
155
) {
93
156
return 412 ;
94
157
}
95
158
}
96
159
if ( $ request ->hasHeader ( 'If-None-Match ' ) ) {
97
160
$ inm = $ request ->getHeader ( 'If-None-Match ' );
98
- foreach ( $ parser ->parseHeaderList ( $ inm ) as $ tag ) {
99
- if ( ( $ tag ['whole ' ] === '* ' && $ this ->hasRepresentation ) ||
100
- $ this ->weakCompare ( $ resourceTag , $ tag )
161
+ foreach ( $ this -> eTagParser ->parseHeaderList ( $ inm ) as $ tag ) {
162
+ if ( ( $ tag ['whole ' ] === '* ' && $ this ->hasRepresentation () ) ||
163
+ $ this ->weakCompare ( $ this -> getETagParts () , $ tag )
101
164
) {
102
165
return $ getOrHead ? 304 : 412 ;
103
166
}
104
167
}
105
168
} elseif ( $ getOrHead && $ request ->hasHeader ( 'If-Modified-Since ' ) ) {
106
169
$ requestDate = HttpDate::parse ( $ request ->getHeader ( 'If-Modified-Since ' )[0 ] );
107
- if ( $ requestDate !== null && $ this ->lastModified !== null
108
- && $ this ->lastModified <= $ requestDate
170
+ $ lastModified = $ this ->getLastModified ();
171
+ if ( $ requestDate !== null && $ lastModified !== null
172
+ && $ lastModified <= $ requestDate
109
173
) {
110
174
return 304 ;
111
175
}
@@ -127,11 +191,21 @@ public function checkPreconditions( RequestInterface $request ) {
127
191
* take precedence.
128
192
*/
129
193
public function applyResponseHeaders ( ResponseInterface $ response ) {
130
- if ( $ this ->lastModified !== null && !$ response ->hasHeader ( 'Last-Modified ' ) ) {
131
- $ response ->setHeader ( 'Last-Modified ' , HttpDate::format ( $ this ->lastModified ) );
194
+ if ( $ response ->getStatusCode () >= 400 ) {
195
+ // Don't add Last-Modified and ETag for errors, including 412.
196
+ // Note that 304 responses are required to have these headers set.
197
+ // See IETF RFC 7232 section 4.
198
+ return ;
199
+ }
200
+
201
+ $ lastModified = $ this ->getLastModified ();
202
+ if ( $ lastModified !== null && !$ response ->hasHeader ( 'Last-Modified ' ) ) {
203
+ $ response ->setHeader ( 'Last-Modified ' , HttpDate::format ( $ lastModified ) );
132
204
}
133
- if ( $ this ->eTag !== null && !$ response ->hasHeader ( 'ETag ' ) ) {
134
- $ response ->setHeader ( 'ETag ' , $ this ->eTag );
205
+
206
+ $ eTag = $ this ->getETag ();
207
+ if ( $ eTag !== null && !$ response ->hasHeader ( 'ETag ' ) ) {
208
+ $ response ->setHeader ( 'ETag ' , $ eTag );
135
209
}
136
210
}
137
211
0 commit comments