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