-
Notifications
You must be signed in to change notification settings - Fork 383
/
Copy pathCachedRemoteGetRequest.php
194 lines (169 loc) · 6.42 KB
/
CachedRemoteGetRequest.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
<?php
/**
* Class CachedRemoteGetRequest.
*
* @package AmpProject\AmpWP
*/
namespace AmpProject\AmpWP\RemoteRequest;
use AmpProject\Exception\FailedToGetCachedResponse;
use AmpProject\Exception\FailedToGetFromRemoteUrl;
use AmpProject\RemoteGetRequest;
use AmpProject\RemoteRequest\RemoteGetRequestResponse;
use AmpProject\Response;
use DateTimeImmutable;
use DateTimeInterface;
/**
* Caching decorator for RemoteGetRequest implementations.
*
* Caching uses WordPress transients.
*
* @package AmpProject\AmpWP
* @since 2.0
* @internal
*/
final class CachedRemoteGetRequest implements RemoteGetRequest {
/**
* Prefix to use to identify transients.
*
* @var string
*/
const TRANSIENT_PREFIX = 'amp_remote_request_';
/**
* Cache control header directive name.
*
* @var string
*/
const CACHE_CONTROL = 'Cache-Control';
/**
* Remote request object to decorate with caching.
*
* @var RemoteGetRequest
*/
private $remote_request;
/**
* Cache expiration time in seconds.
*
* This will be used by default for successful requests when the 'cache-control: max-age' was not provided.
*
* @var int
*/
private $expiry;
/**
* Minimum cache expiration time in seconds.
*
* This will be used for failed requests, or for successful requests when the 'cache-control: max-age' is inferior.
* Caching will never expire quicker than this minimum.
*
* @var int
*/
private $min_expiry;
/**
* Whether to use Cache-Control headers to decide on expiry times if available.
*
* @var bool
*/
private $use_cache_control;
/**
* Instantiate a CachedRemoteGetRequest object.
*
* This is a decorator that can wrap around an existing remote request object to add a caching layer.
*
* @param RemoteGetRequest $remote_request Remote request object to decorate with caching.
* @param int|float $expiry Optional. Default cache expiry in seconds. Defaults to 30 days.
* @param int|float $min_expiry Optional. Default enforced minimum cache expiry in seconds. Defaults
* to 24 hours.
* @param bool $use_cache_control Optional. Use Cache-Control headers for expiry if available. Defaults
* to true.
*/
public function __construct(
RemoteGetRequest $remote_request,
$expiry = MONTH_IN_SECONDS,
$min_expiry = DAY_IN_SECONDS,
$use_cache_control = true
) {
$this->remote_request = $remote_request;
$this->expiry = (int) $expiry;
$this->min_expiry = (int) $min_expiry;
$this->use_cache_control = (bool) $use_cache_control;
}
/**
* Do a GET request to retrieve the contents of a remote URL.
*
* @todo Should this also respect additional Cache-Control directives like 'no-cache'?
*
* @param string $url URL to get.
* @param array $headers Optional. Associative array of headers to send with the request. Defaults to empty array.
* @return Response Response for the executed request.
* @throws FailedToGetCachedResponse If retrieving the contents from the URL failed.
*/
public function get( $url, $headers = [] ) {
$cache_key = self::TRANSIENT_PREFIX . md5( __CLASS__ . $url );
$cached_response = get_transient( $cache_key );
$headers = [];
if ( is_string( $cached_response ) ) {
$cached_response = unserialize( $cached_response, [ CachedResponse::class, DateTimeImmutable::class ] ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.serialize_unserialize,PHPCompatibility.FunctionUse.NewFunctionParameters.unserialize_optionsFound
}
if ( ! $cached_response instanceof CachedResponse || $cached_response->is_expired() ) {
try {
$response = $this->remote_request->get( $url, $headers );
$status = $response->getStatusCode();
/** @var DateTimeImmutable $expiry */
$expiry = $this->get_expiry_time( $response );
$headers = $response->getHeaders();
$body = $response->getBody();
} catch ( FailedToGetFromRemoteUrl $exception ) {
$status = $exception->getStatusCode();
$expiry = new DateTimeImmutable( "+ {$this->min_expiry} seconds" );
$body = $exception->getMessage();
}
$cached_response = new CachedResponse( $body, $headers, $status, $expiry );
// Transient extend beyond cache expiry to allow for serving stale content.
// @TODO: We don't serve stale content atm, but rather synchronously refresh.
// See https://github.com/ampproject/amp-wp/issues/5477.
$transient_expiry = $expiry->modify( "+ {$this->expiry} seconds" );
set_transient( $cache_key, serialize( $cached_response ), $transient_expiry->getTimestamp() ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.serialize_serialize
}
if ( ! $cached_response->is_valid() ) {
throw new FailedToGetCachedResponse( $url );
}
return new RemoteGetRequestResponse( $cached_response->get_body(), $cached_response->get_headers(), $cached_response->get_status_code() );
}
/**
* Get the expiry time of the data to cache.
*
* This will use the cache-control header information in the provided response or fall back to the provided default
* expiry.
*
* @param Response $response Response object to get the expiry from.
* @return DateTimeInterface Expiry of the data.
*/
private function get_expiry_time( Response $response ) {
if ( $this->use_cache_control && $response->hasHeader( self::CACHE_CONTROL ) ) {
$expiry = max( $this->min_expiry, $this->get_max_age( $response->getHeader( self::CACHE_CONTROL ) ) );
return new DateTimeImmutable( "+ {$expiry} seconds" );
}
return new DateTimeImmutable( "+ {$this->expiry} seconds" );
}
/**
* Get the max age setting from one or more cache-control header strings.
*
* @param array|string $cache_control_strings One or more cache control header strings.
* @return int Value of the max-age cache directive. 0 if not found.
*/
private function get_max_age( $cache_control_strings ) {
$max_age = 0;
foreach ( (array) $cache_control_strings as $cache_control_string ) {
$cache_control_parts = array_map( 'trim', explode( ',', $cache_control_string ) );
foreach ( $cache_control_parts as $cache_control_part ) {
$cache_control_setting_parts = array_map( 'trim', explode( '=', $cache_control_part ) );
if ( count( $cache_control_setting_parts ) !== 2 ) {
continue;
}
if ( 'max-age' === $cache_control_setting_parts[0] ) {
$max_age = absint( $cache_control_setting_parts[1] );
}
}
}
return $max_age;
}
}