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