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