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