Api
1 year 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
1 year 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
Pagecache.php
427 lines
| 1 | <?php |
| 2 | |
| 3 | namespace NitroPack\SDK; |
| 4 | |
| 5 | use \NitroPack\Url\Url; |
| 6 | |
| 7 | class Pagecache |
| 8 | { |
| 9 | protected $url; |
| 10 | protected $cookies; |
| 11 | protected $supportedCookies; |
| 12 | protected $dataDir; |
| 13 | protected $isAjax; |
| 14 | protected $referer; |
| 15 | protected $parent; |
| 16 | protected $Device; |
| 17 | protected $useCompression; |
| 18 | protected $useInvalidated; |
| 19 | private $urlPathVersion; |
| 20 | private $cookiesProvider; |
| 21 | private $geotVariations; |
| 22 | |
| 23 | public static function getUrlDir($dataDir, $url, $useInvalidated = false, $pathVersion = 1) |
| 24 | { |
| 25 | $safeUrl = str_replace(array('/', '?', ':', ';', '=', '&', '.', '--', '%', '~'), '-', $url); |
| 26 | if (defined('NITRO_DEBUG_MODE') && NITRO_DEBUG_MODE) { |
| 27 | $urlDir = $dataDir . "/" . $safeUrl; |
| 28 | } else { |
| 29 | switch ($pathVersion) { |
| 30 | case 2: |
| 31 | $urlDir = $dataDir . "/" . md5($url); |
| 32 | break; |
| 33 | default: |
| 34 | $urlDir = $dataDir . "/" . md5($safeUrl); |
| 35 | break; |
| 36 | } |
| 37 | } |
| 38 | |
| 39 | if ($useInvalidated) { |
| 40 | return $urlDir . "_i"; |
| 41 | } else { |
| 42 | return $urlDir; |
| 43 | } |
| 44 | } |
| 45 | |
| 46 | public function __construct($url, $userAgent, $cookies = array(), $supportedCookies = array(), $isAjax = false, $referer = NULL) |
| 47 | { |
| 48 | $this->url = new Url($url); |
| 49 | $this->cookies = $cookies; |
| 50 | $this->supportedCookies = $supportedCookies; |
| 51 | $this->dataDir = NULL; |
| 52 | $this->isAjax = $isAjax; |
| 53 | $this->parent = NULL; |
| 54 | $this->Device = new Device($userAgent); |
| 55 | $this->useCompression = false; |
| 56 | $this->useInvalidated = false; |
| 57 | $this->cookiesProvider = NULL; |
| 58 | $this->geotVariations = new \stdClass; |
| 59 | $this->setUrlPathVersion(1); |
| 60 | $this->setReferer($referer); |
| 61 | } |
| 62 | |
| 63 | public function enableCompression() |
| 64 | { |
| 65 | $this->useCompression = true; |
| 66 | if ($this->parent) { |
| 67 | $this->parent->enableCompression(); |
| 68 | } |
| 69 | } |
| 70 | |
| 71 | public function disableCompression() |
| 72 | { |
| 73 | $this->useCompression = false; |
| 74 | if ($this->parent) { |
| 75 | $this->parent->disableCompression(); |
| 76 | } |
| 77 | } |
| 78 | |
| 79 | public function useInvalidated($useInvalidated) |
| 80 | { |
| 81 | $this->useInvalidated = $useInvalidated; |
| 82 | if ($this->parent) { |
| 83 | $this->parent->useInvalidated($useInvalidated); |
| 84 | } |
| 85 | } |
| 86 | |
| 87 | public function getUseInvalidated() |
| 88 | { |
| 89 | return $this->useInvalidated; |
| 90 | } |
| 91 | |
| 92 | public function setReferer($referer) |
| 93 | { |
| 94 | $this->referer = $referer ? new Url($referer) : NULL; |
| 95 | if ($this->referer) { |
| 96 | $this->parent = new Pagecache($this->referer->getNormalized(), $this->Device->getUserAgent(), $this->cookies, $this->supportedCookies); |
| 97 | $this->parent->setUrlPathVersion($this->urlPathVersion); |
| 98 | //$this->parent->setCookiesProvider($this->cookiesProvider); |
| 99 | |
| 100 | if (count(get_object_vars($this->geotVariations)) > 0) { |
| 101 | $this->parent->setGeotVariations($this->geotVariations); |
| 102 | } |
| 103 | |
| 104 | if ($this->dataDir) { |
| 105 | $this->parent->setDataDir(dirname($this->dataDir)); |
| 106 | } |
| 107 | } else { |
| 108 | $this->parent = NULL; |
| 109 | } |
| 110 | } |
| 111 | |
| 112 | public function setUrlPathVersion($version = 1) |
| 113 | { |
| 114 | $this->urlPathVersion = $version; |
| 115 | if ($this->parent) { |
| 116 | $this->parent->setUrlPathVersion($version); |
| 117 | } |
| 118 | } |
| 119 | |
| 120 | public function getReferer() |
| 121 | { |
| 122 | return $this->referer; |
| 123 | } |
| 124 | |
| 125 | public function getParent() |
| 126 | { |
| 127 | return $this->parent; |
| 128 | } |
| 129 | |
| 130 | public function setDataDir($dir) |
| 131 | { |
| 132 | if ($dir === null) { |
| 133 | $this->dataDir = null; |
| 134 | } else { |
| 135 | $this->dataDir = $dir . "/" . $this->Device->getType(); |
| 136 | } |
| 137 | |
| 138 | if ($this->parent) { |
| 139 | $this->parent->setDataDir($dir); |
| 140 | } |
| 141 | } |
| 142 | |
| 143 | public function setCookiesProvider($provider) |
| 144 | { |
| 145 | $this->cookiesProvider = $provider; |
| 146 | } |
| 147 | |
| 148 | public function setGeotVariations($geotVariations) |
| 149 | { |
| 150 | $this->geotVariations = $geotVariations; |
| 151 | } |
| 152 | |
| 153 | public function hasCache() |
| 154 | { |
| 155 | // If there is no cache for the parent we do not need to check cache existance for the current object, because we only serve AJAX cache for pages already cached by NitroPack |
| 156 | if ($this->parent && !$this->parent->hasCache()) return false; |
| 157 | if ($this->useInvalidated) { |
| 158 | $this->convertToStaleCache(); |
| 159 | return Filesystem::fileExists($this->getCachefilePath("stale")); |
| 160 | } else { |
| 161 | return Filesystem::fileExists($this->getCachefilePath()); |
| 162 | } |
| 163 | } |
| 164 | |
| 165 | public function hasExpired($ttl = 86400, $cacheRevision = NULL) |
| 166 | { |
| 167 | // If the cache for the parent is expired we do not need to check cache for the current object, because we only serve AJAX cache for pages already cached by NitroPack |
| 168 | if ($this->parent && $this->parent->hasExpired($ttl, $cacheRevision)) return true; |
| 169 | |
| 170 | if ($this->useInvalidated) { |
| 171 | $this->convertToStaleCache(); |
| 172 | $cachefilePath = $this->getCachefilePath("stale"); |
| 173 | $mtime = Filesystem::fileMtime(dirname($cachefilePath)); |
| 174 | } else { |
| 175 | $cachefilePath = $this->getCachefilePath(); |
| 176 | $mtime = Filesystem::fileMtime($cachefilePath); |
| 177 | } |
| 178 | |
| 179 | $now = time(); |
| 180 | |
| 181 | if ($now - $mtime >= $ttl) { |
| 182 | return true; |
| 183 | } else { |
| 184 | try { |
| 185 | $headers = Filesystem::fileGetHeaders($cachefilePath); |
| 186 | if (!empty($headers["x-nitro-expires"]) && $now > (int)$headers["x-nitro-expires"]) { |
| 187 | return true; |
| 188 | } |
| 189 | |
| 190 | if ( |
| 191 | !$this->useInvalidated && |
| 192 | !empty($headers["x-cache-ctime"]) && |
| 193 | $now - (int)$headers["x-cache-ctime"] > $ttl |
| 194 | ) { |
| 195 | return true; |
| 196 | } |
| 197 | |
| 198 | $expectedRev = !empty($cacheRevision) ? $cacheRevision : NULL; |
| 199 | $cachedRev = !empty($headers["x-nitro-rev"]) ? $headers["x-nitro-rev"] : NULL; |
| 200 | // Check if the revision has changed which makes cache file obsolete |
| 201 | if ($expectedRev && $expectedRev !== $cachedRev) { |
| 202 | return true; |
| 203 | } |
| 204 | } catch (\Exception $e) { |
| 205 | return true; |
| 206 | } |
| 207 | } |
| 208 | return false; |
| 209 | } |
| 210 | |
| 211 | public function getRemainingTtl($ttl = 86400) |
| 212 | { |
| 213 | // If there is a parent cache file return its remaining TTL |
| 214 | if ($this->parent) return $this->parent->getRemainingTtl(); |
| 215 | |
| 216 | if ($this->useInvalidated) { |
| 217 | return 0; |
| 218 | } else { |
| 219 | $cachefilePath = $this->getCachefilePath(); |
| 220 | } |
| 221 | |
| 222 | $now = time(); |
| 223 | try { |
| 224 | $headers = Filesystem::fileGetHeaders($cachefilePath); |
| 225 | if (!empty($headers["x-nitro-expires"])) { |
| 226 | $expireTime = (int)$headers["x-nitro-expires"]; |
| 227 | } else if (!empty($headers["x-cache-ctime"])) { |
| 228 | $expireTime = (int)$headers["x-cache-ctime"] + $ttl; |
| 229 | } else { |
| 230 | $mtime = Filesystem::fileMtime($cachefilePath); |
| 231 | $expireTime = $mtime + $ttl; |
| 232 | } |
| 233 | |
| 234 | return max($expireTime - $now, 0); |
| 235 | } catch (\Exception $e) { |
| 236 | return 0; |
| 237 | } |
| 238 | |
| 239 | return 0; |
| 240 | } |
| 241 | |
| 242 | public function setContent($content, $headers = NULL) |
| 243 | { |
| 244 | if (!$this->dataDir) return; |
| 245 | |
| 246 | $filePath = $this->getCachefilePath(); |
| 247 | if (Filesystem::createDir(dirname($filePath))) { |
| 248 | if (!Filesystem::filePutContents($filePath, $content, $this->headersFlatten($headers))) { |
| 249 | return false; |
| 250 | } else { |
| 251 | if ($headers && !empty($headers["x-cache-ctime"])) { |
| 252 | Filesystem::touch($filePath, (int)$headers["x-cache-ctime"]); |
| 253 | } |
| 254 | return $this->compress($filePath); |
| 255 | } |
| 256 | } |
| 257 | } |
| 258 | |
| 259 | private function headersFlatten($headers) |
| 260 | { |
| 261 | // The headers from fileGetAll come as associative array |
| 262 | // They must be converted to an array of strings before being passed to filePutContents, which is what this function does |
| 263 | $headersFlat = array(); |
| 264 | foreach ($headers as $name => $value) { |
| 265 | if (is_array($value)) { |
| 266 | foreach ($value as $subValue) { |
| 267 | $headersFlat[] = $name . ":" . $subValue; |
| 268 | } |
| 269 | } else { |
| 270 | $headersFlat[] = $name . ":" . $value; |
| 271 | } |
| 272 | } |
| 273 | |
| 274 | return $headersFlat; |
| 275 | } |
| 276 | |
| 277 | private function compress($filePath) |
| 278 | { |
| 279 | $fileInfo = Filesystem::fileGetAll($filePath); |
| 280 | $fileInfo->headers["Content-Encoding"] = "gzip"; |
| 281 | $gzPath = $filePath . ".gz"; |
| 282 | $res = Filesystem::filePutContents($gzPath, gzencode($fileInfo->content, 4), $this->headersFlatten($fileInfo->headers)); // save compressed version of the cache file |
| 283 | |
| 284 | if ($res && $fileInfo->headers && !empty($fileInfo->headers["x-cache-ctime"])) { |
| 285 | Filesystem::touch($gzPath, (int)$fileInfo->headers["x-cache-ctime"]); |
| 286 | } |
| 287 | |
| 288 | return $res; |
| 289 | } |
| 290 | |
| 291 | public function readfile() |
| 292 | { |
| 293 | $cacheFileContent = $this->returnCacheFileContent(); |
| 294 | $headers = $cacheFileContent[0]; |
| 295 | $contents = $cacheFileContent[1]; |
| 296 | |
| 297 | foreach ($headers as $header) { |
| 298 | header($header['name'] . ': ' . $header['value'], false); |
| 299 | } |
| 300 | |
| 301 | echo $contents; |
| 302 | } |
| 303 | |
| 304 | public function returnCacheFileContent() |
| 305 | { |
| 306 | if ($this->useInvalidated) { |
| 307 | $this->convertToStaleCache(); |
| 308 | $filePath = $this->getCachefilePath("stale"); |
| 309 | } else { |
| 310 | $filePath = $this->getCachefilePath(); |
| 311 | } |
| 312 | if ($this->canUseCompression() && (ini_get("zlib.output_compression") === "0" || ini_set("zlib.output_compression", "0") !== false)) { |
| 313 | $filePath .= ".gz"; |
| 314 | } |
| 315 | $file = Filesystem::fileGetAll($filePath); |
| 316 | $returnHeaders = []; |
| 317 | |
| 318 | foreach ($file->headers as $name => $value) { |
| 319 | if (is_array($value)) { |
| 320 | foreach ($value as $subVal) { |
| 321 | $returnHeaders[] = [ |
| 322 | 'name' => $name, |
| 323 | 'value' => $subVal |
| 324 | ]; |
| 325 | } |
| 326 | } else { |
| 327 | $returnHeaders[] = [ |
| 328 | 'name' => $name, |
| 329 | 'value' => $value |
| 330 | ]; |
| 331 | } |
| 332 | } |
| 333 | |
| 334 | return [ $returnHeaders, $file->content ]; |
| 335 | } |
| 336 | |
| 337 | public function getFileContents() |
| 338 | { |
| 339 | if ($this->useInvalidated) { |
| 340 | $this->convertToStaleCache(); |
| 341 | return Filesystem::fileGetContents($this->getCachefilePath("stale")); |
| 342 | } |
| 343 | return Filesystem::fileGetContents($this->getCachefilePath()); |
| 344 | } |
| 345 | |
| 346 | private function convertToStaleCache() |
| 347 | { |
| 348 | $staleFile = $this->getCachefilePath("stale"); |
| 349 | if (!Filesystem::fileExists($staleFile)) { |
| 350 | $freshFile = $this->getCachefilePath(); |
| 351 | if (Filesystem::fileExists($freshFile)) { |
| 352 | $fileInfo = Filesystem::fileGetAll($freshFile); |
| 353 | $newContent = str_replace("NITROPACK_STATE='FRESH'", "NITROPACK_STATE='STALE'", $fileInfo->content); |
| 354 | Filesystem::filePutContents($freshFile, $newContent, $this->headersFlatten($fileInfo->headers)); |
| 355 | Filesystem::rename($freshFile, $staleFile); |
| 356 | Filesystem::deleteFile($freshFile . ".gz"); |
| 357 | $this->compress($staleFile); |
| 358 | } |
| 359 | } |
| 360 | } |
| 361 | |
| 362 | private function cookiePrefix() |
| 363 | { |
| 364 | $prefix = ''; |
| 365 | |
| 366 | $cookies = $this->cookiesProvider ? call_user_func($this->cookiesProvider) : $this->cookies; |
| 367 | |
| 368 | ksort($cookies); |
| 369 | |
| 370 | foreach ($cookies as $cookieName => $cookieValue) { |
| 371 | foreach ($this->supportedCookies as $cookie) { |
| 372 | if (preg_match('/' . NitroPack::wildcardToRegex($cookie) . '/', $cookieName)) { |
| 373 | if (preg_match("/^nitro_geot_(.*)/", $cookieName, $matches)) { |
| 374 | $geotComponent = $matches[1]; |
| 375 | if (property_exists($this->geotVariations, $geotComponent) && !empty($this->geotVariations->$geotComponent) && !in_array($cookieValue, $this->geotVariations->$geotComponent)) { |
| 376 | $prefix .= $cookieName . '=' . $this->geotVariations->{$geotComponent}[0] . ';'; // Use the first variation as default |
| 377 | continue; |
| 378 | } |
| 379 | } |
| 380 | $prefix .= $cookieName . '=' . $cookieValue . ';'; |
| 381 | } |
| 382 | } |
| 383 | } |
| 384 | |
| 385 | return substr(md5($prefix), 0, 16); |
| 386 | } |
| 387 | |
| 388 | private function sslPrefix() |
| 389 | { |
| 390 | return $this->url->getScheme() == "https" ? "ssl-" : ""; |
| 391 | } |
| 392 | |
| 393 | private function ajaxPrefix() |
| 394 | { |
| 395 | return $this->isAjax && $this->parent ? "ajax-" . md5($this->url->getNormalized()) . "-" : ""; |
| 396 | } |
| 397 | |
| 398 | private function customCachePrefix() |
| 399 | { |
| 400 | $customCachePrefix = NitroPack::getCustomCachePrefix(); |
| 401 | return $customCachePrefix ? $customCachePrefix . "-" : ""; |
| 402 | } |
| 403 | |
| 404 | private function isCompressionAllowed() |
| 405 | { |
| 406 | return isset($_SERVER['HTTP_ACCEPT_ENCODING']) && strpos($_SERVER['HTTP_ACCEPT_ENCODING'], 'gzip') !== false; |
| 407 | } |
| 408 | |
| 409 | private function canUseCompression() |
| 410 | { |
| 411 | return $this->useCompression && $this->isCompressionAllowed() && !headers_sent(); |
| 412 | } |
| 413 | |
| 414 | public function nameOfCachefile() |
| 415 | { |
| 416 | return $this->customCachePrefix() . $this->ajaxPrefix() . $this->sslPrefix() . $this->cookiePrefix() . ".html"; |
| 417 | } |
| 418 | |
| 419 | public function getCachefilePath($suffix = "") |
| 420 | { |
| 421 | if ($suffix) $suffix = "." . $suffix; |
| 422 | if ($this->isAjax && $this->referer) { |
| 423 | return self::getUrlDir($this->dataDir, $this->referer->getNormalized(), $this->useInvalidated, $this->urlPathVersion) . "/" . $this->nameOfCachefile() . $suffix; |
| 424 | } |
| 425 | return self::getUrlDir($this->dataDir, $this->url->getNormalized(), $this->useInvalidated, $this->urlPathVersion) . "/" . $this->nameOfCachefile() . $suffix; |
| 426 | } |
| 427 | } |