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