VvebOIDC/system/page-cache.php

315 lines
6.8 KiB
PHP

<?php
/**
* Vvveb
*
* Copyright (C) 2022 Ziadin Givan
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
*/
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;
}
}