PluginProbe ʕ •ᴥ•ʔ
NitroPack – Performance, Page Speed & Cache Plugin for Core Web Vitals, CDN & Image Optimization / 1.19.8
NitroPack – Performance, Page Speed & Cache Plugin for Core Web Vitals, CDN & Image Optimization v1.19.8
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 7 months ago Integrations 1 month ago StorageDriver 1 year ago Url 1 year ago Utils 1 year ago data 1 year ago Api.php 1 year ago Backlog.php 1 year ago BacklogReplayTimeoutException.php 1 year ago ChallengeProcessingException.php 1 year ago ChallengeVerificationException.php 1 year ago ConfigFetcherException.php 1 year ago Crypto.php 1 year ago Device.php 1 year ago DeviceType.php 1 year ago ElementRevision.php 1 year ago EmptyConfigException.php 1 year ago ExcludeEntry.php 1 year ago ExcludeOperationType.php 1 year ago FileHandle.php 1 year ago Filesystem.php 1 year ago HealthStatus.php 1 year ago IntegrationUrl.php 1 year ago NitroPack.php 1 month ago NoConfigException.php 1 year ago Pagecache.php 5 months ago PurgeType.php 1 year ago ServiceDownException.php 1 year ago StorageException.php 1 year ago VariationCookieException.php 1 year ago WebhookException.php 1 year ago Website.php 1 year ago
Pagecache.php
530 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 private $geotVariations;
22 private $NotModifiedHeaderEnabled = false;
23
24 public static function getUrlDir($dataDir, $url, $useInvalidated = false, $pathVersion = 1)
25 {
26 $safeUrl = str_replace(array('/', '?', ':', ';', '=', '&', '.', '--', '%', '~'), '-', $url);
27 if (defined('NITRO_DEBUG_MODE') && NITRO_DEBUG_MODE) {
28 $urlDir = $dataDir . "/" . $safeUrl;
29 } else {
30 switch ($pathVersion) {
31 case 2:
32 $urlDir = $dataDir . "/" . md5($url);
33 break;
34 default:
35 $urlDir = $dataDir . "/" . md5($safeUrl);
36 break;
37 }
38 }
39
40 if ($useInvalidated) {
41 return $urlDir . "_i";
42 } else {
43 return $urlDir;
44 }
45 }
46
47 public function __construct($url, $userAgent, $cookies = array(), $supportedCookies = array(), $isAjax = false, $referer = NULL)
48 {
49 $this->url = new Url($url);
50 $this->cookies = $cookies;
51 $this->supportedCookies = $supportedCookies;
52 $this->dataDir = NULL;
53 $this->isAjax = $isAjax;
54 $this->parent = NULL;
55 $this->Device = new Device($userAgent);
56 $this->useCompression = false;
57 $this->useInvalidated = false;
58 $this->cookiesProvider = NULL;
59 $this->geotVariations = new \stdClass;
60 $this->setUrlPathVersion(1);
61 $this->setReferer($referer);
62 }
63
64 public function enableCompression()
65 {
66 $this->useCompression = true;
67 if ($this->parent) {
68 $this->parent->enableCompression();
69 }
70 }
71
72 public function disableCompression()
73 {
74 $this->useCompression = false;
75 if ($this->parent) {
76 $this->parent->disableCompression();
77 }
78 }
79
80 public function useInvalidated($useInvalidated)
81 {
82 $this->useInvalidated = $useInvalidated;
83 if ($this->parent) {
84 $this->parent->useInvalidated($useInvalidated);
85 }
86 }
87
88 public function getUseInvalidated()
89 {
90 return $this->useInvalidated;
91 }
92
93 public function setReferer($referer)
94 {
95 $this->referer = $referer ? new Url($referer) : NULL;
96 if ($this->referer) {
97 $this->parent = new Pagecache($this->referer->getNormalized(), $this->Device->getUserAgent(), $this->cookies, $this->supportedCookies);
98 $this->parent->setUrlPathVersion($this->urlPathVersion);
99 //$this->parent->setCookiesProvider($this->cookiesProvider);
100
101 if (count(get_object_vars($this->geotVariations)) > 0) {
102 $this->parent->setGeotVariations($this->geotVariations);
103 }
104
105 if ($this->dataDir) {
106 $this->parent->setDataDir(dirname($this->dataDir));
107 }
108 } else {
109 $this->parent = NULL;
110 }
111 }
112
113 public function setUrlPathVersion($version = 1)
114 {
115 $this->urlPathVersion = $version;
116 if ($this->parent) {
117 $this->parent->setUrlPathVersion($version);
118 }
119 }
120
121 public function getReferer()
122 {
123 return $this->referer;
124 }
125
126 public function getParent()
127 {
128 return $this->parent;
129 }
130
131 public function setDataDir($dir)
132 {
133 if ($dir === null) {
134 $this->dataDir = null;
135 } else {
136 $this->dataDir = $dir . "/" . $this->Device->getType();
137 }
138
139 if ($this->parent) {
140 $this->parent->setDataDir($dir);
141 }
142 }
143
144 public function setCookiesProvider($provider)
145 {
146 $this->cookiesProvider = $provider;
147 }
148
149 public function setGeotVariations($geotVariations)
150 {
151 $this->geotVariations = $geotVariations;
152 }
153
154 public function enableNotModifiedHeader()
155 {
156 $this->NotModifiedHeaderEnabled = true;
157 }
158
159 public function hasCache()
160 {
161 // 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
162 if ($this->parent && !$this->parent->hasCache()) return false;
163 if ($this->useInvalidated) {
164 $this->convertToStaleCache();
165 return Filesystem::fileExists($this->getCachefilePath("stale"));
166 } else {
167 return Filesystem::fileExists($this->getCachefilePath());
168 }
169 }
170
171 public function hasExpired($ttl = 86400, $cacheRevision = NULL)
172 {
173 // 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
174 if ($this->parent && $this->parent->hasExpired($ttl, $cacheRevision)) return true;
175
176 if ($this->useInvalidated) {
177 $this->convertToStaleCache();
178 $cachefilePath = $this->getCachefilePath("stale");
179 $mtime = Filesystem::fileMtime(dirname($cachefilePath));
180 } else {
181 $cachefilePath = $this->getCachefilePath();
182 $mtime = Filesystem::fileMtime($cachefilePath);
183 }
184
185 $now = time();
186
187 if ($now - $mtime >= $ttl) {
188 return true;
189 } else {
190 try {
191 $headers = Filesystem::fileGetHeaders($cachefilePath);
192 if (!empty($headers["x-nitro-expires"]) && $now > (int)$headers["x-nitro-expires"]) {
193 return true;
194 }
195
196 if (
197 !$this->useInvalidated &&
198 !empty($headers["x-cache-ctime"]) &&
199 $now - (int)$headers["x-cache-ctime"] > $ttl
200 ) {
201 return true;
202 }
203
204 $expectedRev = !empty($cacheRevision) ? $cacheRevision : NULL;
205 $cachedRev = !empty($headers["x-nitro-rev"]) ? $headers["x-nitro-rev"] : NULL;
206 // Check if the revision has changed which makes cache file obsolete
207 if ($expectedRev && $expectedRev !== $cachedRev) {
208 return true;
209 }
210 } catch (\Exception $e) {
211 return true;
212 }
213 }
214 return false;
215 }
216
217 public function getRemainingTtl($ttl = 86400)
218 {
219 // If there is a parent cache file return its remaining TTL
220 if ($this->parent) return $this->parent->getRemainingTtl();
221
222 if ($this->useInvalidated) {
223 return 0;
224 } else {
225 $cachefilePath = $this->getCachefilePath();
226 }
227
228 $now = time();
229 try {
230 $headers = Filesystem::fileGetHeaders($cachefilePath);
231 if (!empty($headers["x-nitro-expires"])) {
232 $expireTime = (int)$headers["x-nitro-expires"];
233 } else if (!empty($headers["x-cache-ctime"])) {
234 $expireTime = (int)$headers["x-cache-ctime"] + $ttl;
235 } else {
236 $mtime = Filesystem::fileMtime($cachefilePath);
237 $expireTime = $mtime + $ttl;
238 }
239
240 return max($expireTime - $now, 0);
241 } catch (\Exception $e) {
242 return 0;
243 }
244
245 return 0;
246 }
247
248 public function setContent($content, $headers = NULL)
249 {
250 if (!$this->dataDir) return;
251
252 $filePath = $this->getCachefilePath();
253 if (Filesystem::createDir(dirname($filePath))) {
254 if (!Filesystem::filePutContents($filePath, $content, $this->headersFlatten($headers))) {
255 return false;
256 } else {
257 if ($headers && !empty($headers["x-cache-ctime"])) {
258 Filesystem::touch($filePath, (int)$headers["x-cache-ctime"]);
259 }
260 return $this->compress($filePath);
261 }
262 }
263 }
264
265 private function headersFlatten($headers)
266 {
267 // The headers from fileGetAll come as associative array
268 // They must be converted to an array of strings before being passed to filePutContents, which is what this function does
269 $headersFlat = array();
270 foreach ($headers as $name => $value) {
271 if (is_array($value)) {
272 foreach ($value as $subValue) {
273 $headersFlat[] = $name . ":" . $subValue;
274 }
275 } else {
276 $headersFlat[] = $name . ":" . $value;
277 }
278 }
279
280 return $headersFlat;
281 }
282
283 private function compress($filePath)
284 {
285 $fileInfo = Filesystem::fileGetAll($filePath);
286 $fileInfo->headers["Content-Encoding"] = "gzip";
287 $gzPath = $filePath . ".gz";
288 $res = Filesystem::filePutContents($gzPath, gzencode($fileInfo->content, 4), $this->headersFlatten($fileInfo->headers)); // save compressed version of the cache file
289
290 if ($res && $fileInfo->headers && !empty($fileInfo->headers["x-cache-ctime"])) {
291 Filesystem::touch($gzPath, (int)$fileInfo->headers["x-cache-ctime"]);
292 }
293
294 return $res;
295 }
296
297 public function readfile()
298 {
299 $setOwnLastModified = false;
300
301 // 304 Not Modified improvement - only when enabled via config
302 if ($this->NotModifiedHeaderEnabled) {
303 $lastModified = $this->getCacheCreationTime();
304
305 // Set Last-Modified header based on x-cache-ctime
306 if ($lastModified > 0) {
307 $lastModifiedHeader = gmdate('D, d M Y H:i:s', $lastModified) . ' GMT';
308 header('Last-Modified: ' . $lastModifiedHeader);
309 $setOwnLastModified = true;
310
311 // Calculate current variation hash
312 $currentVariationPrefix = $this->getVariationCookiePrefix();
313 $currentVariationHash = md5($currentVariationPrefix);
314
315 // Get last served variation from tracking cookie
316 $lastVariationHash = isset($_COOKIE['np_cache_variation'])
317 ? $_COOKIE['np_cache_variation']
318 : '';
319
320 // If variation is different now - bypass 304 mechanism.
321 $variationChanged = ($currentVariationHash !== $lastVariationHash);
322
323 // Update tracking cookie to current variation
324 if ($variationChanged) {
325 setcookie('np_cache_variation', $currentVariationHash, 0, '/');
326 }
327
328 if (!$variationChanged) {
329 $httpModifiedSince = isset($_SERVER['HTTP_IF_MODIFIED_SINCE'])
330 ? strtotime($_SERVER['HTTP_IF_MODIFIED_SINCE'])
331 : 0;
332
333 if ($httpModifiedSince && $httpModifiedSince >= $lastModified) {
334 http_response_code(304);
335 return;
336 }
337 }
338 // If variation changed, skip 304 - always serve full response
339 }
340 }
341
342 // Normal flow - serve cache file
343 $cacheFileContent = $this->returnCacheFileContent();
344 $headers = $cacheFileContent[0];
345 $contents = $cacheFileContent[1];
346
347 foreach ($headers as $header) {
348 // Skip Last-Modified from cache file only if we set our own from x-cache-ctime
349 if ($setOwnLastModified && strtolower($header['name']) === 'last-modified') {
350 continue;
351 }
352 header($header['name'] . ': ' . $header['value'], false);
353 }
354
355 echo $contents;
356 }
357
358 /**
359 * Get cache creation time from x-cache-ctime header.
360 */
361 private function getCacheCreationTime()
362 {
363 if ($this->useInvalidated) {
364 $filePath = $this->getCachefilePath("stale");
365 } else {
366 $filePath = $this->getCachefilePath();
367 }
368
369 try {
370 $headers = Filesystem::fileGetHeaders($filePath);
371 return !empty($headers["x-cache-ctime"]) ? (int)$headers["x-cache-ctime"] : 0;
372 } catch (\Exception $e) {
373 return 0;
374 }
375 }
376
377 /**
378 * Get variation cookie prefix (only cookies that create cache variations).
379 * Excludes the tracking cookie to prevent circular dependency.
380 * Returns empty string if no variation cookies are present.
381 */
382 private function getVariationCookiePrefix()
383 {
384 $prefix = '';
385
386 $cookies = $this->cookiesProvider ? call_user_func($this->cookiesProvider) : $this->cookies;
387 ksort($cookies);
388
389 foreach ($cookies as $cookieName => $cookieValue) {
390 // Exclude our tracking cookie to prevent circular dependency
391 // (tracking cookie value depends on this prefix)
392 if ($cookieName === 'np_cache_variation') {
393 continue;
394 }
395
396 foreach ($this->supportedCookies as $cookie) {
397 if (preg_match('/' . NitroPack::wildcardToRegex($cookie) . '/', $cookieName)) {
398 $prefix .= $cookieName . '=' . $cookieValue . ';';
399 break;
400 }
401 }
402 }
403
404 return $prefix;
405 }
406
407 public function returnCacheFileContent()
408 {
409 if ($this->useInvalidated) {
410 $this->convertToStaleCache();
411 $filePath = $this->getCachefilePath("stale");
412 } else {
413 $filePath = $this->getCachefilePath();
414 }
415 if ($this->canUseCompression() && (ini_get("zlib.output_compression") === "0" || ini_set("zlib.output_compression", "0") !== false)) {
416 $filePath .= ".gz";
417 }
418 $file = Filesystem::fileGetAll($filePath);
419 $returnHeaders = [];
420
421 foreach ($file->headers as $name => $value) {
422 if (is_array($value)) {
423 foreach ($value as $subVal) {
424 $returnHeaders[] = [
425 'name' => $name,
426 'value' => $subVal
427 ];
428 }
429 } else {
430 $returnHeaders[] = [
431 'name' => $name,
432 'value' => $value
433 ];
434 }
435 }
436
437 return [ $returnHeaders, $file->content ];
438 }
439
440 public function getFileContents()
441 {
442 if ($this->useInvalidated) {
443 $this->convertToStaleCache();
444 return Filesystem::fileGetContents($this->getCachefilePath("stale"));
445 }
446 return Filesystem::fileGetContents($this->getCachefilePath());
447 }
448
449 private function convertToStaleCache()
450 {
451 $staleFile = $this->getCachefilePath("stale");
452 if (!Filesystem::fileExists($staleFile)) {
453 $freshFile = $this->getCachefilePath();
454 if (Filesystem::fileExists($freshFile)) {
455 $fileInfo = Filesystem::fileGetAll($freshFile);
456 $newContent = str_replace("NITROPACK_STATE='FRESH'", "NITROPACK_STATE='STALE'", $fileInfo->content);
457 Filesystem::filePutContents($freshFile, $newContent, $this->headersFlatten($fileInfo->headers));
458 Filesystem::rename($freshFile, $staleFile);
459 Filesystem::deleteFile($freshFile . ".gz");
460 $this->compress($staleFile);
461 }
462 }
463 }
464
465 private function cookiePrefix()
466 {
467 $prefix = '';
468
469 $cookies = $this->cookiesProvider ? call_user_func($this->cookiesProvider) : $this->cookies;
470
471 ksort($cookies);
472
473 foreach ($cookies as $cookieName => $cookieValue) {
474 foreach ($this->supportedCookies as $cookie) {
475 if (preg_match('/' . NitroPack::wildcardToRegex($cookie) . '/', $cookieName)) {
476 if (preg_match("/^nitro_geot_(.*)/", $cookieName, $matches)) {
477 $geotComponent = $matches[1];
478 if (property_exists($this->geotVariations, $geotComponent) && !empty($this->geotVariations->$geotComponent) && !in_array($cookieValue, $this->geotVariations->$geotComponent)) {
479 $prefix .= $cookieName . '=' . $this->geotVariations->{$geotComponent}[0] . ';'; // Use the first variation as default
480 continue;
481 }
482 }
483 $prefix .= $cookieName . '=' . $cookieValue . ';';
484 }
485 }
486 }
487
488 return substr(md5($prefix), 0, 16);
489 }
490
491 private function sslPrefix()
492 {
493 return $this->url->getScheme() == "https" ? "ssl-" : "";
494 }
495
496 private function ajaxPrefix()
497 {
498 return $this->isAjax && $this->parent ? "ajax-" . md5($this->url->getNormalized()) . "-" : "";
499 }
500
501 private function customCachePrefix()
502 {
503 $customCachePrefix = NitroPack::getCustomCachePrefix();
504 return $customCachePrefix ? $customCachePrefix . "-" : "";
505 }
506
507 private function isCompressionAllowed()
508 {
509 return isset($_SERVER['HTTP_ACCEPT_ENCODING']) && strpos($_SERVER['HTTP_ACCEPT_ENCODING'], 'gzip') !== false;
510 }
511
512 private function canUseCompression()
513 {
514 return $this->useCompression && $this->isCompressionAllowed() && !headers_sent();
515 }
516
517 public function nameOfCachefile()
518 {
519 return $this->customCachePrefix() . $this->ajaxPrefix() . $this->sslPrefix() . $this->cookiePrefix() . ".html";
520 }
521
522 public function getCachefilePath($suffix = "")
523 {
524 if ($suffix) $suffix = "." . $suffix;
525 if ($this->isAjax && $this->referer) {
526 return self::getUrlDir($this->dataDir, $this->referer->getNormalized(), $this->useInvalidated, $this->urlPathVersion) . "/" . $this->nameOfCachefile() . $suffix;
527 }
528 return self::getUrlDir($this->dataDir, $this->url->getNormalized(), $this->useInvalidated, $this->urlPathVersion) . "/" . $this->nameOfCachefile() . $suffix;
529 }
530 }