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