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