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