. * */ namespace Vvveb\System; use function Vvveb\globBrace; use Vvveb\System\Core\FrontController; class PageCache { const STALE_EXT = '.old'; const LOCK_EXT = '.new'; const CACHE_DIR = PAGE_CACHE_DIR; const MAX_LOCK_SECONDS = 10; static $cacheCookies = ['user', 'cart', 'admin', 'nocache']; private $fileName; private $uri; private $canCache; //can serve cached page private $canSaveCache; //can save page in cache private $cacheFolder; static private $enabled; static private $instance; static function getInstance() { if (! self :: $instance) { self :: $instance = new self(); } return self :: $instance; } function __construct($host = null) { $this->cacheFolder = $this->cacheFolder($host); $this->uri = $this->uri(); if ($this->uri) { $this->fileName = $this->cacheFolder . $this->uri; } //$this->canSaveCache = $this->canSaveCache(); } function cacheFolder($host) { $host = $host ?? $_SERVER['HTTP_HOST'] ?? ''; $hostWp = substr($host, 0, strpos($host, ':') ?: null); return DIR_PUBLIC . self :: CACHE_DIR . ($hostWp ?? 'default'); } function uri($url = null) { $uri = $url ?? $_SERVER['REQUEST_URI'] ?? '/'; if (strlen($uri) > 300) { return false; } if (substr($uri, -1) == '/') { $uri .= 'index.html'; } return $uri; } function isGenerating() { return is_file($this->fileName . self :: LOCK_EXT); } function isGeneratingStuck() { return time() - filemtime($this->fileName . self :: LOCK_EXT) > self :: MAX_LOCK_SECONDS; } function isStale() { return is_file($this->fileName . self :: STALE_EXT); } function getStale() { return $this->fileName . self :: STALE_EXT; } function startGenerating() { $dir = dirname($this->fileName); if (is_dir($this->fileName)) { $this->fileName .= DS . 'index.html'; } if (! is_dir($dir)) { //if page with the same name as folder remove if (file_exists($dir)) { unlink($dir); } if (! mkdir($dir, (0755 & ~umask()), true)) { return false; } } //keep old cache to serve while generating if (file_exists($this->fileName)) { rename($this->fileName, $this->fileName . self :: STALE_EXT); } return touch($this->fileName . self :: LOCK_EXT); } function startCapture() { ob_start(); } function validUrl($url) { //no ?&\ or .. in the url and the number of levels should not exceed 4 $invalid = strpbrk($url, '?&\\') || (strpos($url, '..') !== false) || (substr_count($url,'/') > 4); if (! $invalid) { foreach (['/user', '/cart', '/checkout', '/feed'] as $a) { if (strncmp($url, $a, strlen($a)) === 0) { $invalid = true; break; } } } return ! $invalid; } function canCache() { if ($this->canCache !== NULL) { return $this->canCache; } if (! $this->uri || APP == 'admin' || //no cache for admin APP == 'install' || //no cache for install ! empty($_POST) || //forms posts isset($_COOKIE['nocache']) || //cookie set by plugin isset($_COOKIE['cart']) || // cookie set by add to cart isset($_COOKIE['user']) || //cookie set by login isset($_COOKIE['admin']) || //cookie set by admin login ! $this->validUrl($this->uri) //valid url ) { return $this->canCache = false; } return $this->canCache = true; } function canSaveCache() { if ($this->canSaveCache !== NULL) { return $this->canSaveCache; } if (! $this->canCache || ($user = \Vvveb\System\User\User::current()) || //not logged in ($admin = \Vvveb\System\User\Admin::current())) { return $this->canSaveCache = false; } return $this->canSaveCache = true; } function hasCache() { return is_file($this->fileName); } function getCache() { return readfile($this->fileName); } function cleanUp() { //remove lock and stale cache foreach ([$this->fileName . self :: LOCK_EXT, $this->fileName . self :: STALE_EXT, ] as $file) { if (file_exists($file)) { return unlink($file); } } return false; } function saveCache() { $data = ob_get_contents(); $lock = $this->fileName . self :: LOCK_EXT; //if page not found or server error don't cache if (FrontController::getStatus() != 200) { //remove lock if (file_exists($lock)) { unlink($lock); } //remove all empty created folders while ( ($dir = dirname($this->fileName)) && (strpos($dir, $this->cacheFolder . DS) !== false) && //don't go above site cache folder @rmdir($dir) //try to remove empty folder ) { //go one level up $this->fileName = $dir; } return $data; } if ($this->canSaveCache && $this->fileName && $data && http_response_code() == 200) { //create directory structure if (is_dir($this->fileName)) { $this->fileName .= DS . 'index.html'; } $dir = dirname($this->fileName); if (! file_exists($dir)) { if (! mkdir($dir, (0755 & ~umask()), true)) { return false; } } //save cache file_put_contents($this->fileName, $data); //remove lock if (file_exists($lock)) { unlink($lock); } //remove stale if (file_exists($this->fileName . self :: STALE_EXT)) { @unlink($this->fileName . self :: STALE_EXT); } } return $data; } function purge($path = '/') { $folder = $this->cacheFolder . $path; $glob = ['', '*/*/', '*/']; //$files = glob($name, GLOB_BRACE); $files = globBrace($folder, $glob, '*'); if ($files) { foreach ($files as $file) { if ($file[0] === '.') { continue; } if (is_file($file)) { if (! @unlink($file)) { clearstatcache(false, $file); } } else { /* if (! @rmdir($file)) { clearstatcache(false, $file); } */ } } } return true; } static function enable($type = false) { $cookie = (in_array($type, self :: $cacheCookies) ? $type : 'nocache'); if (! self :: $enabled && isset($_COOKIE[$cookie])) { @setcookie($cookie, '', time() - 3600, '/'); } self :: $enabled = true; } static function disable($type = false) { $cookie = (in_array($type, self :: $cacheCookies) ? $type : 'nocache'); if (self :: $enabled && ! isset($_COOKIE[$cookie])) { @setcookie($cookie, '1', 0, '/'); } self :: $enabled = false; } }