PluginProbe ʕ •ᴥ•ʔ
NitroPack – Performance, Page Speed & Cache Plugin for Core Web Vitals, CDN & Image Optimization / 1.10.0
NitroPack – Performance, Page Speed & Cache Plugin for Core Web Vitals, CDN & Image Optimization v1.10.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 / NitroPack.php
nitropack / nitropack-sdk / NitroPack / SDK Last commit date
Api 2 years ago Integrations 3 years ago StorageDriver 4 years ago Url 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 2 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
NitroPack.php
1080 lines
1 <?php
2 namespace NitroPack\SDK;
3
4 use \NitroPack\Url\Url;
5 use \NitroPack\SDK\Url\Embedjs;
6
7 class NitroPack {
8 const VERSION = '0.55.0';
9 const PAGECACHE_LOCK_EXPIRATION_TIME = 300; // in seconds
10 private $dataDir;
11 private $cachePath = array('data', 'pagecache');
12 private $configFile = array('data', 'config.json');
13 private $healthStatusFile = array('data', 'service-health');
14 private $timestampFile = array('data', 'time.mark');
15 private $pageCacheLockFile = array('data', 'get_cache.lock');
16 private $statefulCacheRevisionsFile = array('data', 'element-revision.json');
17 private $cachePathSuffix = NULL;
18 private $configTTL; // In seconds
19
20 private $siteId;
21 private $siteSecret;
22 private $userAgent; // Defaults to desktop Chrome
23
24 private $url;
25 private $config;
26 private $device;
27 private $api;
28 private $varnishProxyCacheHeaders = [];
29 private $referer;
30
31 public $backlog;
32 public $elementRevision;
33 public $healthStatus;
34 public $pageCache; // TODO: consider better ways of protecting/providing this outside the class
35 public $useCompression;
36
37 private static $cachePrefixes = array();
38 private static $cookieFilters = array();
39
40 public static function getRemoteAddr() {
41 // IP check order is: CloudFlare, Proxy, Client IP
42 $ipKeys = ["HTTP_X_FORWARDED_FOR", "HTTP_CF_CONNECTING_IP", "REMOTE_ADDR"];
43 foreach ($ipKeys as $key) {
44 if (!empty($_SERVER[$key])) {
45 return $_SERVER[$key];
46 }
47 }
48
49 return NULL;
50 }
51
52 public static function getCookies() {
53 $cookies = array();
54
55 foreach ($_COOKIE as $name=>$value) {
56 if (is_array($value)) {
57 foreach ($value as $k=>$v) {
58 $key = $name . "[$k]";
59 $cookies[$key] = $v;
60 }
61 } else {
62 $cookies[$name] = $value;
63 }
64 }
65
66 foreach (self::$cookieFilters as $cookieFilter) {
67 call_user_func_array($cookieFilter, array(&$cookies));
68 }
69
70 return $cookies;
71 }
72
73 public static function addCookieFilter($callback) {
74 if (is_callable($callback)) {
75 if (!in_array($callback, self::$cookieFilters, true)) {
76 self::$cookieFilters[] = $callback;
77 }
78 } else {
79 throw new \RuntimeException("Non-callable callback passed to " . __FUNCTION__);
80 }
81 }
82
83 public static function addCustomCachePrefix($prefix = "") {
84 self::$cachePrefixes[] = $prefix;
85 }
86
87 public static function getCustomCachePrefix() {
88 return implode("-", self::$cachePrefixes);
89 }
90
91 public static function wildcardToRegex($str, $delim = "/") {
92 return implode(".*?", array_map(function($input) use ($delim) { return preg_quote($input, $delim); }, explode("*", $str)));
93 }
94
95 private function nitro_parse_str($qStr, &$resArr) {
96 if (strpos($qStr, '?') !== false) {
97 $tmpArr = explode('?', $qStr, 2);
98 parse_str($tmpArr[0], $resArr);
99 $completeValue = end($resArr) . $tmpArr[1];
100 $resArr[key($resArr)] = $completeValue;
101 reset($resArr);
102 } else {
103 parse_str($qStr, $resArr);
104 }
105 }
106
107 public function __construct($siteId, $siteSecret, $userAgent = NULL, $url = NULL, $dataDir = __DIR__, $referer = NULL) {
108 $this->configTTL = 3600;
109 $this->siteId = $siteId;
110 $this->siteSecret = $siteSecret;
111 $this->dataDir = $dataDir;
112 $this->referer = $referer;
113 $this->backlog = new Backlog($dataDir, $this);
114 $this->elementRevision = new ElementRevision($siteId, $this->getStatefulCacheRevisionFile());
115 $this->healthStatus = HealthStatus::HEALTHY;
116 $this->loadHealthStatus();
117
118 if (empty($userAgent)) {
119 $this->userAgent = !empty($_SERVER['HTTP_USER_AGENT']) ? $_SERVER['HTTP_USER_AGENT'] : 'Mozilla/5.0 (Macintosh; Intel Mac OS X 13_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/112.0.0.0 Safari/537.36';
120 } else {
121 $this->userAgent = $userAgent;
122 }
123
124 $this->loadConfig($siteId, $siteSecret);
125 $this->device = new Device($this->userAgent);
126
127 if(empty($url)) {
128 $host = !empty($_SERVER['HTTP_HOST']) ? $_SERVER['HTTP_HOST'] : "example.com";
129 $uri = !empty($_SERVER['REQUEST_URI']) ? $_SERVER['REQUEST_URI'] : "/";
130 $url = $this->getScheme() . $host . $uri;
131 }
132
133 $queryStr = parse_url($url, PHP_URL_QUERY);
134
135 if ($queryStr) {
136 $this->nitro_parse_str($queryStr, $queryParams);
137
138 if ($queryParams) {
139 if ($this->config->IgnoredParams) {
140 foreach ($this->config->IgnoredParams as $ignorePattern) {
141 $regex = "/^" . self::wildcardToRegex($ignorePattern) . "$/";
142 foreach($queryParams as $paramName => $paramValue) {
143 if (preg_match($regex, $paramName)) {
144 unset($queryParams[$paramName]);
145 }
146 }
147 }
148 }
149
150 ksort($queryParams);
151 $url = str_replace($queryStr, http_build_query($queryParams), $url);
152 }
153 }
154
155 $urlInfo = new Url($url);
156 $this->url = $urlInfo->getNormalized();
157
158 $this->pageCache = new Pagecache($this->url, $this->userAgent, $this->supportedCookiesFilter(self::getCookies()), $this->config->PageCache->SupportedCookies, $this->isAJAXRequest());
159 $this->pageCache->setCookiesProvider([$this, "getPagecacheCookies"]);
160 if ($this->isAJAXRequest() && $this->isAllowedAJAXUrl($this->url) && !empty($_SERVER["HTTP_REFERER"]) && !$this->isAllowedStandaloneAJAXUrl($url)) {
161 $refererInfo = new Url($_SERVER["HTTP_REFERER"]);
162 $this->pageCache->setReferer($refererInfo->getNormalized());
163 }
164 if (!empty($this->config->URLPathVersion)) {
165 $this->pageCache->setUrlPathVersion($this->config->URLPathVersion);
166 }
167
168
169 $this->api = new Api($this->siteId, $siteSecret);
170 $this->api->setBacklog($this->backlog);
171 $this->api->setNitroPack($this);
172
173 $this->pageCache->setDataDir($this->getCacheDir());
174
175 $this->useCompression = false;
176 }
177
178 public function setReferer($referer) {
179 $refererInfo = new Url($referer);
180 $this->referer = $refererInfo->getNormalized();
181 $this->pageCache->setReferer($this->referer);
182 }
183
184 public function getPagecacheCookies() {
185 return $this->supportedCookiesFilter(self::getCookies());
186 }
187
188 public function supportedCookiesFilter($cookies) {
189 $supportedCookies = array();
190 foreach ($cookies as $cookieName=>$cookieValue) {
191 foreach ($this->config->PageCache->SupportedCookies as $cookie) {
192 if (preg_match('/^' . self::wildcardToRegex($cookie) . '$/', $cookieName)) {
193 $supportedCookies[$cookieName] = $cookieValue;
194 }
195 }
196 }
197 return $supportedCookies;
198 }
199
200 public function tagUrl($url, $tag) {
201 if ($this->isAllowedUrl($url)) {
202 return $this->api->tagUrl($url, $tag);
203 } else {
204 return false;
205 }
206 }
207
208 public function setCachePathSuffix($suffix) {
209 $this->cachePathSuffix = $suffix;
210 $this->pageCache->setDataDir($this->getCacheDir());
211 }
212
213 public function enableCompression() {
214 $this->pageCache->enableCompression();
215 }
216
217 public function disableCompression() {
218 $this->pageCache->disableCompression();
219 }
220
221 public function getUrl() {
222 return $this->url;
223 }
224
225 public function getApi() {
226 return $this->api;
227 }
228
229 public function getSiteId() {
230 return $this->siteId;
231 }
232
233 public function getConfig() {
234 return $this->config;
235 }
236
237 public function getCacheDir() {
238 $cachePath = $this->cachePath;
239 array_unshift($cachePath, $this->dataDir);
240 if ($this->cachePathSuffix) {
241 $cachePath[] = $this->cachePathSuffix;
242 }
243 return Filesystem::getOsPath($cachePath);
244 }
245
246 public function getStatefulCacheRevisionFile() {
247 $revisionFile = $this->statefulCacheRevisionsFile;
248 array_unshift($revisionFile, $this->dataDir);
249 return Filesystem::getOsPath($revisionFile);
250 }
251
252 public function getHealthStatus() {
253 return $this->healthStatus;
254 }
255
256 public function getHealthStatusFile() {
257 $healthStatusFile = $this->healthStatusFile;
258 array_unshift($healthStatusFile, $this->dataDir);
259 return Filesystem::getOsPath($healthStatusFile);
260 }
261
262 public function setHealthStatus($status) {
263 $this->healthStatus = $status;
264 Filesystem::filePutContents($this->getHealthStatusFile(), $status);
265 }
266
267 public function loadHealthStatus() {
268 if (defined("NITROPACK_DISABLE_BACKLOG")) return;
269 if (Filesystem::fileExists($this->getHealthStatusFile())) {
270 $this->healthStatus = Filesystem::fileGetContents($this->getHealthStatusFile());
271 } else {
272 $this->healthStatus = HealthStatus::HEALTHY;
273 }
274 }
275
276 public function checkHealthStatus() {
277 try {
278 // TODO: Potentially replace this with a dedicated method in the API
279 $this->fetchConfig(true);
280 $this->setHealthStatus(HealthStatus::HEALTHY);
281 return HealthStatus::HEALTHY;
282 } catch (\Exception $e) {
283 $this->setHealthStatus(HealthStatus::SICK);
284 return HealthStatus::SICK;
285 }
286 }
287
288 public function getTimestampFile() {
289 $timestampFile = $this->timestampFile;
290 array_unshift($timestampFile, $this->dataDir);
291 return Filesystem::getOsPath($timestampFile);
292 }
293
294 public function loadTimeMarks() {
295 if (Filesystem::fileExists($this->getTimestampFile())) {
296 try {
297 $marks = @json_decode(Filesystem::fileGetContents($this->getTimestampFile()), true);
298 if (!$marks) {
299 $marks = [];
300 }
301 } catch (\Exception $e) {
302 $marks = [];
303 }
304 } else {
305 $marks = [];
306 }
307
308 return $marks;
309 }
310
311 public function setTimeMark($name, $time = NULL) {
312 if ($time === NULL) $time = microtime(true);
313 $marks = $this->loadTimeMarks();
314 $marks[$name] = $time;
315 Filesystem::filePutContents($this->getTimestampFile(), json_encode($marks));
316 }
317
318 public function unsetTimeMark($name) {
319 $marks = $this->loadTimeMarks();
320 if (isset($marks[$name])) {
321 unset($marks[$name]);
322 }
323 Filesystem::filePutContents($this->getTimestampFile(), json_encode($marks));
324 }
325
326 public function getTimeMark($name) {
327 $marks = $this->loadTimeMarks();
328 if (!empty($marks[$name])) {
329 return $marks[$name];
330 } else {
331 return NULL;
332 }
333 }
334
335 public function getRemainingCacheTtl() {
336 return $this->pageCache->getRemainingTtl($this->config->PageCache->ExpireTime);
337 }
338
339 public function hasCache($layout = 'default') {
340 if ($this->hasLocalCache()) {
341 return true;
342 } else {
343 return $this->hasRemoteCache($layout);
344 }
345 }
346
347 public function hasLocalCache($checkIfRequestIsAllowed = true) {
348 if ($this->backlog->exists()) return false;
349 if (!$this->isAllowedUrl($this->url) || ($checkIfRequestIsAllowed && !$this->isAllowedRequest())) return false;
350 $cacheRevision = !empty($this->config->RevisionHash) ? $this->config->RevisionHash : NULL;
351 if ($this->getHealthStatus() !== HealthStatus::HEALTHY) {
352 return false;
353 }
354
355 if (!$this->pageCache->getUseInvalidated()) {
356 $ttl = $this->config->PageCache->ExpireTime;
357 } else {
358 $ttl = $this->config->PageCache->StaleExpireTime;
359 }
360
361 return $this->pageCache->hasCache() && !$this->pageCache->hasExpired($ttl, $cacheRevision);
362 }
363
364 public function hasRemoteCache($layout, $checkIfRequestIsAllowed = true) {
365 if ($this->backlog->exists()) return false;
366 if (
367 !$this->isAllowedUrl($this->url) ||
368 ($checkIfRequestIsAllowed && !$this->isAllowedRequest()) ||
369 ($this->pageCache->getParent() && !$this->pageCache->getParent()->hasCache()) ||
370 $this->isPageCacheLocked()
371 ) return false;
372
373 $resp = $this->api->getCache($this->url, $this->userAgent, $this->supportedCookiesFilter(self::getCookies()), $this->isAJAXRequest(), $layout, $this->referer);
374 if ($resp->getStatus() == Api\ResponseStatus::OK) {// We have cache response
375
376 // Check for invalidated cache and delete it if such is found
377 $this->pageCache->useInvalidated(true);
378 if ($this->pageCache->hasCache()) {
379 $path = $this->pageCache->getCachefilePath();
380 Filesystem::deleteFile($path);
381 Filesystem::deleteFile($path . ".gz");
382 Filesystem::deleteFile($path . ".stale");
383 Filesystem::deleteFile($path . ".stale.gz");
384 if (Filesystem::isDirEmpty(dirname($path))) {
385 Filesystem::deleteDir(dirname($path));
386 }
387 }
388 $this->pageCache->useInvalidated(false);
389 // End of check
390
391 list($headers, $content) = Filesystem::explodeByHeaders($resp->getBody());
392 $this->pageCache->setContent($content, $headers);
393 return true;
394 } else {
395 // The goal is to serve cache at all times even when it is slightly outdated. This approach should be ok because new cache has been requested and it should be ready soon
396 if ($this->pageCache->hasCache()) {
397 return true;
398 } else {
399 // Check for invalidated cache
400 $this->pageCache->useInvalidated(true);
401 if ($this->hasLocalCache(false)) {
402 return true;
403 } else {
404 $this->pageCache->useInvalidated(false);
405 }
406 }
407
408 return false;
409 }
410 }
411
412 public function hasRemoteCacheMulti($urls, $layout, $checkIfRequestIsAllowed = true) {
413 if ($this->backlog->exists()) return false;
414
415 foreach ($urls as $url) {
416 if (
417 !$this->isAllowedUrl($url) ||
418 ($checkIfRequestIsAllowed && !$this->isAllowedRequest()) ||
419 ($this->pageCache->getParent() && !$this->pageCache->getParent()->hasCache()) || // This check is works correctly despite the fact that it doesn't check the current iteration's url because the check is based on headers and all of the headers for the URLs are the same for batched webhooks
420 $this->isPageCacheLocked()
421 ) return false;
422 }
423
424 $hasCache = true; // hasCache is false if even one of the URLs doesn't have a cache
425 $responses = $this->api->getCacheMulti($urls, $this->userAgent, $this->supportedCookiesFilter(self::getCookies()), $this->isAJAXRequest(), $layout, $this->referer);
426
427 $originalURL = $this->url;
428 $originalPageCache = $this->pageCache;
429
430 foreach ($responses as $url => $resp) {
431 // Overwrite the url
432 $urlInfo = new Url($url);
433 $this->url = $urlInfo->getNormalized();
434
435 // Overwrite the page cache
436 $this->pageCache = new Pagecache($this->url, $this->userAgent, $this->supportedCookiesFilter(self::getCookies()), $this->config->PageCache->SupportedCookies, $this->isAJAXRequest());
437 $this->pageCache->setCookiesProvider([$this, "getPagecacheCookies"]);
438 if ($this->isAJAXRequest() && $this->isAllowedAJAXUrl($this->url) && !empty($_SERVER["HTTP_REFERER"]) && !$this->isAllowedStandaloneAJAXUrl($url)) {
439 $refererInfo = new Url($_SERVER["HTTP_REFERER"]);
440 $this->pageCache->setReferer($refererInfo->getNormalized());
441 }
442 if (!empty($this->config->URLPathVersion)) {
443 $this->pageCache->setUrlPathVersion($this->config->URLPathVersion);
444 }
445 $this->pageCache->setDataDir($this->getCacheDir());
446
447 // Process the response
448 if ($resp->getStatus() == Api\ResponseStatus::OK) {// We have cache response
449
450 // Check for invalidated cache and delete it if such is found
451 $this->pageCache->useInvalidated(true);
452 if ($this->pageCache->hasCache()) {
453 $path = $this->pageCache->getCachefilePath();
454 Filesystem::deleteFile($path);
455 Filesystem::deleteFile($path . ".gz");
456 Filesystem::deleteFile($path . ".stale");
457 Filesystem::deleteFile($path . ".stale.gz");
458 if (Filesystem::isDirEmpty(dirname($path))) {
459 Filesystem::deleteDir(dirname($path));
460 }
461 }
462 $this->pageCache->useInvalidated(false);
463 // End of check
464
465 list($headers, $content) = Filesystem::explodeByHeaders($resp->getBody());
466 $this->pageCache->setContent($content, $headers);
467 continue;
468 } else {
469 // The goal is to serve cache at all times even when it is slightly outdated. This approach should be ok because new cache has been requested and it should be ready soon
470 if ($this->pageCache->hasCache()) {
471 continue;
472 } else {
473 // Check for invalidated cache
474 $this->pageCache->useInvalidated(true);
475 if ($this->hasLocalCache(false)) {
476 continue;
477 } else {
478 $this->pageCache->useInvalidated(false);
479 }
480 }
481
482 $hasCache = false;
483 continue;
484 }
485 }
486
487 // Restore the url and page cache
488 $this->url = $originalURL;
489 $this->pageCache = $originalPageCache;
490
491 return $hasCache;
492 }
493
494 public function invalidateCache($url = NULL, $tag = NULL, $reason = NULL) {
495 return $this->purgeCache($url, $tag, PurgeType::INVALIDATE | PurgeType::PAGECACHE_ONLY, $reason);
496 }
497
498 public function clearPageCache($reason = NULL) {
499 return $this->purgeCache(NULL, NULL, PurgeType::PAGECACHE_ONLY, $reason);
500 }
501
502 public function purgeCache($url = NULL, $tag = NULL, $purgeType = PurgeType::COMPLETE, $reason = NULL) {
503 @set_time_limit(0);
504 $this->lockPageCache(); // Set the page cache lock, expires after self::PAGECACHE_LOCK_EXPIRATION_TIME seconds
505
506 try {
507 $invalidate = !!($purgeType & PurgeType::INVALIDATE);
508 $pageCacheOnly = !!($purgeType & PurgeType::PAGECACHE_ONLY);
509 $lightPurge = !!($purgeType & PurgeType::LIGHT_PURGE);
510
511 if ($url || $tag) {
512 $localResult = true;
513 $apiResult = true;
514 if ($url) {
515 if (is_array($url)) {
516 foreach ($url as &$urlLink) {
517 $urlLink = $this->normalizeUrl($urlLink);
518 if ($invalidate) {
519 $localResult &= $this->invalidateLocalUrlCache($urlLink);
520 } else {
521 $localResult &= $this->purgeLocalUrlCache($urlLink);
522 }
523 }
524 } else {
525 $url = $this->normalizeUrl($url);
526 if ($invalidate) {
527 $localResult &= $this->invalidateLocalUrlCache($url);
528 } else {
529 $localResult &= $this->purgeLocalUrlCache($url);
530 }
531 }
532
533 try {
534 $apiResult &= $this->api->purgeCache($url, false, $reason, $lightPurge);
535 } catch (ServiceDownException $e) {
536 $apiResult = false;
537 // TODO: Potentially log this
538 }
539 }
540
541 if ($tag) {
542 $attemptsLeft = 10;
543 $purgedUrls = array();
544 do {
545 $hadError = false;
546
547 try {
548 $purgedUrls = $this->api->purgeCacheByTag($tag, $reason);
549
550 foreach ($purgedUrls as $url) {
551 if ($invalidate) {
552 $localResult &= $this->invalidateLocalUrlCache($url);
553 } else {
554 $localResult &= $this->purgeLocalUrlCache($url);
555 }
556 }
557 } catch (ServiceDownException $e) {
558 $this->purgeLocalCache(true); // TODO: This will leave stale cache files. Think of a way to delete them on systems that do not have a heartbeat (i.e custom integrations).
559 $apiResult = false;
560 // TODO: Log this
561 break;
562 } catch (\Exception $e) {
563 $hadError = true;
564 $attemptsLeft--;
565 sleep(3);
566 }
567 } while ($attemptsLeft > 0 && count($purgedUrls) > 0);
568 }
569 } else {
570 if ($invalidate) {
571 $localResult = $this->invalidateLocalCache();
572 $apiResult = $this->api->purgeCache(NULL, $pageCacheOnly, $reason, $lightPurge); // delete only page cache
573 } else {
574 $staleCacheDir = $this->purgeLocalCache(true);
575
576 // Call the cache purge method
577 $apiResult = $this->api->purgeCache(NULL, $pageCacheOnly, $reason, $lightPurge);
578
579 // Finally, delete the files of the stale directory
580 Filesystem::deleteDir($staleCacheDir);
581
582 $localResult = true; // We do not care if $staleCacheDir was not deleted successfully
583 }
584
585 $this->elementRevision->refresh();
586 }
587
588 $this->unlockPageCache(); // Purge cache is done, we can now unlock
589 } catch (\Exception $e) {
590 $this->unlockPageCache(); // Purge cache had an error, so just unlock
591
592 throw $e;
593 }
594
595 return $apiResult && $localResult;
596 }
597
598 public function purgeLocalCache($quick = false) {
599 $staleCacheDir = $this->getCacheDir() . '.stale.' . md5(microtime(true));
600 $staleCacheDirSuffix = "";
601 $counter = 0;
602 while (Filesystem::fileExists($staleCacheDir . $staleCacheDirSuffix)) {
603 $counter++;
604 $staleCacheDirSuffix = "_" . $counter;
605 }
606 $staleCacheDir .= $staleCacheDirSuffix;
607 $this->purgeProxyCache();
608 $this->config->LastFetch = 0;
609 $this->setConfig($this->config);
610
611 // Rename cache files directory
612 if (Filesystem::fileExists($this->getCacheDir()) && !Filesystem::rename($this->getCacheDir(), $staleCacheDir)) {
613 throw new \Exception("No write permissions to rename the directory: " . $this->getCacheDir());
614 }
615
616 // Create a new empty directory
617 if (!Filesystem::createDir($this->getCacheDir())) {
618 throw new \Exception("No write permissions to create the directory: " . $this->getCacheDir());
619 }
620
621 if (!$quick) {
622 // Finally, delete the files of the stale directory
623 Filesystem::deleteDir($staleCacheDir);
624 }
625
626 $this->elementRevision->refresh();
627
628 return $staleCacheDir;
629 }
630
631 public function fetchConfig($ignoreHealthStatus = false) {
632 // TODO: Record failures and repeat with a delay
633 $fetcher = new Api\RemoteConfigFetcher($this->siteId, $this->siteSecret);
634
635 if (!$ignoreHealthStatus) {
636 $fetcher->setBacklog($this->backlog);
637 $fetcher->setNitroPack($this);
638 }
639
640 try {
641 $configContents = $fetcher->get(); // this can throw in case of http errors or validation failures
642 } catch (\Exception $e) {
643 // TODO: Record this failure, possibly in the backlog
644 throw $e;
645 }
646
647 $config = json_decode($configContents);
648 if ($config) {
649 $config->SDKVersion = NitroPack::VERSION;
650 $config->LastFetch = time();
651
652 $this->setConfig($config);
653 return true;
654 } else {
655 throw new EmptyConfigException("Config response was empty");
656 }
657 }
658
659 public function setConfig($config) {
660 $file = $this->getConfigFile();
661 if (Filesystem::createDir(dirname($file))) {
662 if (Filesystem::filePutContents($file, json_encode($config))) {
663 return true;
664 } else {
665 throw new StorageException(sprintf("Config file %s cannot be saved to disk", $file));
666 }
667 } else {
668 throw new StorageException(sprintf("Storage directory %s cannot be created", dirname($file)));
669 }
670 }
671
672 public function setVarnishProxyCacheHeaders($newHeaders) {
673 $this->varnishProxyCacheHeaders = $newHeaders;
674 }
675
676 public function purgeProxyCache($url = NULL) {
677 if (!empty($this->config->CacheIntegrations)) {
678 if (!empty($this->config->CacheIntegrations->Varnish)) {
679 if ($url) {
680 $url = $this->normalizeUrl($url);
681 $varnish = new Integrations\Varnish(
682 $this->config->CacheIntegrations->Varnish->Servers,
683 $this->config->CacheIntegrations->Varnish->PurgeSingleMethod,
684 $this->varnishProxyCacheHeaders
685 );
686 $varnish->purge($url);
687 } else {
688 $varnish = new Integrations\Varnish(
689 $this->config->CacheIntegrations->Varnish->Servers,
690 $this->config->CacheIntegrations->Varnish->PurgeAllMethod,
691 $this->varnishProxyCacheHeaders
692 );
693 $varnish->purge($this->config->CacheIntegrations->Varnish->PurgeAllUrl);
694 }
695 }
696
697 //if (!empty($this->config->CacheIntegrations->LiteSpeed) && php_sapi_name() !== "cli") {
698 // if ($url) {
699 // $urlObj = new Url($url);
700 // $liteSpeedPath = $urlObj->getPath();
701 // if ($urlObj->getQuery()) {
702 // $liteSpeedPath .= "?" . $urlObj->getQuery();
703 // }
704 // header("X-LiteSpeed-Purge: $liteSpeedPath", false);
705 // } else {
706 // header("X-LiteSpeed-Purge: *", false);
707 // }
708 //}
709 }
710 }
711
712 public function isAllowedUrl($url) {
713 if (strpos($url, 'sucurianticache=') !== false) return false;
714
715 if ($this->config->EnabledURLs->Status) {
716 if (!empty($this->config->EnabledURLs->URLs)) {
717 foreach ($this->config->EnabledURLs->URLs as $enabledUrl) {
718 $enabledUrlModified = preg_replace("/^(https?:)?\/\//", "*", $enabledUrl);
719
720 if (preg_match('/^' . self::wildcardToRegex($enabledUrlModified) . '$/', $url)) {
721 return true;
722 }
723 }
724
725 return false;
726 }
727 } else if ($this->config->DisabledURLs->Status) {
728 if (!empty($this->config->DisabledURLs->URLs)) {
729 foreach ($this->config->DisabledURLs->URLs as $disabledUrl) {
730 $disabledUrlModified = preg_replace("/^(https?:)?\/\//", "*", $disabledUrl);
731
732 if (preg_match('/^' . self::wildcardToRegex($disabledUrlModified) . '$/', $url)) {
733 return false; // don't cache disabled URLs
734 }
735 }
736 }
737 }
738
739 return true;
740 }
741
742 public function isAllowedRequest($allowServiceRequests = false) {
743 if (($this->isAJAXRequest() && !$this->isAllowedAJAX()) || !($this->isRequestMethod("GET") || $this->isRequestMethod("HEAD"))) {// TODO: Allow URLs which match a pattern in the AJAX URL whitelist
744 return false; // don't cache ajax or not GET requests
745 }
746
747 if (!$allowServiceRequests && isset($_SERVER["HTTP_X_NITROPACK_REQUEST"])) { // Skip requests coming from NitroPack
748 return false;
749 }
750
751 if (isset($_GET["nonitro"])) { // Skip requests having ?nonitro
752 return false;
753 }
754
755 if (!$this->isAllowedBrowser()) {
756 return false;
757 }
758
759 if (isset($this->config->ExcludedCookies) && $this->config->ExcludedCookies->Status) {
760 foreach ($this->config->ExcludedCookies->Cookies as $cookieExclude) {
761 foreach (self::getCookies() as $cookieName => $cookieValue) {
762 if (preg_match('/^' . self::wildcardToRegex($cookieExclude->name) . '$/', $cookieName)) {
763 if (count($cookieExclude->values) == 0) {
764 return false; // no excluded cookie values entered, reject all values
765 } else {
766 foreach ($cookieExclude->values as $val) {
767 if (preg_match('/^' . self::wildcardToRegex($val) . '$/', $cookieValue)) {
768 return false;
769 }
770 }
771 }
772 }
773 }
774 }
775 }
776
777 return true;
778 }
779
780 public function isAllowedBrowser() {
781 if (empty($_SERVER["HTTP_USER_AGENT"])) return true;
782
783 if (preg_match("~MSIE|Internet Explorer~i", $_SERVER["HTTP_USER_AGENT"]) || strpos($_SERVER["HTTP_USER_AGENT"], "Trident/7.0; rv:11.0") !== false) { // Skip IE
784 return false;
785 }
786
787 return true;
788 }
789
790 public function getScheme() {
791 return $this->isSecure() ? 'https://' : 'http://';
792 }
793
794 public function isSecure() {
795 $result = (!empty($_SERVER['HTTP_X_FORWARDED_PROTO']) && in_array("https", array_map("strtolower", array_map("trim", explode(",", $_SERVER['HTTP_X_FORWARDED_PROTO']))))) ||
796 (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ||
797 (!empty($_SERVER['SERVER_PORT']) && $_SERVER['SERVER_PORT'] == 443) ||
798 (!empty($_SERVER['HTTP_SSL_FLAG']) && $_SERVER['HTTP_SSL_FLAG'] == 'SSL');
799
800 if (!$result && !empty($_SERVER['HTTP_CF_VISITOR'])) {
801 $visitor = json_decode($_SERVER['HTTP_CF_VISITOR']);
802 $result = $visitor && property_exists($visitor, "scheme") && strtolower($visitor->scheme) == "https";
803 }
804
805 return $result;
806 }
807
808 public function isAJAXRequest() {
809 return
810 (!empty($_SERVER['HTTP_X_REQUESTED_WITH']) && strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) == 'xmlhttprequest') ||
811 $this->isAllowedAJAXUrl($this->url) ||
812 $this->isAllowedStandaloneAJAXUrl($this->url);
813 }
814
815 public function isRequestMethod($method) {
816 return empty($_SERVER['REQUEST_METHOD']) || $_SERVER['REQUEST_METHOD'] == $method;
817 }
818
819 public function isAllowedAJAX() {
820 if (!$this->isAllowedStandaloneAJAXUrl($this->url)) {
821 if (!$this->pageCache->getParent()) return false;
822 if (!$this->pageCache->getParent()->hasCache() || $this->pageCache->getParent()->hasExpired($this->config->PageCache->ExpireTime)) return false;
823 }
824 return true;
825 }
826
827 public function isAllowedAJAXUrl($url) {
828 if ($this->config->AjaxURLs->Status) {
829 if (!empty($this->config->AjaxURLs->URLs)) {
830 foreach ($this->config->AjaxURLs->URLs as $ajaxUrl) {
831 $ajaxUrlModified = preg_replace("/^(https?:)?\/\//", "*", $ajaxUrl);
832 if (preg_match('/^' . self::wildcardToRegex($ajaxUrlModified) . '$/', $url)) {
833 return true;
834 }
835 }
836 return false;
837 }
838 }
839 return false;
840 }
841
842 public function isAllowedStandaloneAJAXUrl($url)
843 {
844 if ($this->config->AjaxURLs->Status) {
845 if (!empty($this->config->AjaxURLs->StandaloneURLs)) {
846 foreach ($this->config->AjaxURLs->StandaloneURLs as $ajaxUrl) {
847 $ajaxUrlModified = preg_replace("/^(https?:)?\/\//", "*", $ajaxUrl);
848 if (preg_match('/^' . self::wildcardToRegex($ajaxUrlModified) . '$/', $url)) {
849 return true;
850 }
851 }
852 }
853 }
854 return false;
855 }
856
857 public function isCacheAllowed() {
858 return $this->isAllowedRequest() && $this->isAllowedUrl($this->url);
859 }
860
861 public function isStatefulCacheSatisfied($type = NULL) {
862 if ($this->config->StatefulCache->Status) {
863 $foundTypeSelectors = false;
864 foreach ($this->config->StatefulCache->Selectors as $selector) {
865 if ($type !== NULL) {
866 if ($selector->type === $type) {
867 $foundTypeSelectors = true;
868 } else {
869 continue;
870 }
871 }
872
873 $selectorEncoded = str_replace("=", "", base64_encode($selector->string)); // base64 can produce == at the end which breaks is invalid for a cookie name, hence why we need to remove it
874 $cookieKey = 'np-' . $selector->type . '-' . $selectorEncoded . '-override';
875 if (empty($_COOKIE[$cookieKey]) || $_COOKIE[$cookieKey] != $this->elementRevision->get()) {
876 return false;
877 }
878 }
879
880 return $type !== NULL ? $foundTypeSelectors : true;
881 }
882
883 return false;
884 }
885
886 public function purgeLocalUrlCache($url) {
887 $url = $this->normalizeUrl($url);
888 $this->purgeProxyCache($url);
889 $localResult = true;
890 $cacheDir = $this->getCacheDir();
891 $knownDeviceTypes = Device::getKnownTypes();
892 foreach ($knownDeviceTypes as $deviceType) {
893 $urlDir = PageCache::getUrlDir($cacheDir . "/" . $deviceType, $url, false, $this->config->URLPathVersion);
894 $invalidatedUrlDir = PageCache::getUrlDir($cacheDir . "/" . $deviceType, $url, true, $this->config->URLPathVersion);
895 $localResult &= Filesystem::deleteDir($urlDir);
896 $localResult &= Filesystem::deleteDir($invalidatedUrlDir);
897 }
898 return $localResult;
899 }
900
901 public function invalidateLocalUrlCache($url) {
902 $url = $this->normalizeUrl($url);
903 $this->purgeProxyCache($url);
904 $localResult = true;
905 $cacheDir = $this->getCacheDir();
906 $knownDeviceTypes = Device::getKnownTypes();
907 foreach ($knownDeviceTypes as $deviceType) {
908 $urlDir = PageCache::getUrlDir($cacheDir . "/" . $deviceType, $url, false, $this->config->URLPathVersion);
909 $urlDirInvalid = PageCache::getUrlDir($cacheDir . "/" . $deviceType, $url, true, $this->config->URLPathVersion);
910
911 $this->invalidateDir($urlDir, $urlDirInvalid);
912 }
913 return $localResult;
914 }
915
916 public function invalidateLocalCache() {
917 $this->purgeProxyCache();
918 $this->config->LastFetch = 0;
919 $this->setConfig($this->config);
920
921 $cacheDir = $this->getCacheDir();
922 $knownDeviceTypes = Device::getKnownTypes();
923 foreach ($knownDeviceTypes as $deviceType) {
924 $deviceTypeDir = $cacheDir . "/" . $deviceType;
925 Filesystem::dirForeach($deviceTypeDir, function($urlDir) {
926 if (substr($urlDir, -2) !== "_i") {
927 $this->invalidateDir($urlDir, $urlDir . "_i");
928 }
929 });
930 }
931 return true;
932 }
933
934 private function invalidateDir($urlDir, $urlDirInvalid) {
935 if (Filesystem::fileExists($urlDirInvalid)) {
936 Filesystem::dirForeach($urlDir, function($file) use ($urlDirInvalid) {
937 Filesystem::rename($file, $urlDirInvalid . "/" . basename($file));
938 });
939
940 Filesystem::deleteDir($urlDir);
941 } else {
942 Filesystem::rename($urlDir, $urlDirInvalid);
943 }
944 Filesystem::touch($urlDirInvalid);
945 }
946
947 public function integrationUrl($widget, $version = null) {
948 $integration = new IntegrationUrl($widget, $this->siteId, $this->siteSecret, $version);
949
950 return $integration->getUrl();
951 }
952
953 public function embedJsUrl() {
954 $embedjs = new Embedjs();
955
956 return $embedjs->getUrl();
957 }
958
959 public function enableSafeMode() {
960 $this->api->safe_mode->enable();
961 $this->fetchConfig();
962 }
963
964 public function disableSafeMode() {
965 $this->api->safe_mode->disable();
966 $this->fetchConfig();
967 }
968
969 public function enableCartCache() {
970 $this->api->enableCartCache();
971 }
972
973 public function disableCartCache() {
974 $this->api->disableCartCache();
975 }
976
977 public function getStatefulCacheHandlerScript() {
978 if ($this->config->StatefulCache->Status && $this->config->StatefulCache->HandlerScript) {
979 $keyRevision = $this->elementRevision->get();
980 return '<script id="nitro-stateful-cache" nitro-exclude>' . str_replace("KEY_REVISION", $keyRevision, $this->config->StatefulCache->HandlerScript) . '</script>';
981 }
982
983 return "";
984 }
985
986 private function loadConfig() {
987 $file = $this->getConfigFile();
988
989 $config = array();
990 if (Filesystem::fileExists($file) || $this->fetchConfig(true)) {
991 $config = json_decode(Filesystem::fileGetContents($file));
992 if (empty($config->SDKVersion) || $config->SDKVersion !== NitroPack::VERSION || empty($config->LastFetch) || time() - $config->LastFetch >= $this->configTTL) {
993 if ($this->getHealthStatus() === HealthStatus::HEALTHY) {
994 if ($this->fetchConfig()) {
995 $config = json_decode(Filesystem::fileGetContents($file));
996 } else {
997 throw new NoConfigException("Can't load config file");
998 }
999 }
1000 }
1001 $this->config = $config;
1002 } else {
1003 throw new NoConfigException("Can't load config file");
1004 }
1005 }
1006
1007 private function getConfigFile() {
1008 $configFile = $this->configFile;
1009
1010 $filename = array_pop($configFile);
1011
1012 $filename = $this->siteId . '-' . $filename;
1013
1014 array_push($configFile, $filename);
1015 array_unshift($configFile, $this->dataDir);
1016
1017 return Filesystem::getOsPath($configFile);
1018 }
1019
1020 private function lockPageCache() {
1021 $filename = $this->getPageCacheLockFilename();
1022
1023 if (Filesystem::fileExists($filename)) {
1024 $sem = 1 + (int)Filesystem::fileGetContents($filename);
1025 } else {
1026 $sem = 1;
1027 }
1028
1029 return !!Filesystem::filePutContents($filename, $sem);
1030 }
1031
1032 private function unlockPageCache() {
1033 $filename = $this->getPageCacheLockFilename();
1034
1035 if (Filesystem::fileExists($filename)) {
1036 $sem = (int)Filesystem::fileGetContents($filename);
1037
1038 $sem--;
1039
1040 if ($sem <= 0) {
1041 return !!Filesystem::deleteFile($filename);
1042 } else {
1043 return !!Filesystem::filePutContents($filename, $sem);
1044 }
1045 }
1046
1047 return false;
1048 }
1049
1050 private function isPageCacheLocked() {
1051 $filename = $this->getPageCacheLockFilename();
1052
1053 if (!Filesystem::fileExists($filename)) {
1054 return false;
1055 } else {
1056 if (time() - Filesystem::fileMTime($filename) <= self::PAGECACHE_LOCK_EXPIRATION_TIME) {
1057 return true;
1058 } else {
1059 Filesystem::deleteFile($filename);
1060
1061 return false;
1062 }
1063 }
1064
1065 // We should never get here, so consider this a default return value in case of future changes
1066 return false;
1067 }
1068
1069 private function getPageCacheLockFilename() {
1070 $pageCacheLockFile = $this->pageCacheLockFile;
1071 array_unshift($pageCacheLockFile, $this->dataDir);
1072 return Filesystem::getOsPath($pageCacheLockFile);
1073 }
1074
1075 private function normalizeUrl($url) {
1076 $urlObj = new Url($url);
1077 return $urlObj->getNormalized();
1078 }
1079 }
1080