PluginProbe ʕ •ᴥ•ʔ
NitroPack – Performance, Page Speed & Cache Plugin for Core Web Vitals, CDN & Image Optimization / 1.16.4
NitroPack – Performance, Page Speed & Cache Plugin for Core Web Vitals, CDN & Image Optimization v1.16.4
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 2 years ago Integrations 2 years ago StorageDriver 4 years ago Url 2 years ago Utils 2 years ago data 6 years ago Api.php 2 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 ExcludeEntry.php 2 years ago ExcludeOperationType.php 2 years ago FileHandle.php 5 years ago Filesystem.php 4 years ago HealthStatus.php 5 years ago IntegrationUrl.php 2 years ago NitroPack.php 1 year ago NoConfigException.php 5 years ago Pagecache.php 2 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
427 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
23 public static function getUrlDir($dataDir, $url, $useInvalidated = false, $pathVersion = 1)
24 {
25 $safeUrl = str_replace(array('/', '?', ':', ';', '=', '&', '.', '--', '%', '~'), '-', $url);
26 if (defined('NITRO_DEBUG_MODE') && NITRO_DEBUG_MODE) {
27 $urlDir = $dataDir . "/" . $safeUrl;
28 } else {
29 switch ($pathVersion) {
30 case 2:
31 $urlDir = $dataDir . "/" . md5($url);
32 break;
33 default:
34 $urlDir = $dataDir . "/" . md5($safeUrl);
35 break;
36 }
37 }
38
39 if ($useInvalidated) {
40 return $urlDir . "_i";
41 } else {
42 return $urlDir;
43 }
44 }
45
46 public function __construct($url, $userAgent, $cookies = array(), $supportedCookies = array(), $isAjax = false, $referer = NULL)
47 {
48 $this->url = new Url($url);
49 $this->cookies = $cookies;
50 $this->supportedCookies = $supportedCookies;
51 $this->dataDir = NULL;
52 $this->isAjax = $isAjax;
53 $this->parent = NULL;
54 $this->Device = new Device($userAgent);
55 $this->useCompression = false;
56 $this->useInvalidated = false;
57 $this->cookiesProvider = NULL;
58 $this->geotVariations = new \stdClass;
59 $this->setUrlPathVersion(1);
60 $this->setReferer($referer);
61 }
62
63 public function enableCompression()
64 {
65 $this->useCompression = true;
66 if ($this->parent) {
67 $this->parent->enableCompression();
68 }
69 }
70
71 public function disableCompression()
72 {
73 $this->useCompression = false;
74 if ($this->parent) {
75 $this->parent->disableCompression();
76 }
77 }
78
79 public function useInvalidated($useInvalidated)
80 {
81 $this->useInvalidated = $useInvalidated;
82 if ($this->parent) {
83 $this->parent->useInvalidated($useInvalidated);
84 }
85 }
86
87 public function getUseInvalidated()
88 {
89 return $this->useInvalidated;
90 }
91
92 public function setReferer($referer)
93 {
94 $this->referer = $referer ? new Url($referer) : NULL;
95 if ($this->referer) {
96 $this->parent = new Pagecache($this->referer->getNormalized(), $this->Device->getUserAgent(), $this->cookies, $this->supportedCookies);
97 $this->parent->setUrlPathVersion($this->urlPathVersion);
98 //$this->parent->setCookiesProvider($this->cookiesProvider);
99
100 if (count(get_object_vars($this->geotVariations)) > 0) {
101 $this->parent->setGeotVariations($this->geotVariations);
102 }
103
104 if ($this->dataDir) {
105 $this->parent->setDataDir(dirname($this->dataDir));
106 }
107 } else {
108 $this->parent = NULL;
109 }
110 }
111
112 public function setUrlPathVersion($version = 1)
113 {
114 $this->urlPathVersion = $version;
115 if ($this->parent) {
116 $this->parent->setUrlPathVersion($version);
117 }
118 }
119
120 public function getReferer()
121 {
122 return $this->referer;
123 }
124
125 public function getParent()
126 {
127 return $this->parent;
128 }
129
130 public function setDataDir($dir)
131 {
132 if ($dir === null) {
133 $this->dataDir = null;
134 } else {
135 $this->dataDir = $dir . "/" . $this->Device->getType();
136 }
137
138 if ($this->parent) {
139 $this->parent->setDataDir($dir);
140 }
141 }
142
143 public function setCookiesProvider($provider)
144 {
145 $this->cookiesProvider = $provider;
146 }
147
148 public function setGeotVariations($geotVariations)
149 {
150 $this->geotVariations = $geotVariations;
151 }
152
153 public function hasCache()
154 {
155 // 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
156 if ($this->parent && !$this->parent->hasCache()) return false;
157 if ($this->useInvalidated) {
158 $this->convertToStaleCache();
159 return Filesystem::fileExists($this->getCachefilePath("stale"));
160 } else {
161 return Filesystem::fileExists($this->getCachefilePath());
162 }
163 }
164
165 public function hasExpired($ttl = 86400, $cacheRevision = NULL)
166 {
167 // 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
168 if ($this->parent && $this->parent->hasExpired($ttl, $cacheRevision)) return true;
169
170 if ($this->useInvalidated) {
171 $this->convertToStaleCache();
172 $cachefilePath = $this->getCachefilePath("stale");
173 $mtime = Filesystem::fileMtime(dirname($cachefilePath));
174 } else {
175 $cachefilePath = $this->getCachefilePath();
176 $mtime = Filesystem::fileMtime($cachefilePath);
177 }
178
179 $now = time();
180
181 if ($now - $mtime >= $ttl) {
182 return true;
183 } else {
184 try {
185 $headers = Filesystem::fileGetHeaders($cachefilePath);
186 if (!empty($headers["x-nitro-expires"]) && $now > (int)$headers["x-nitro-expires"]) {
187 return true;
188 }
189
190 if (
191 !$this->useInvalidated &&
192 !empty($headers["x-cache-ctime"]) &&
193 $now - (int)$headers["x-cache-ctime"] > $ttl
194 ) {
195 return true;
196 }
197
198 $expectedRev = !empty($cacheRevision) ? $cacheRevision : NULL;
199 $cachedRev = !empty($headers["x-nitro-rev"]) ? $headers["x-nitro-rev"] : NULL;
200 // Check if the revision has changed which makes cache file obsolete
201 if ($expectedRev && $expectedRev !== $cachedRev) {
202 return true;
203 }
204 } catch (\Exception $e) {
205 return true;
206 }
207 }
208 return false;
209 }
210
211 public function getRemainingTtl($ttl = 86400)
212 {
213 // If there is a parent cache file return its remaining TTL
214 if ($this->parent) return $this->parent->getRemainingTtl();
215
216 if ($this->useInvalidated) {
217 return 0;
218 } else {
219 $cachefilePath = $this->getCachefilePath();
220 }
221
222 $now = time();
223 try {
224 $headers = Filesystem::fileGetHeaders($cachefilePath);
225 if (!empty($headers["x-nitro-expires"])) {
226 $expireTime = (int)$headers["x-nitro-expires"];
227 } else if (!empty($headers["x-cache-ctime"])) {
228 $expireTime = (int)$headers["x-cache-ctime"] + $ttl;
229 } else {
230 $mtime = Filesystem::fileMtime($cachefilePath);
231 $expireTime = $mtime + $ttl;
232 }
233
234 return max($expireTime - $now, 0);
235 } catch (\Exception $e) {
236 return 0;
237 }
238
239 return 0;
240 }
241
242 public function setContent($content, $headers = NULL)
243 {
244 if (!$this->dataDir) return;
245
246 $filePath = $this->getCachefilePath();
247 if (Filesystem::createDir(dirname($filePath))) {
248 if (!Filesystem::filePutContents($filePath, $content, $this->headersFlatten($headers))) {
249 return false;
250 } else {
251 if ($headers && !empty($headers["x-cache-ctime"])) {
252 Filesystem::touch($filePath, (int)$headers["x-cache-ctime"]);
253 }
254 return $this->compress($filePath);
255 }
256 }
257 }
258
259 private function headersFlatten($headers)
260 {
261 // The headers from fileGetAll come as associative array
262 // They must be converted to an array of strings before being passed to filePutContents, which is what this function does
263 $headersFlat = array();
264 foreach ($headers as $name => $value) {
265 if (is_array($value)) {
266 foreach ($value as $subValue) {
267 $headersFlat[] = $name . ":" . $subValue;
268 }
269 } else {
270 $headersFlat[] = $name . ":" . $value;
271 }
272 }
273
274 return $headersFlat;
275 }
276
277 private function compress($filePath)
278 {
279 $fileInfo = Filesystem::fileGetAll($filePath);
280 $fileInfo->headers["Content-Encoding"] = "gzip";
281 $gzPath = $filePath . ".gz";
282 $res = Filesystem::filePutContents($gzPath, gzencode($fileInfo->content, 4), $this->headersFlatten($fileInfo->headers)); // save compressed version of the cache file
283
284 if ($res && $fileInfo->headers && !empty($fileInfo->headers["x-cache-ctime"])) {
285 Filesystem::touch($gzPath, (int)$fileInfo->headers["x-cache-ctime"]);
286 }
287
288 return $res;
289 }
290
291 public function readfile()
292 {
293 $cacheFileContent = $this->returnCacheFileContent();
294 $headers = $cacheFileContent[0];
295 $contents = $cacheFileContent[1];
296
297 foreach ($headers as $header) {
298 header($header['name'] . ': ' . $header['value'], false);
299 }
300
301 echo $contents;
302 }
303
304 public function returnCacheFileContent()
305 {
306 if ($this->useInvalidated) {
307 $this->convertToStaleCache();
308 $filePath = $this->getCachefilePath("stale");
309 } else {
310 $filePath = $this->getCachefilePath();
311 }
312 if ($this->canUseCompression() && (ini_get("zlib.output_compression") === "0" || ini_set("zlib.output_compression", "0") !== false)) {
313 $filePath .= ".gz";
314 }
315 $file = Filesystem::fileGetAll($filePath);
316 $returnHeaders = [];
317
318 foreach ($file->headers as $name => $value) {
319 if (is_array($value)) {
320 foreach ($value as $subVal) {
321 $returnHeaders[] = [
322 'name' => $name,
323 'value' => $subVal
324 ];
325 }
326 } else {
327 $returnHeaders[] = [
328 'name' => $name,
329 'value' => $value
330 ];
331 }
332 }
333
334 return [ $returnHeaders, $file->content ];
335 }
336
337 public function getFileContents()
338 {
339 if ($this->useInvalidated) {
340 $this->convertToStaleCache();
341 return Filesystem::fileGetContents($this->getCachefilePath("stale"));
342 }
343 return Filesystem::fileGetContents($this->getCachefilePath());
344 }
345
346 private function convertToStaleCache()
347 {
348 $staleFile = $this->getCachefilePath("stale");
349 if (!Filesystem::fileExists($staleFile)) {
350 $freshFile = $this->getCachefilePath();
351 if (Filesystem::fileExists($freshFile)) {
352 $fileInfo = Filesystem::fileGetAll($freshFile);
353 $newContent = str_replace("NITROPACK_STATE='FRESH'", "NITROPACK_STATE='STALE'", $fileInfo->content);
354 Filesystem::filePutContents($freshFile, $newContent, $this->headersFlatten($fileInfo->headers));
355 Filesystem::rename($freshFile, $staleFile);
356 Filesystem::deleteFile($freshFile . ".gz");
357 $this->compress($staleFile);
358 }
359 }
360 }
361
362 private function cookiePrefix()
363 {
364 $prefix = '';
365
366 $cookies = $this->cookiesProvider ? call_user_func($this->cookiesProvider) : $this->cookies;
367
368 ksort($cookies);
369
370 foreach ($cookies as $cookieName => $cookieValue) {
371 foreach ($this->supportedCookies as $cookie) {
372 if (preg_match('/' . NitroPack::wildcardToRegex($cookie) . '/', $cookieName)) {
373 if (preg_match("/^nitro_geot_(.*)/", $cookieName, $matches)) {
374 $geotComponent = $matches[1];
375 if (property_exists($this->geotVariations, $geotComponent) && !empty($this->geotVariations->$geotComponent) && !in_array($cookieValue, $this->geotVariations->$geotComponent)) {
376 $prefix .= $cookieName . '=' . $this->geotVariations->$geotComponent[0] . ';'; // Use the first variation as default
377 continue;
378 }
379 }
380 $prefix .= $cookieName . '=' . $cookieValue . ';';
381 }
382 }
383 }
384
385 return substr(md5($prefix), 0, 16);
386 }
387
388 private function sslPrefix()
389 {
390 return $this->url->getScheme() == "https" ? "ssl-" : "";
391 }
392
393 private function ajaxPrefix()
394 {
395 return $this->isAjax && $this->parent ? "ajax-" . md5($this->url->getNormalized()) . "-" : "";
396 }
397
398 private function customCachePrefix()
399 {
400 $customCachePrefix = NitroPack::getCustomCachePrefix();
401 return $customCachePrefix ? $customCachePrefix . "-" : "";
402 }
403
404 private function isCompressionAllowed()
405 {
406 return isset($_SERVER['HTTP_ACCEPT_ENCODING']) && strpos($_SERVER['HTTP_ACCEPT_ENCODING'], 'gzip') !== false;
407 }
408
409 private function canUseCompression()
410 {
411 return $this->useCompression && $this->isCompressionAllowed() && !headers_sent();
412 }
413
414 public function nameOfCachefile()
415 {
416 return $this->customCachePrefix() . $this->ajaxPrefix() . $this->sslPrefix() . $this->cookiePrefix() . ".html";
417 }
418
419 public function getCachefilePath($suffix = "")
420 {
421 if ($suffix) $suffix = "." . $suffix;
422 if ($this->isAjax && $this->referer) {
423 return self::getUrlDir($this->dataDir, $this->referer->getNormalized(), $this->useInvalidated, $this->urlPathVersion) . "/" . $this->nameOfCachefile() . $suffix;
424 }
425 return self::getUrlDir($this->dataDir, $this->url->getNormalized(), $this->useInvalidated, $this->urlPathVersion) . "/" . $this->nameOfCachefile() . $suffix;
426 }
427 }