PluginProbe ʕ •ᴥ•ʔ
NitroPack – Performance, Page Speed & Cache Plugin for Core Web Vitals, CDN & Image Optimization / 1.7.0
NitroPack – Performance, Page Speed & Cache Plugin for Core Web Vitals, CDN & Image Optimization v1.7.0
1.19.8 1.19.7 1.19.6 1.19.5 trunk 1.10.0 1.10.1 1.10.2 1.10.3 1.10.4 1.11.0 1.12.0 1.13.0 1.14.0 1.15.0 1.15.1 1.15.2 1.15.3 1.16.0 1.16.1 1.16.2 1.16.3 1.16.4 1.16.5 1.16.6 1.16.7 1.16.8 1.17.0 1.17.6 1.17.7 1.17.8 1.17.9 1.18.0 1.18.1 1.18.2 1.18.3 1.18.4 1.18.5 1.18.6 1.18.7 1.18.8 1.18.9 1.19.0 1.19.1 1.19.2 1.19.3 1.19.4 1.3.19 1.3.20 1.4.0 1.4.1 1.5.0 1.5.1 1.5.10 1.5.11 1.5.12 1.5.13 1.5.14 1.5.15 1.5.16 1.5.17 1.5.18 1.5.19 1.5.2 1.5.3 1.5.4 1.5.5 1.5.6 1.5.7 1.5.8 1.5.9 1.6.0 1.6.1 1.7.0 1.7.1 1.8.0 1.8.1 1.8.3 1.9.0 1.9.1 1.9.2
nitropack / nitropack-sdk / NitroPack / SDK / Pagecache.php
nitropack / nitropack-sdk / NitroPack / SDK Last commit date
Api 3 years ago Integrations 3 years ago StorageDriver 4 years ago Url 3 years ago data 6 years ago Api.php 3 years ago Backlog.php 4 years ago BacklogReplayTimeoutException.php 4 years ago ChallengeProcessingException.php 4 years ago ChallengeVerificationException.php 4 years ago ConfigFetcherException.php 4 years ago Crypto.php 3 years ago Device.php 6 years ago DeviceType.php 6 years ago ElementRevision.php 3 years ago EmptyConfigException.php 6 years ago FileHandle.php 5 years ago Filesystem.php 4 years ago HealthStatus.php 5 years ago IntegrationUrl.php 6 years ago NitroPack.php 3 years ago NoConfigException.php 5 years ago Pagecache.php 3 years ago PurgeType.php 3 years ago ServiceDownException.php 5 years ago StorageException.php 6 years ago VariationCookieException.php 5 years ago WebhookException.php 5 years ago Website.php 6 years ago
Pagecache.php
403 lines
1 <?php
2
3 namespace NitroPack\SDK;
4
5 use \NitroPack\Url\Url;
6
7 class Pagecache
8 {
9 protected $url;
10 protected $cookies;
11 protected $supportedCookies;
12 protected $dataDir;
13 protected $isAjax;
14 protected $referer;
15 protected $parent;
16 protected $Device;
17 protected $useCompression;
18 protected $useInvalidated;
19 private $urlPathVersion;
20 private $cookiesProvider;
21
22 public static function getUrlDir($dataDir, $url, $useInvalidated = false, $pathVersion = 1)
23 {
24 $safeUrl = str_replace(array('/', '?', ':', ';', '=', '&', '.', '--', '%', '~'), '-', $url);
25 if (defined('NITRO_DEBUG_MODE') && NITRO_DEBUG_MODE) {
26 $urlDir = $dataDir . "/" . $safeUrl;
27 } else {
28 switch ($pathVersion) {
29 case 2:
30 $urlDir = $dataDir . "/" . md5($url);
31 break;
32 default:
33 $urlDir = $dataDir . "/" . md5($safeUrl);
34 break;
35 }
36 }
37
38 if ($useInvalidated) {
39 return $urlDir . "_i";
40 } else {
41 return $urlDir;
42 }
43 }
44
45 public function __construct($url, $userAgent, $cookies = array(), $supportedCookies = array(), $isAjax = false, $referer = NULL)
46 {
47 $this->url = new Url($url);
48 $this->cookies = $cookies;
49 $this->supportedCookies = $supportedCookies;
50 $this->dataDir = NULL;
51 $this->isAjax = $isAjax;
52 $this->parent = NULL;
53 $this->Device = new Device($userAgent);
54 $this->useCompression = false;
55 $this->useInvalidated = false;
56 $this->cookiesProvider = NULL;
57 $this->setUrlPathVersion(1);
58 $this->setReferer($referer);
59 }
60
61 public function enableCompression()
62 {
63 $this->useCompression = true;
64 if ($this->parent) {
65 $this->parent->enableCompression();
66 }
67 }
68
69 public function disableCompression()
70 {
71 $this->useCompression = false;
72 if ($this->parent) {
73 $this->parent->disableCompression();
74 }
75 }
76
77 public function useInvalidated($useInvalidated)
78 {
79 $this->useInvalidated = $useInvalidated;
80 if ($this->parent) {
81 $this->parent->useInvalidated($useInvalidated);
82 }
83 }
84
85 public function getUseInvalidated()
86 {
87 return $this->useInvalidated;
88 }
89
90 public function setReferer($referer)
91 {
92 $this->referer = $referer ? new Url($referer) : NULL;
93 if ($this->referer) {
94 $this->parent = new Pagecache($this->referer->getNormalized(), $this->Device->getUserAgent(), $this->cookies, $this->supportedCookies);
95 $this->parent->setUrlPathVersion($this->urlPathVersion);
96 if ($this->dataDir) {
97 $this->parent->setDataDir(dirname($this->dataDir));
98 }
99 } else {
100 $this->parent = NULL;
101 }
102 }
103
104 public function setUrlPathVersion($version = 1)
105 {
106 $this->urlPathVersion = $version;
107 if ($this->parent) {
108 $this->parent->setUrlPathVersion($version);
109 }
110 }
111
112 public function getReferer()
113 {
114 return $this->referer;
115 }
116
117 public function getParent()
118 {
119 return $this->parent;
120 }
121
122 public function setDataDir($dir)
123 {
124 if ($dir === null) {
125 $this->dataDir = null;
126 } else {
127 $this->dataDir = $dir . "/" . $this->Device->getType();
128 }
129
130 if ($this->parent) {
131 $this->parent->setDataDir($dir);
132 }
133 }
134
135 public function setCookiesProvider($provider)
136 {
137 $this->cookiesProvider = $provider;
138 }
139
140 public function hasCache()
141 {
142 // If there is no cache for the parent we do not need to check cache existance for the current object, because we only serve AJAX cache for pages already cached by NitroPack
143 if ($this->parent && !$this->parent->hasCache()) return false;
144 if ($this->useInvalidated) {
145 $this->convertToStaleCache();
146 return Filesystem::fileExists($this->getCachefilePath("stale"));
147 } else {
148 return Filesystem::fileExists($this->getCachefilePath());
149 }
150 }
151
152 public function hasExpired($ttl = 86400, $cacheRevision = NULL)
153 {
154 // If the cache for the parent is expired we do not need to check cache for the current object, because we only serve AJAX cache for pages already cached by NitroPack
155 if ($this->parent && $this->parent->hasExpired($ttl, $cacheRevision)) return true;
156
157 if ($this->useInvalidated) {
158 $this->convertToStaleCache();
159 $cachefilePath = $this->getCachefilePath("stale");
160 $mtime = Filesystem::fileMtime(dirname($cachefilePath));
161 } else {
162 $cachefilePath = $this->getCachefilePath();
163 $mtime = Filesystem::fileMtime($cachefilePath);
164 }
165
166 $now = time();
167
168 if ($now - $mtime >= $ttl) {
169 return true;
170 } else {
171 try {
172 $headers = Filesystem::fileGetHeaders($cachefilePath);
173 if (!empty($headers["x-nitro-expires"]) && $now > (int)$headers["x-nitro-expires"]) {
174 return true;
175 }
176
177 if (
178 !$this->useInvalidated &&
179 !empty($headers["x-cache-ctime"]) &&
180 $now - (int)$headers["x-cache-ctime"] > $ttl
181 ) {
182 return true;
183 }
184
185 // Check if the revision has changed which makes cache file obsolete
186 if (!empty($headers["x-nitro-rev"]) && $cacheRevision && $cacheRevision != $headers["x-nitro-rev"]) {
187 return true;
188 }
189 } catch (\Exception $e) {
190 return true;
191 }
192 }
193 return false;
194 }
195
196 public function getRemainingTtl($ttl = 86400)
197 {
198 // If there is a parent cache file return its remaining TTL
199 if ($this->parent) return $this->parent->getRemainingTtl();
200
201 if ($this->useInvalidated) {
202 return 0;
203 } else {
204 $cachefilePath = $this->getCachefilePath();
205 }
206
207 $now = time();
208 try {
209 $headers = Filesystem::fileGetHeaders($cachefilePath);
210 if (!empty($headers["x-nitro-expires"])) {
211 $expireTime = (int)$headers["x-nitro-expires"];
212 } else if (!empty($headers["x-cache-ctime"])) {
213 $expireTime = (int)$headers["x-cache-ctime"] + $ttl;
214 } else {
215 $mtime = Filesystem::fileMtime($cachefilePath);
216 $expireTime = $mtime + $ttl;
217 }
218
219 return max($expireTime - $now, 0);
220 } catch (\Exception $e) {
221 return 0;
222 }
223
224 return 0;
225 }
226
227 public function setContent($content, $headers = NULL)
228 {
229 if (!$this->dataDir) return;
230
231 $filePath = $this->getCachefilePath();
232 if (Filesystem::createDir(dirname($filePath))) {
233 if (!Filesystem::filePutContents($filePath, $content, $this->headersFlatten($headers))) {
234 return false;
235 } else {
236 if ($headers && !empty($headers["x-cache-ctime"])) {
237 Filesystem::touch($filePath, (int)$headers["x-cache-ctime"]);
238 }
239 return $this->compress($filePath);
240 }
241 }
242 }
243
244 private function headersFlatten($headers)
245 {
246 // The headers from fileGetAll come as associative array
247 // They must be converted to an array of strings before being passed to filePutContents, which is what this function does
248 $headersFlat = array();
249 foreach ($headers as $name => $value) {
250 if (is_array($value)) {
251 foreach ($value as $subValue) {
252 $headersFlat[] = $name . ":" . $subValue;
253 }
254 } else {
255 $headersFlat[] = $name . ":" . $value;
256 }
257 }
258
259 return $headersFlat;
260 }
261
262 private function compress($filePath)
263 {
264 $fileInfo = Filesystem::fileGetAll($filePath);
265 $fileInfo->headers["Content-Encoding"] = "gzip";
266 $gzPath = $filePath . ".gz";
267 $res = Filesystem::filePutContents($gzPath, gzencode($fileInfo->content, 4), $this->headersFlatten($fileInfo->headers)); // save compressed version of the cache file
268
269 if ($res && $fileInfo->headers && !empty($fileInfo->headers["x-cache-ctime"])) {
270 Filesystem::touch($gzPath, (int)$fileInfo->headers["x-cache-ctime"]);
271 }
272
273 return $res;
274 }
275
276 public function readfile()
277 {
278 [ $headers, $contents ] = $this->returnCacheFileContent();
279
280 foreach ($headers as $header) {
281 header($header['name'] . ': ' . $header['value'], false);
282 }
283
284 echo $contents;
285 }
286
287 public function returnCacheFileContent()
288 {
289 if ($this->useInvalidated) {
290 $this->convertToStaleCache();
291 $filePath = $this->getCachefilePath("stale");
292 } else {
293 $filePath = $this->getCachefilePath();
294 }
295 if ($this->canUseCompression() && (ini_get("zlib.output_compression") === "0" || ini_set("zlib.output_compression", "0") !== false)) {
296 $filePath .= ".gz";
297 }
298 $file = Filesystem::fileGetAll($filePath);
299 $returnHeaders = [];
300
301 foreach ($file->headers as $name => $value) {
302 if (is_array($value)) {
303 foreach ($value as $subVal) {
304 $returnHeaders[] = [
305 'name' => $name,
306 'value' => $subVal
307 ];
308 }
309 } else {
310 $returnHeaders[] = [
311 'name' => $name,
312 'value' => $value
313 ];
314 }
315 }
316
317 return [ $returnHeaders, $file->content ];
318 }
319
320 public function getFileContents()
321 {
322 if ($this->useInvalidated) {
323 $this->convertToStaleCache();
324 return Filesystem::fileGetContents($this->getCachefilePath("stale"));
325 }
326 return Filesystem::fileGetContents($this->getCachefilePath());
327 }
328
329 private function convertToStaleCache()
330 {
331 $staleFile = $this->getCachefilePath("stale");
332 if (!Filesystem::fileExists($staleFile)) {
333 $freshFile = $this->getCachefilePath();
334 if (Filesystem::fileExists($freshFile)) {
335 $fileInfo = Filesystem::fileGetAll($freshFile);
336 $newContent = str_replace("NITROPACK_STATE='FRESH'", "NITROPACK_STATE='STALE'", $fileInfo->content);
337 Filesystem::filePutContents($freshFile, $newContent, $this->headersFlatten($fileInfo->headers));
338 Filesystem::rename($freshFile, $staleFile);
339 Filesystem::deleteFile($freshFile . ".gz");
340 $this->compress($staleFile);
341 }
342 }
343 }
344
345 private function cookiePrefix()
346 {
347 $prefix = '';
348
349 $cookies = $this->cookiesProvider ? call_user_func($this->cookiesProvider) : $this->cookies;
350
351 ksort($cookies);
352
353 foreach ($cookies as $cookieName => $cookieValue) {
354 foreach ($this->supportedCookies as $cookie) {
355 if (preg_match('/' . NitroPack::wildcardToRegex($cookie) . '/', $cookieName)) {
356 $prefix .= $cookieName . '=' . $cookieValue . ';';
357 }
358 }
359 }
360
361 return substr(md5($prefix), 0, 16);
362 }
363
364 private function sslPrefix()
365 {
366 return $this->url->getScheme() == "https" ? "ssl-" : "";
367 }
368
369 private function ajaxPrefix()
370 {
371 return $this->isAjax && $this->parent ? "ajax-" . md5($this->url->getNormalized()) . "-" : "";
372 }
373
374 private function customCachePrefix()
375 {
376 $customCachePrefix = NitroPack::getCustomCachePrefix();
377 return $customCachePrefix ? $customCachePrefix . "-" : "";
378 }
379
380 private function isCompressionAllowed()
381 {
382 return isset($_SERVER['HTTP_ACCEPT_ENCODING']) && strpos($_SERVER['HTTP_ACCEPT_ENCODING'], 'gzip') !== false;
383 }
384
385 private function canUseCompression()
386 {
387 return $this->useCompression && $this->isCompressionAllowed() && !headers_sent();
388 }
389
390 public function nameOfCachefile()
391 {
392 return $this->customCachePrefix() . $this->ajaxPrefix() . $this->sslPrefix() . $this->cookiePrefix() . ".html";
393 }
394
395 public function getCachefilePath($suffix = "")
396 {
397 if ($suffix) $suffix = "." . $suffix;
398 if ($this->isAjax && $this->referer) {
399 return self::getUrlDir($this->dataDir, $this->referer->getNormalized(), $this->useInvalidated, $this->urlPathVersion) . "/" . $this->nameOfCachefile() . $suffix;
400 }
401 return self::getUrlDir($this->dataDir, $this->url->getNormalized(), $this->useInvalidated, $this->urlPathVersion) . "/" . $this->nameOfCachefile() . $suffix;
402 }
403 }