PluginProbe ʕ •ᴥ•ʔ
NitroPack – Performance, Page Speed & Cache Plugin for Core Web Vitals, CDN & Image Optimization / 1.17.7
NitroPack – Performance, Page Speed & Cache Plugin for Core Web Vitals, CDN & Image Optimization v1.17.7
1.19.8 1.19.7 1.19.6 1.19.5 trunk 1.10.0 1.10.1 1.10.2 1.10.3 1.10.4 1.11.0 1.12.0 1.13.0 1.14.0 1.15.0 1.15.1 1.15.2 1.15.3 1.16.0 1.16.1 1.16.2 1.16.3 1.16.4 1.16.5 1.16.6 1.16.7 1.16.8 1.17.0 1.17.6 1.17.7 1.17.8 1.17.9 1.18.0 1.18.1 1.18.2 1.18.3 1.18.4 1.18.5 1.18.6 1.18.7 1.18.8 1.18.9 1.19.0 1.19.1 1.19.2 1.19.3 1.19.4 1.3.19 1.3.20 1.4.0 1.4.1 1.5.0 1.5.1 1.5.10 1.5.11 1.5.12 1.5.13 1.5.14 1.5.15 1.5.16 1.5.17 1.5.18 1.5.19 1.5.2 1.5.3 1.5.4 1.5.5 1.5.6 1.5.7 1.5.8 1.5.9 1.6.0 1.6.1 1.7.0 1.7.1 1.8.0 1.8.1 1.8.3 1.9.0 1.9.1 1.9.2
nitropack / nitropack-sdk / NitroPack / SDK / NitroPack.php
nitropack / nitropack-sdk / NitroPack / SDK Last commit date
Api 2 years ago Integrations 2 years ago StorageDriver 4 years ago Url 2 years ago Utils 2 years ago data 6 years ago Api.php 2 years ago Backlog.php 4 years ago BacklogReplayTimeoutException.php 4 years ago ChallengeProcessingException.php 4 years ago ChallengeVerificationException.php 4 years ago ConfigFetcherException.php 4 years ago Crypto.php 3 years ago Device.php 6 years ago DeviceType.php 6 years ago ElementRevision.php 3 years ago EmptyConfigException.php 6 years ago ExcludeEntry.php 2 years ago ExcludeOperationType.php 2 years ago FileHandle.php 5 years ago Filesystem.php 4 years ago HealthStatus.php 5 years ago IntegrationUrl.php 2 years ago NitroPack.php 1 year ago NoConfigException.php 5 years ago Pagecache.php 2 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
1227 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.56.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 getExternalCustomPrefix() {
84 return !empty($_SERVER["HTTP_X_NITRO_CACHE_PREFIX"]) ? $_SERVER["HTTP_X_NITRO_CACHE_PREFIX"] : NULL;
85 }
86
87 public static function addCustomCachePrefix($prefix = "") {
88 if (!in_array($prefix, self::$cachePrefixes)) {
89 self::$cachePrefixes[] = $prefix;
90 }
91 }
92
93 public static function getCustomCachePrefix() {
94 return implode("-", self::$cachePrefixes);
95 }
96
97 public static function wildcardToRegex($str, $delim = "/") {
98 return implode(".*?", array_map(function($input) use ($delim) { return preg_quote($input, $delim); }, explode("*", $str)));
99 }
100
101 private function nitro_parse_str($qStr, &$resArr) {
102 if (strpos($qStr, '?') !== false) {
103 $tmpArr = explode('?', $qStr, 2);
104 parse_str($tmpArr[0], $resArr);
105 $completeValue = end($resArr) . $tmpArr[1];
106 $resArr[key($resArr)] = $completeValue;
107 reset($resArr);
108 } else {
109 parse_str($qStr, $resArr);
110 }
111 }
112
113 public function __construct($siteId, $siteSecret, $userAgent = NULL, $url = NULL, $dataDir = __DIR__, $referer = NULL) {
114 $this->configTTL = 3600;
115 $this->siteId = $siteId;
116 $this->siteSecret = $siteSecret;
117 $this->dataDir = $dataDir;
118 $this->referer = $referer;
119 $this->backlog = new Backlog($dataDir, $this);
120 $this->elementRevision = new ElementRevision($siteId, $this->getStatefulCacheRevisionFile());
121 $this->healthStatus = HealthStatus::HEALTHY;
122 $this->loadHealthStatus();
123
124 if (empty($userAgent)) {
125 $this->userAgent = !empty($_SERVER['HTTP_USER_AGENT']) ? $_SERVER['HTTP_USER_AGENT'] : 'Mozilla/5.0 (Macintosh; Intel Mac OS X 13_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/112.0.0.0 Safari/537.36';
126 } else {
127 $this->userAgent = $userAgent;
128 }
129
130 $this->loadConfig($siteId, $siteSecret);
131 $this->device = new Device($this->userAgent);
132
133 if(empty($url)) {
134 $host = !empty($_SERVER['HTTP_HOST']) ? $_SERVER['HTTP_HOST'] : "example.com";
135 $uri = !empty($_SERVER['REQUEST_URI']) ? $_SERVER['REQUEST_URI'] : "/";
136 $url = $this->getScheme() . $host . $uri;
137 }
138
139 $queryStr = parse_url($url, PHP_URL_QUERY);
140
141 if ($queryStr) {
142 $this->nitro_parse_str($queryStr, $queryParams);
143
144 if ($queryParams) {
145 if ($this->config->IgnoredParams) {
146 foreach ($this->config->IgnoredParams as $ignorePattern) {
147 $regex = "/^" . self::wildcardToRegex($ignorePattern) . "$/";
148 foreach($queryParams as $paramName => $paramValue) {
149 if (preg_match($regex, $paramName)) {
150 unset($queryParams[$paramName]);
151 }
152 }
153 }
154 }
155
156 ksort($queryParams);
157 $url = str_replace($queryStr, http_build_query($queryParams), $url);
158 }
159 }
160
161 $urlInfo = new Url($url);
162 $this->url = $urlInfo->getNormalized();
163
164 $this->pageCache = new Pagecache($this->url, $this->userAgent, $this->supportedCookiesFilter(self::getCookies()), $this->config->PageCache->SupportedCookies, $this->isAJAXRequest());
165 $this->pageCache->setCookiesProvider([$this, "getPagecacheCookies"]);
166
167 if ($this->config->PageCache->Geot->Status) {
168 $this->pageCache->setGeotVariations($this->config->PageCache->Geot->Variations);
169 }
170
171 if ($this->isAJAXRequest() && $this->isAllowedAJAXUrl($this->url) && !empty($_SERVER["HTTP_REFERER"]) && !$this->isAllowedStandaloneAJAXUrl($url)) {
172 $refererInfo = new Url($_SERVER["HTTP_REFERER"]);
173 $this->pageCache->setReferer($refererInfo->getNormalized());
174 }
175
176 if ($this->isServiceRequest()) {
177 $customPrefix = self::getExternalCustomPrefix();
178 if ($customPrefix) {
179 self::addCustomCachePrefix($customPrefix);
180 }
181 } else {
182 if (!!$this->config->LanguageVary->Status) {
183 $lang = $this->config->LanguageVary->DefaultLanguage;
184 $clientLanguagePreference = !empty($_SERVER["HTTP_ACCEPT_LANGUAGE"]) ? $_SERVER["HTTP_ACCEPT_LANGUAGE"] : "";
185 $languagePreferences = $this->parseLanguagePreferences($clientLanguagePreference);
186 $possibilities = [];
187 foreach ($languagePreferences as $pref) {
188 list($langCode, $langVariant, $weight) = $pref;
189
190 if ($langCode === "*") {
191 $langTries = [$this->config->LanguageVary->DefaultLanguage];
192 } else {
193 $langTries = ["$langCode-$langVariant", $langCode];
194 }
195
196 foreach ($langTries as $langTry) {
197 if (in_array($langTry, $this->config->LanguageVary->AvailableLanguages)) {
198 $possibilities[] = [$langTry, $weight];
199 }
200 }
201 }
202
203 if (!empty($possibilities)) {
204 usort($possibilities, function($a, $b) {
205 return $a[1] < $b[1] ? 1 : -1;
206 });
207 $lang = $possibilities[0][0];
208 }
209
210 self::addCustomCachePrefix("lang_$lang");
211 }
212 }
213
214 if (!empty($this->config->URLPathVersion)) {
215 $this->pageCache->setUrlPathVersion($this->config->URLPathVersion);
216 }
217
218
219 $this->api = new Api($this->siteId, $siteSecret);
220 $this->api->setBacklog($this->backlog);
221 $this->api->setNitroPack($this);
222
223 $this->pageCache->setDataDir($this->getCacheDir());
224
225 $this->useCompression = false;
226 }
227
228 private function parseLanguagePreferences($languagePreferences) {
229 $preferenceParts = array_filter(array_map("trim", explode(",", $languagePreferences)));
230 $preferences = [];
231 foreach ($preferenceParts as $lang) {
232 // Parse variants of the form en-US;q=0.8, en-US, en;q=0.8, en, *;q=0.8, *
233 if (preg_match("/^(?<code>[a-z\*]+)(-(?<variant>[A-Z]+))?(;q=(?<weight>[\d\.]+))?$/", $lang, $matches)) {
234 $langCode = $matches["code"];
235 $langVariant = !empty($matches["variant"]) ? $matches["variant"] : "";
236 $weight = !empty($matches["weight"]) ? (float)$matches["weight"] : 1;
237 $preferences[] = [$langCode, $langVariant, $weight];
238 }
239 }
240
241 return $preferences;
242 }
243
244 public function setReferer($referer) {
245 $refererInfo = new Url($referer);
246 $this->referer = $refererInfo->getNormalized();
247 $this->pageCache->setReferer($this->referer);
248 }
249
250 public function getPagecacheCookies() {
251 return $this->supportedCookiesFilter(self::getCookies());
252 }
253
254 public function supportedCookiesFilter($cookies) {
255 $supportedCookies = array();
256 foreach ($cookies as $cookieName=>$cookieValue) {
257 foreach ($this->config->PageCache->SupportedCookies as $cookie) {
258 if (preg_match('/^' . self::wildcardToRegex($cookie) . '$/', $cookieName)) {
259 $supportedCookies[$cookieName] = $cookieValue;
260 }
261 }
262 }
263 return $supportedCookies;
264 }
265
266 public function tagUrl($url, $tag) {
267 if ($this->isAllowedUrl($url)) {
268 return $this->api->tagUrl($url, $tag);
269 } else {
270 return false;
271 }
272 }
273
274 public function setCachePathSuffix($suffix) {
275 $this->cachePathSuffix = $suffix;
276 $this->pageCache->setDataDir($this->getCacheDir());
277 }
278
279 public function enableCompression() {
280 $this->pageCache->enableCompression();
281 }
282
283 public function disableCompression() {
284 $this->pageCache->disableCompression();
285 }
286
287 public function getUrl() {
288 return $this->url;
289 }
290
291 public function getApi() {
292 return $this->api;
293 }
294
295 public function getSiteId() {
296 return $this->siteId;
297 }
298
299 public function getConfig() {
300 return $this->config;
301 }
302
303 public function getCacheDir() {
304 $cachePath = $this->cachePath;
305 array_unshift($cachePath, $this->dataDir);
306 if ($this->cachePathSuffix) {
307 $cachePath[] = $this->cachePathSuffix;
308 }
309 return Filesystem::getOsPath($cachePath);
310 }
311
312 public function getStatefulCacheRevisionFile() {
313 $revisionFile = $this->statefulCacheRevisionsFile;
314 array_unshift($revisionFile, $this->dataDir);
315 return Filesystem::getOsPath($revisionFile);
316 }
317
318 public function getHealthStatus() {
319 return $this->healthStatus;
320 }
321
322 public function getHealthStatusFile() {
323 $healthStatusFile = $this->healthStatusFile;
324 array_unshift($healthStatusFile, $this->dataDir);
325 return Filesystem::getOsPath($healthStatusFile);
326 }
327
328 public function setHealthStatus($status) {
329 $this->healthStatus = $status;
330 Filesystem::filePutContents($this->getHealthStatusFile(), $status);
331 }
332
333 public function loadHealthStatus() {
334 if (defined("NITROPACK_DISABLE_BACKLOG")) return;
335 if (Filesystem::fileExists($this->getHealthStatusFile())) {
336 $this->healthStatus = Filesystem::fileGetContents($this->getHealthStatusFile());
337 } else {
338 $this->healthStatus = HealthStatus::HEALTHY;
339 }
340 }
341
342 public function checkHealthStatus() {
343 try {
344 // TODO: Potentially replace this with a dedicated method in the API
345 $this->fetchConfig(true);
346 $this->setHealthStatus(HealthStatus::HEALTHY);
347 return HealthStatus::HEALTHY;
348 } catch (\Exception $e) {
349 $this->setHealthStatus(HealthStatus::SICK);
350 return HealthStatus::SICK;
351 }
352 }
353
354 public function getTimestampFile() {
355 $timestampFile = $this->timestampFile;
356 array_unshift($timestampFile, $this->dataDir);
357 return Filesystem::getOsPath($timestampFile);
358 }
359
360 public function loadTimeMarks() {
361 if (Filesystem::fileExists($this->getTimestampFile())) {
362 try {
363 $marks = @json_decode(Filesystem::fileGetContents($this->getTimestampFile()), true);
364 if (!$marks) {
365 $marks = [];
366 }
367 } catch (\Exception $e) {
368 $marks = [];
369 }
370 } else {
371 $marks = [];
372 }
373
374 return $marks;
375 }
376
377 public function setTimeMark($name, $time = NULL) {
378 if ($time === NULL) $time = microtime(true);
379 $marks = $this->loadTimeMarks();
380 $marks[$name] = $time;
381 Filesystem::filePutContents($this->getTimestampFile(), json_encode($marks));
382 }
383
384 public function unsetTimeMark($name) {
385 $marks = $this->loadTimeMarks();
386 if (isset($marks[$name])) {
387 unset($marks[$name]);
388 }
389 Filesystem::filePutContents($this->getTimestampFile(), json_encode($marks));
390 }
391
392 public function getTimeMark($name) {
393 $marks = $this->loadTimeMarks();
394 if (!empty($marks[$name])) {
395 return $marks[$name];
396 } else {
397 return NULL;
398 }
399 }
400
401 public function getRemainingCacheTtl() {
402 return $this->pageCache->getRemainingTtl($this->config->PageCache->ExpireTime);
403 }
404
405 public function hasCache($layout = 'default') {
406 if ($this->hasLocalCache()) {
407 return true;
408 } else {
409 return $this->hasRemoteCache($layout);
410 }
411 }
412
413 public function hasLocalCache($checkIfRequestIsAllowed = true) {
414 if ($this->backlog->exists()) return false;
415 if (!$this->isAllowedUrl($this->url) || ($checkIfRequestIsAllowed && !$this->isAllowedRequest())) return false;
416 $cacheRevision = !empty($this->config->RevisionHash) ? $this->config->RevisionHash : NULL;
417 if ($this->getHealthStatus() !== HealthStatus::HEALTHY) {
418 return false;
419 }
420
421 if (!$this->pageCache->getUseInvalidated()) {
422 $ttl = $this->config->PageCache->ExpireTime;
423 } else {
424 $ttl = $this->config->PageCache->StaleExpireTime;
425 }
426
427 return $this->pageCache->hasCache() && !$this->pageCache->hasExpired($ttl, $cacheRevision);
428 }
429
430 public function hasRemoteCache($layout, $checkIfRequestIsAllowed = true) {
431 if ($this->backlog->exists()) return false;
432 if (
433 !$this->isAllowedUrl($this->url) ||
434 ($checkIfRequestIsAllowed && !$this->isAllowedRequest()) ||
435 ($this->pageCache->getParent() && !$this->pageCache->getParent()->hasCache()) ||
436 $this->isPageCacheLocked()
437 ) return false;
438
439 $resp = $this->api->getCache($this->url, $this->userAgent, $this->supportedCookiesFilter(self::getCookies()), $this->isAJAXRequest(), $layout, $this->referer);
440 if ($resp->getStatus() == Api\ResponseStatus::OK) {// We have cache response
441
442 // Check for invalidated cache and delete it if such is found
443 $this->pageCache->useInvalidated(true);
444 if ($this->pageCache->hasCache()) {
445 $path = $this->pageCache->getCachefilePath();
446 Filesystem::deleteFile($path);
447 Filesystem::deleteFile($path . ".gz");
448 Filesystem::deleteFile($path . ".stale");
449 Filesystem::deleteFile($path . ".stale.gz");
450 if (Filesystem::isDirEmpty(dirname($path))) {
451 Filesystem::deleteDir(dirname($path));
452 }
453 }
454 $this->pageCache->useInvalidated(false);
455 // End of check
456
457 list($headers, $content) = Filesystem::explodeByHeaders($resp->getBody());
458 $this->pageCache->setContent($content, $headers);
459 return true;
460 } else {
461 // 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
462 if ($this->pageCache->hasCache()) {
463 return true;
464 } else {
465 // Check for invalidated cache
466 $this->pageCache->useInvalidated(true);
467 if ($this->hasLocalCache(false)) {
468 return true;
469 } else {
470 $this->pageCache->useInvalidated(false);
471 }
472 }
473
474 return false;
475 }
476 }
477
478 public function hasRemoteCacheMulti($urls, $layout, $checkIfRequestIsAllowed = true) {
479 if ($this->backlog->exists()) return false;
480
481 foreach ($urls as $url) {
482 if (
483 !$this->isAllowedUrl($url) ||
484 ($checkIfRequestIsAllowed && !$this->isAllowedRequest()) ||
485 ($this->pageCache->getParent() && !$this->pageCache->getParent()->hasCache()) || // This check is works correctly despite the fact that it doesn't check the current iteration's url because the check is based on headers and all of the headers for the URLs are the same for batched webhooks
486 $this->isPageCacheLocked()
487 ) return false;
488 }
489
490 $hasCache = true; // hasCache is false if even one of the URLs doesn't have a cache
491 $responses = $this->api->getCacheMulti($urls, $this->userAgent, $this->supportedCookiesFilter(self::getCookies()), $this->isAJAXRequest(), $layout, $this->referer);
492
493 $originalURL = $this->url;
494 $originalPageCache = $this->pageCache;
495
496 foreach ($responses as $url => $resp) {
497 // Overwrite the url
498 $urlInfo = new Url($url);
499 $this->url = $urlInfo->getNormalized();
500
501 // Overwrite the page cache
502 $this->pageCache = new Pagecache($this->url, $this->userAgent, $this->supportedCookiesFilter(self::getCookies()), $this->config->PageCache->SupportedCookies, $this->isAJAXRequest());
503 $this->pageCache->setCookiesProvider([$this, "getPagecacheCookies"]);
504
505 if ($this->config->PageCache->Geot->Status) {
506 $this->pageCache->setGeotVariations($this->config->PageCache->Geot->Variations);
507 }
508
509 if ($this->isAJAXRequest() && $this->isAllowedAJAXUrl($this->url) && !empty($_SERVER["HTTP_REFERER"]) && !$this->isAllowedStandaloneAJAXUrl($url)) {
510 $refererInfo = new Url($_SERVER["HTTP_REFERER"]);
511 $this->pageCache->setReferer($refererInfo->getNormalized());
512 }
513 if (!empty($this->config->URLPathVersion)) {
514 $this->pageCache->setUrlPathVersion($this->config->URLPathVersion);
515 }
516 $this->pageCache->setDataDir($this->getCacheDir());
517
518 // Process the response
519 if ($resp->getStatus() == Api\ResponseStatus::OK) {// We have cache response
520
521 // Check for invalidated cache and delete it if such is found
522 $this->pageCache->useInvalidated(true);
523 if ($this->pageCache->hasCache()) {
524 $path = $this->pageCache->getCachefilePath();
525 Filesystem::deleteFile($path);
526 Filesystem::deleteFile($path . ".gz");
527 Filesystem::deleteFile($path . ".stale");
528 Filesystem::deleteFile($path . ".stale.gz");
529 if (Filesystem::isDirEmpty(dirname($path))) {
530 Filesystem::deleteDir(dirname($path));
531 }
532 }
533 $this->pageCache->useInvalidated(false);
534 // End of check
535
536 list($headers, $content) = Filesystem::explodeByHeaders($resp->getBody());
537 $this->pageCache->setContent($content, $headers);
538 continue;
539 } else {
540 // 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
541 if ($this->pageCache->hasCache()) {
542 continue;
543 } else {
544 // Check for invalidated cache
545 $this->pageCache->useInvalidated(true);
546 if ($this->hasLocalCache(false)) {
547 continue;
548 } else {
549 $this->pageCache->useInvalidated(false);
550 }
551 }
552
553 $hasCache = false;
554 continue;
555 }
556 }
557
558 // Restore the url and page cache
559 $this->url = $originalURL;
560 $this->pageCache = $originalPageCache;
561
562 return $hasCache;
563 }
564
565 public function invalidateCache($url = NULL, $tag = NULL, $reason = NULL) {
566 return $this->purgeCache($url, $tag, PurgeType::INVALIDATE | PurgeType::PAGECACHE_ONLY, $reason);
567 }
568
569 public function clearPageCache($reason = NULL) {
570 return $this->purgeCache(NULL, NULL, PurgeType::PAGECACHE_ONLY, $reason);
571 }
572
573 public function purgeCache($url = NULL, $tag = NULL, $purgeType = PurgeType::COMPLETE, $reason = NULL) {
574 @set_time_limit(0);
575 $this->lockPageCache(); // Set the page cache lock, expires after self::PAGECACHE_LOCK_EXPIRATION_TIME seconds
576
577 try {
578 $invalidate = !!($purgeType & PurgeType::INVALIDATE);
579 $pageCacheOnly = !!($purgeType & PurgeType::PAGECACHE_ONLY);
580 $lightPurge = !!($purgeType & PurgeType::LIGHT_PURGE);
581
582 if ($url || $tag) {
583 $localResult = true;
584 $apiResult = true;
585 if ($url) {
586 if (is_array($url)) {
587 foreach ($url as &$urlLink) {
588 $urlLink = $this->normalizeUrl($urlLink);
589 if ($invalidate) {
590 $localResult &= $this->invalidateLocalUrlCache($urlLink);
591 } else {
592 $localResult &= $this->purgeLocalUrlCache($urlLink);
593 }
594 }
595 } else {
596 $url = $this->normalizeUrl($url);
597 if ($invalidate) {
598 $localResult &= $this->invalidateLocalUrlCache($url);
599 } else {
600 $localResult &= $this->purgeLocalUrlCache($url);
601 }
602 }
603
604 try {
605 $apiResult &= $this->api->purgeCache($url, false, $reason, $lightPurge);
606 } catch (ServiceDownException $e) {
607 $apiResult = false;
608 // TODO: Potentially log this
609 }
610 }
611
612 if ($tag) {
613 $attemptsLeft = 10;
614 $purgedUrls = array();
615 do {
616 $hadError = false;
617
618 try {
619 $purgedUrls = $this->api->purgeCacheByTag($tag, $reason);
620
621 foreach ($purgedUrls as $url) {
622 if ($invalidate) {
623 $localResult &= $this->invalidateLocalUrlCache($url);
624 } else {
625 $localResult &= $this->purgeLocalUrlCache($url);
626 }
627 }
628 } catch (ServiceDownException $e) {
629 $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).
630 $apiResult = false;
631 // TODO: Log this
632 break;
633 } catch (\Exception $e) {
634 $hadError = true;
635 $attemptsLeft--;
636 sleep(3);
637 }
638 } while ($attemptsLeft > 0 && count($purgedUrls) > 0);
639 }
640 } else {
641 if ($invalidate) {
642 $localResult = $this->invalidateLocalCache();
643 $apiResult = $this->api->purgeCache(NULL, $pageCacheOnly, $reason, $lightPurge); // delete only page cache
644 } else {
645 $staleCacheDir = $this->purgeLocalCache(true);
646
647 // Call the cache purge method
648 $apiResult = $this->api->purgeCache(NULL, $pageCacheOnly, $reason, $lightPurge);
649
650 // Finally, delete the files of the stale directory
651 Filesystem::deleteDir($staleCacheDir);
652
653 $localResult = true; // We do not care if $staleCacheDir was not deleted successfully
654 }
655
656 $this->elementRevision->refresh();
657 }
658
659 $this->unlockPageCache(); // Purge cache is done, we can now unlock
660 } catch (\Exception $e) {
661 $this->unlockPageCache(); // Purge cache had an error, so just unlock
662
663 throw $e;
664 }
665
666 return $apiResult && $localResult;
667 }
668
669 public function purgeLocalCache($quick = false) {
670 $staleCacheDir = $this->getCacheDir() . '.stale.' . md5(microtime(true));
671 $staleCacheDirSuffix = "";
672 $counter = 0;
673 while (Filesystem::fileExists($staleCacheDir . $staleCacheDirSuffix)) {
674 $counter++;
675 $staleCacheDirSuffix = "_" . $counter;
676 }
677 $staleCacheDir .= $staleCacheDirSuffix;
678 $this->purgeProxyCache();
679 $this->config->LastFetch = 0;
680 $this->setConfig($this->config);
681
682 // Rename cache files directory
683 if (Filesystem::fileExists($this->getCacheDir()) && !Filesystem::rename($this->getCacheDir(), $staleCacheDir)) {
684 throw new \Exception("No write permissions to rename the directory: " . $this->getCacheDir());
685 }
686
687 // Create a new empty directory
688 if (!Filesystem::createDir($this->getCacheDir())) {
689 throw new \Exception("No write permissions to create the directory: " . $this->getCacheDir());
690 }
691
692 if (!$quick) {
693 // Finally, delete the files of the stale directory
694 Filesystem::deleteDir($staleCacheDir);
695 }
696
697 $this->elementRevision->refresh();
698
699 return $staleCacheDir;
700 }
701
702 public function fetchConfig($ignoreHealthStatus = false) {
703 // TODO: Record failures and repeat with a delay
704 $fetcher = new Api\RemoteConfigFetcher($this->siteId, $this->siteSecret);
705
706 if (!$ignoreHealthStatus) {
707 $fetcher->setBacklog($this->backlog);
708 $fetcher->setNitroPack($this);
709 }
710
711 try {
712 $configContents = $fetcher->get(); // this can throw in case of http errors or validation failures
713 } catch (\Exception $e) {
714 // TODO: Record this failure, possibly in the backlog
715 throw $e;
716 }
717
718 $config = json_decode($configContents);
719 if ($config) {
720 $config->SDKVersion = NitroPack::VERSION;
721 $config->LastFetch = time();
722
723 $this->setConfig($config);
724 return true;
725 } else {
726 throw new EmptyConfigException("Config response was empty");
727 }
728 }
729
730 public function setConfig($config) {
731 $file = $this->getConfigFile();
732 if (Filesystem::createDir(dirname($file))) {
733 if (Filesystem::filePutContents($file, json_encode($config))) {
734 return true;
735 } else {
736 throw new StorageException(sprintf("Config file %s cannot be saved to disk", $file));
737 }
738 } else {
739 throw new StorageException(sprintf("Storage directory %s cannot be created", dirname($file)));
740 }
741 }
742
743 public function setVarnishProxyCacheHeaders($newHeaders) {
744 $this->varnishProxyCacheHeaders = $newHeaders;
745 }
746
747 public function purgeProxyCache($url = NULL) {
748 if (empty($this->config->CacheIntegrations)) {
749 return;
750 }
751
752 if (!empty($this->config->CacheIntegrations->Varnish)) {
753 if (empty($this->config->CacheIntegrations->Varnish->PurgeConfigSet)) {
754 if ($url) {
755 $url = $this->normalizeUrl($url);
756 $varnish = new Integrations\Varnish(
757 $this->config->CacheIntegrations->Varnish->Servers,
758 $this->config->CacheIntegrations->Varnish->PurgeSingleMethod,
759 $this->varnishProxyCacheHeaders
760 );
761 $varnish->purge($url);
762 } else {
763 $varnish = new Integrations\Varnish(
764 $this->config->CacheIntegrations->Varnish->Servers,
765 $this->config->CacheIntegrations->Varnish->PurgeAllMethod,
766 $this->varnishProxyCacheHeaders
767 );
768 $varnish->purge($this->config->CacheIntegrations->Varnish->PurgeAllUrl);
769 }
770
771 return;
772 }
773
774 foreach ($this->config->CacheIntegrations->Varnish->PurgeConfigSet as $purgeSet) {
775 if ($url) {
776 foreach ($purgeSet->PurgeSingleHeadersTemplates as $headerTemplateTuple) {
777 $this->varnishProxyCacheHeaders[$headerTemplateTuple->HeaderName] = $this->valueFromTemplate($url, $headerTemplateTuple->HeaderTemplate);
778 }
779
780 $varnish = new Integrations\Varnish(
781 null,
782 $purgeSet->PurgeSingleMethod,
783 $this->varnishProxyCacheHeaders
784 );
785
786 $purgeUrl = $this->valueFromTemplate($url, $purgeSet->PurgeSingleTemplate);
787 $varnish->purge($purgeUrl, true);
788 } else {
789 $varnish = new Integrations\Varnish(
790 null,
791 $purgeSet->PurgeAllMethod,
792 $this->varnishProxyCacheHeaders
793 );
794 $varnish->purge($purgeSet->PurgeAllUrl, true);
795 }
796 }
797 }
798
799 //if (!empty($this->config->CacheIntegrations->LiteSpeed) && php_sapi_name() !== "cli") {
800 // if ($url) {
801 // $urlObj = new Url($url);
802 // $liteSpeedPath = $urlObj->getPath();
803 // if ($urlObj->getQuery()) {
804 // $liteSpeedPath .= "?" . $urlObj->getQuery();
805 // }
806 // header("X-LiteSpeed-Purge: $liteSpeedPath", false);
807 // } else {
808 // header("X-LiteSpeed-Purge: *", false);
809 // }
810 //}
811 }
812
813 public function isAllowedUrl($url) {
814 if (strpos($url, 'sucurianticache=') !== false) return false;
815
816 if ($this->config->EnabledURLs->Status) {
817 if (!empty($this->config->EnabledURLs->URLs)) {
818 foreach ($this->config->EnabledURLs->URLs as $enabledUrl) {
819 $enabledUrlModified = preg_replace("/^(https?:)?\/\//", "*", $enabledUrl);
820
821 if (preg_match('/^' . self::wildcardToRegex($enabledUrlModified) . '$/', $url)) {
822 return true;
823 }
824 }
825
826 return false;
827 }
828 } else if ($this->config->DisabledURLs->Status) {
829 if (!empty($this->config->DisabledURLs->URLs)) {
830 foreach ($this->config->DisabledURLs->URLs as $disabledUrl) {
831 $disabledUrlModified = preg_replace("/^(https?:)?\/\//", "*", $disabledUrl);
832
833 if (preg_match('/^' . self::wildcardToRegex($disabledUrlModified) . '$/', $url)) {
834 return false; // don't cache disabled URLs
835 }
836 }
837 }
838 }
839
840 return true;
841 }
842
843 public function isServiceRequest() {
844 return isset($_SERVER["HTTP_X_NITROPACK_REQUEST"]);
845 }
846
847 public function isAllowedRequest($allowServiceRequests = false) {
848 if (($this->isAJAXRequest() && !$this->isAllowedAJAX()) || !($this->isRequestMethod("GET") || $this->isRequestMethod("HEAD"))) {// TODO: Allow URLs which match a pattern in the AJAX URL whitelist
849 return false; // don't cache ajax or not GET requests
850 }
851
852 if (!$allowServiceRequests && $this->isServiceRequest()) { // Skip requests coming from NitroPack
853 return false;
854 }
855
856 if (isset($_GET["nonitro"])) { // Skip requests having ?nonitro
857 return false;
858 }
859
860 if (!$this->isAllowedBrowser()) {
861 return false;
862 }
863
864 if (isset($this->config->ExcludedCookies) && $this->config->ExcludedCookies->Status) {
865 foreach ($this->config->ExcludedCookies->Cookies as $cookieExclude) {
866 foreach (self::getCookies() as $cookieName => $cookieValue) {
867 if (preg_match('/^' . self::wildcardToRegex($cookieExclude->name) . '$/', $cookieName)) {
868 if (count($cookieExclude->values) == 0) {
869 return false; // no excluded cookie values entered, reject all values
870 } else {
871 foreach ($cookieExclude->values as $val) {
872 if (preg_match('/^' . self::wildcardToRegex($val) . '$/', $cookieValue)) {
873 return false;
874 }
875 }
876 }
877 }
878 }
879 }
880 }
881
882 // Deactivate NitroPack for a desired % of journeys
883 if ($this->config->TestImpact->Status) {
884 $impactGroup = $this->getImpactGroupValue();
885 if ($impactGroup <= $this->config->TestImpact->ThresholdPercentage) {
886 return false;
887 }
888 }
889
890 return true;
891 }
892
893 private function getImpactGroupValue() {
894 if (!empty($_COOKIE['nitroImpactGroup'])) {
895 return $_COOKIE['nitroImpactGroup'];
896 } else {
897 $impactGroup = mt_rand(1,100);
898 setcookie('nitroImpactGroup', $impactGroup, time() + 86400, '/', $_SERVER['HTTP_HOST']);
899 return $impactGroup;
900 }
901 }
902
903 public function isAllowedBrowser() {
904 if (empty($_SERVER["HTTP_USER_AGENT"])) return true;
905
906 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
907 return false;
908 }
909
910 return true;
911 }
912
913 public function getScheme() {
914 return $this->isSecure() ? 'https://' : 'http://';
915 }
916
917 public function isSecure() {
918 $result = (!empty($_SERVER['HTTP_X_FORWARDED_PROTO']) && in_array("https", array_map("strtolower", array_map("trim", explode(",", $_SERVER['HTTP_X_FORWARDED_PROTO']))))) ||
919 (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ||
920 (!empty($_SERVER['SERVER_PORT']) && $_SERVER['SERVER_PORT'] == 443) ||
921 (!empty($_SERVER['HTTP_SSL_FLAG']) && $_SERVER['HTTP_SSL_FLAG'] == 'SSL');
922
923 if (!$result && !empty($_SERVER['HTTP_CF_VISITOR'])) {
924 $visitor = json_decode($_SERVER['HTTP_CF_VISITOR']);
925 $result = $visitor && property_exists($visitor, "scheme") && strtolower($visitor->scheme) == "https";
926 }
927
928 return $result;
929 }
930
931 public function isAJAXRequest() {
932 return
933 (!empty($_SERVER['HTTP_X_REQUESTED_WITH']) && strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) == 'xmlhttprequest') ||
934 $this->isAllowedAJAXUrl($this->url) ||
935 $this->isAllowedStandaloneAJAXUrl($this->url);
936 }
937
938 public function isRequestMethod($method) {
939 return empty($_SERVER['REQUEST_METHOD']) || $_SERVER['REQUEST_METHOD'] == $method;
940 }
941
942 public function isAllowedAJAX() {
943 if (!$this->isAllowedStandaloneAJAXUrl($this->url)) {
944 if (!$this->pageCache->getParent()) return false;
945 if (!$this->pageCache->getParent()->hasCache() || $this->pageCache->getParent()->hasExpired($this->config->PageCache->ExpireTime)) return false;
946 }
947 return true;
948 }
949
950 public function isAllowedAJAXUrl($url) {
951 if ($this->config->AjaxURLs->Status) {
952 if (!empty($this->config->AjaxURLs->URLs)) {
953 foreach ($this->config->AjaxURLs->URLs as $ajaxUrl) {
954 $ajaxUrlModified = preg_replace("/^(https?:)?\/\//", "*", $ajaxUrl);
955 if (preg_match('/^' . self::wildcardToRegex($ajaxUrlModified) . '$/', $url)) {
956 return true;
957 }
958 }
959 return false;
960 }
961 }
962 return false;
963 }
964
965 public function isAllowedStandaloneAJAXUrl($url)
966 {
967 if ($this->config->AjaxURLs->Status) {
968 if (!empty($this->config->AjaxURLs->StandaloneURLs)) {
969 foreach ($this->config->AjaxURLs->StandaloneURLs as $ajaxUrl) {
970 $ajaxUrlModified = preg_replace("/^(https?:)?\/\//", "*", $ajaxUrl);
971 if (preg_match('/^' . self::wildcardToRegex($ajaxUrlModified) . '$/', $url)) {
972 return true;
973 }
974 }
975 }
976 }
977 return false;
978 }
979
980 public function isCacheAllowed() {
981 return $this->isAllowedRequest() && $this->isAllowedUrl($this->url);
982 }
983
984 public function isStatefulCacheSatisfied($type = NULL) {
985 if ($this->config->StatefulCache->Status) {
986 $foundTypeSelectors = false;
987 foreach ($this->config->StatefulCache->Selectors as $selector) {
988 if ($type !== NULL) {
989 if ($selector->type === $type) {
990 $foundTypeSelectors = true;
991 } else {
992 continue;
993 }
994 }
995
996 $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
997 $cookieKey = 'np-' . $selector->type . '-' . $selectorEncoded . '-override';
998 if (empty($_COOKIE[$cookieKey]) || $_COOKIE[$cookieKey] != $this->elementRevision->get()) {
999 return false;
1000 }
1001 }
1002
1003 return $type !== NULL ? $foundTypeSelectors : true;
1004 }
1005
1006 return false;
1007 }
1008
1009 public function purgeLocalUrlCache($url) {
1010 $url = $this->normalizeUrl($url);
1011 $this->purgeProxyCache($url);
1012 $localResult = true;
1013 $cacheDir = $this->getCacheDir();
1014 $knownDeviceTypes = Device::getKnownTypes();
1015 foreach ($knownDeviceTypes as $deviceType) {
1016 $urlDir = PageCache::getUrlDir($cacheDir . "/" . $deviceType, $url, false, $this->config->URLPathVersion);
1017 $invalidatedUrlDir = PageCache::getUrlDir($cacheDir . "/" . $deviceType, $url, true, $this->config->URLPathVersion);
1018 $localResult &= Filesystem::deleteDir($urlDir);
1019 $localResult &= Filesystem::deleteDir($invalidatedUrlDir);
1020 }
1021 return $localResult;
1022 }
1023
1024 public function invalidateLocalUrlCache($url) {
1025 $url = $this->normalizeUrl($url);
1026 $this->purgeProxyCache($url);
1027 $localResult = true;
1028 $cacheDir = $this->getCacheDir();
1029 $knownDeviceTypes = Device::getKnownTypes();
1030 foreach ($knownDeviceTypes as $deviceType) {
1031 $urlDir = PageCache::getUrlDir($cacheDir . "/" . $deviceType, $url, false, $this->config->URLPathVersion);
1032 $urlDirInvalid = PageCache::getUrlDir($cacheDir . "/" . $deviceType, $url, true, $this->config->URLPathVersion);
1033
1034 $this->invalidateDir($urlDir, $urlDirInvalid);
1035 }
1036 return $localResult;
1037 }
1038
1039 public function invalidateLocalCache() {
1040 $this->purgeProxyCache();
1041 $this->config->LastFetch = 0;
1042 $this->setConfig($this->config);
1043
1044 $cacheDir = $this->getCacheDir();
1045 $knownDeviceTypes = Device::getKnownTypes();
1046 foreach ($knownDeviceTypes as $deviceType) {
1047 $deviceTypeDir = $cacheDir . "/" . $deviceType;
1048 Filesystem::dirForeach($deviceTypeDir, function($urlDir) {
1049 if (substr($urlDir, -2) !== "_i") {
1050 $this->invalidateDir($urlDir, $urlDir . "_i");
1051 }
1052 });
1053 }
1054 return true;
1055 }
1056
1057 private function invalidateDir($urlDir, $urlDirInvalid) {
1058 if (Filesystem::fileExists($urlDirInvalid)) {
1059 Filesystem::dirForeach($urlDir, function($file) use ($urlDirInvalid) {
1060 Filesystem::rename($file, $urlDirInvalid . "/" . basename($file));
1061 });
1062
1063 Filesystem::deleteDir($urlDir);
1064 } else {
1065 Filesystem::rename($urlDir, $urlDirInvalid);
1066 }
1067 Filesystem::touch($urlDirInvalid);
1068 }
1069
1070 public function integrationUrl($widget, $version = null) {
1071 $integration = new IntegrationUrl($widget, $this->siteId, $this->siteSecret, $version);
1072
1073 return $integration->getUrl();
1074 }
1075
1076 public function embedJsUrl() {
1077 $embedjs = new Embedjs();
1078
1079 return $embedjs->getUrl();
1080 }
1081
1082 public function enableSafeMode() {
1083 $this->api->safe_mode->enable();
1084 $this->fetchConfig();
1085 }
1086
1087 public function disableSafeMode() {
1088 $this->api->safe_mode->disable();
1089 $this->fetchConfig();
1090 }
1091
1092 public function enableCartCache() {
1093 $this->api->enableCartCache();
1094 }
1095
1096 public function disableCartCache() {
1097 $this->api->disableCartCache();
1098 }
1099
1100 public function getStatefulCacheHandlerScript() {
1101 if ($this->config->StatefulCache->Status && $this->config->StatefulCache->HandlerScript) {
1102 $keyRevision = $this->elementRevision->get();
1103 return '<script id="nitro-stateful-cache" nitro-exclude>' . str_replace("KEY_REVISION", $keyRevision, $this->config->StatefulCache->HandlerScript) . '</script>';
1104 }
1105
1106 return "";
1107 }
1108
1109 private function loadConfig() {
1110 $file = $this->getConfigFile();
1111
1112 $config = array();
1113 if (Filesystem::fileExists($file) || $this->fetchConfig(true)) {
1114 $config = json_decode(Filesystem::fileGetContents($file));
1115 if (empty($config->SDKVersion) || $config->SDKVersion !== NitroPack::VERSION || empty($config->LastFetch) || time() - $config->LastFetch >= $this->configTTL) {
1116 if ($this->getHealthStatus() === HealthStatus::HEALTHY) {
1117 if ($this->fetchConfig()) {
1118 $config = json_decode(Filesystem::fileGetContents($file));
1119 } else {
1120 throw new NoConfigException("Can't load config file");
1121 }
1122 }
1123 }
1124 $this->config = $config;
1125 } else {
1126 throw new NoConfigException("Can't load config file");
1127 }
1128 }
1129
1130 private function getConfigFile() {
1131 $configFile = $this->configFile;
1132
1133 $filename = array_pop($configFile);
1134
1135 $filename = $this->siteId . '-' . $filename;
1136
1137 array_push($configFile, $filename);
1138 array_unshift($configFile, $this->dataDir);
1139
1140 return Filesystem::getOsPath($configFile);
1141 }
1142
1143 private function lockPageCache() {
1144 $filename = $this->getPageCacheLockFilename();
1145
1146 if (Filesystem::fileExists($filename)) {
1147 $sem = 1 + (int)Filesystem::fileGetContents($filename);
1148 } else {
1149 $sem = 1;
1150 }
1151
1152 return !!Filesystem::filePutContents($filename, $sem);
1153 }
1154
1155 private function unlockPageCache() {
1156 $filename = $this->getPageCacheLockFilename();
1157
1158 if (Filesystem::fileExists($filename)) {
1159 $sem = (int)Filesystem::fileGetContents($filename);
1160
1161 $sem--;
1162
1163 if ($sem <= 0) {
1164 return !!Filesystem::deleteFile($filename);
1165 } else {
1166 return !!Filesystem::filePutContents($filename, $sem);
1167 }
1168 }
1169
1170 return false;
1171 }
1172
1173 private function isPageCacheLocked() {
1174 $filename = $this->getPageCacheLockFilename();
1175
1176 if (!Filesystem::fileExists($filename)) {
1177 return false;
1178 } else {
1179 if (time() - Filesystem::fileMTime($filename) <= self::PAGECACHE_LOCK_EXPIRATION_TIME) {
1180 return true;
1181 } else {
1182 Filesystem::deleteFile($filename);
1183
1184 return false;
1185 }
1186 }
1187
1188 // We should never get here, so consider this a default return value in case of future changes
1189 return false;
1190 }
1191
1192 private function getPageCacheLockFilename() {
1193 $pageCacheLockFile = $this->pageCacheLockFile;
1194 array_unshift($pageCacheLockFile, $this->dataDir);
1195 return Filesystem::getOsPath($pageCacheLockFile);
1196 }
1197
1198 private function normalizeUrl($url) {
1199 $urlObj = new Url($url);
1200 return $urlObj->getNormalized();
1201 }
1202
1203 /**
1204 * Substitutes values from a template based on URL components
1205 * @param string $url The URL, from which to extract substitution values
1206 * @param string $template The template to use
1207 * @return string
1208 */
1209 private function valueFromTemplate($url, $template) {
1210 $urlObj = new Url($url);
1211
1212 $proxyPurgeTemplate = array
1213 (
1214 '{{targetUrl}}' => $urlObj->getNormalized(),
1215 '{{targetProtocol}}' => $urlObj->getScheme(),
1216 '{{targetHost}}' => $urlObj->getHost(),
1217 '{{targetPath}}' => $urlObj->getPath(),
1218 '{{targetQuery}}' => $urlObj->getQuery(),
1219 );
1220
1221 $templateKeys = array_keys($proxyPurgeTemplate);
1222 $templateVals = array_values($proxyPurgeTemplate);
1223 $constructed = str_replace($templateKeys, $templateVals, $template);
1224
1225 return $constructed;
1226 }
1227 }