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