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