diff options
Diffstat (limited to 'classes/diskcache.php')
-rw-r--r-- | classes/diskcache.php | 262 |
1 files changed, 122 insertions, 140 deletions
diff --git a/classes/diskcache.php b/classes/diskcache.php index 34bba25f1..2a3f8c8d7 100644 --- a/classes/diskcache.php +++ b/classes/diskcache.php @@ -1,15 +1,17 @@ <?php -class DiskCache { - // TODO: class properties can be switched to PHP typing if/when the minimum PHP_VERSION is raised to 7.4.0+ - /** @var string */ - private $dir; +class DiskCache implements Cache_Adapter { + /** @var Cache_Adapter $adapter */ + private $adapter; + + /** @var array<string, DiskCache> $instances */ + private static $instances = []; /** * https://stackoverflow.com/a/53662733 * * @var array<string, string> */ - private $mimeMap = [ + private array $mimeMap = [ 'video/3gpp2' => '3g2', 'video/3gp' => '3gp', 'video/3gpp' => '3gp', @@ -196,48 +198,60 @@ class DiskCache { 'text/x-scriptzsh' => 'zsh' ]; + public static function instance(string $dir) : DiskCache { + if ((self::$instances[$dir] ?? null) == null) + self::$instances[$dir] = new self($dir); + + return self::$instances[$dir]; + } + public function __construct(string $dir) { - $this->dir = Config::get(Config::CACHE_DIR) . "/" . basename(clean($dir)); + foreach (PluginHost::getInstance()->get_plugins() as $n => $p) { + if (implements_interface($p, "Cache_Adapter")) { + + /** @var Cache_Adapter $p */ + $this->adapter = clone $p; // we need separate object instances for separate directories + $this->adapter->set_dir($dir); + return; + } + } + + $this->adapter = new Cache_Local(); + $this->adapter->set_dir($dir); } - public function get_dir(): string { - return $this->dir; + public function remove(string $filename): bool { + return $this->adapter->remove($filename); + } + + public function set_dir(string $dir) : void { + $this->adapter->set_dir($dir); + } + + /** + * @return int|false -1 if the file doesn't exist, false if an error occurred, timestamp otherwise + */ + public function get_mtime(string $filename) { + return $this->adapter->get_mtime(basename($filename)); } public function make_dir(): bool { - if (!is_dir($this->dir)) { - return mkdir($this->dir); - } - return false; + return $this->adapter->make_dir(); } public function is_writable(?string $filename = null): bool { - if ($filename) { - if (file_exists($this->get_full_path($filename))) - return is_writable($this->get_full_path($filename)); - else - return is_writable($this->dir); - } else { - return is_writable($this->dir); - } + return $this->adapter->is_writable(basename($filename)); } public function exists(string $filename): bool { - return file_exists($this->get_full_path($filename)); + return $this->adapter->exists(basename($filename)); } /** * @return int|false -1 if the file doesn't exist, false if an error occurred, size in bytes otherwise */ public function get_size(string $filename) { - if ($this->exists($filename)) - return filesize($this->get_full_path($filename)); - else - return -1; - } - - public function get_full_path(string $filename): string { - return $this->dir . "/" . basename(clean($filename)); + return $this->adapter->get_size(basename($filename)); } /** @@ -246,11 +260,27 @@ class DiskCache { * @return int|false Bytes written or false if an error occurred. */ public function put(string $filename, $data) { - return file_put_contents($this->get_full_path($filename), $data); + return $this->adapter->put(basename($filename), $data); } + /** @deprecated we can't assume cached files are local, and other storages + * might not support this operation (object metadata may be immutable) */ public function touch(string $filename): bool { - return touch($this->get_full_path($filename)); + user_error("DiskCache: called unsupported method touch() for $filename", E_USER_DEPRECATED); + + return false; + } + + public function get(string $filename): ?string { + return $this->adapter->get(basename($filename)); + } + + public function expire_all(): void { + $this->adapter->expire_all(); + } + + public function get_dir(): string { + return $this->adapter->get_dir(); } /** Downloads $url to cache as $local_filename if its missing (unless $force-ed) @@ -273,48 +303,80 @@ class DiskCache { return false; } - public function get(string $filename): ?string { - if ($this->exists($filename)) - return file_get_contents($this->get_full_path($filename)); - else - return null; - } + public function send(string $filename) { + $filename = basename($filename); - /** - * @return false|null|string false if detection failed, null if the file doesn't exist, string mime content type otherwise - */ - public function get_mime_type(string $filename) { - if ($this->exists($filename)) - return mime_content_type($this->get_full_path($filename)); - else - return null; - } + if (!$this->exists($filename)) { + header($_SERVER["SERVER_PROTOCOL"]." 404 Not Found"); + echo "File not found."; + return false; + } + + $file_mtime = $this->get_mtime($filename); + $gmt_modified = gmdate("D, d M Y H:i:s", (int)$file_mtime) . " GMT"; + + if (($_SERVER['HTTP_IF_MODIFIED_SINCE'] ?? '') == $gmt_modified || ($_SERVER['HTTP_IF_NONE_MATCH'] ?? '') == $file_mtime) { + header('HTTP/1.1 304 Not Modified'); + return false; + } - public function get_fake_extension(string $filename): string { $mimetype = $this->get_mime_type($filename); - if ($mimetype) - return isset($this->mimeMap[$mimetype]) ? $this->mimeMap[$mimetype] : ""; - else - return ""; - } + if ($mimetype == "application/octet-stream") + $mimetype = "video/mp4"; + + # block SVG because of possible embedded javascript (.....) + $mimetype_blacklist = [ "image/svg+xml" ]; + + /* only serve video and images */ + if (!preg_match("/(image|audio|video)\//", (string)$mimetype) || in_array($mimetype, $mimetype_blacklist)) { + http_response_code(400); + header("Content-type: text/plain"); + + print "Stored file has disallowed content type ($mimetype)"; + return false; + } - /** - * @return bool|int false if the file doesn't exist (or unreadable) or isn't audio/video, true if a plugin handled, otherwise int of bytes sent - */ - public function send(string $filename) { $fake_extension = $this->get_fake_extension($filename); if ($fake_extension) $fake_extension = ".$fake_extension"; - header("Content-Disposition: inline; filename=\"${filename}${fake_extension}\""); + header("Content-Disposition: inline; filename=\"{$filename}{$fake_extension}\""); + header("Content-type: $mimetype"); - return $this->send_local_file($this->get_full_path($filename)); + $stamp_expires = gmdate("D, d M Y H:i:s", + (int)$this->get_mtime($filename) + 86400 * Config::get(Config::CACHE_MAX_DAYS)) . " GMT"; + + header("Expires: $stamp_expires", true); + header("Last-Modified: $gmt_modified", true); + header("Cache-Control: no-cache"); + header("ETag: $file_mtime"); + + header_remove("Pragma"); + + return $this->adapter->send($filename); + } + + public function get_full_path(string $filename): string { + return $this->adapter->get_full_path(basename($filename)); + } + + public function get_mime_type(string $filename) { + return $this->adapter->get_mime_type(basename($filename)); + } + + public function get_fake_extension(string $filename): string { + $mimetype = $this->adapter->get_mime_type(basename($filename)); + + if ($mimetype) + return isset($this->mimeMap[$mimetype]) ? $this->mimeMap[$mimetype] : ""; + else + return ""; } public function get_url(string $filename): string { - return Config::get_self_url() . "/public.php?op=cached&file=" . basename($this->dir) . "/" . basename($filename); + return Config::get_self_url() . "/public.php?op=cached&file=" . basename($this->adapter->get_dir()) . "/" . basename($filename); } // check for locally cached (media) URLs and rewrite to local versions @@ -328,7 +390,7 @@ class DiskCache { $doc = new DOMDocument(); if (@$doc->loadHTML('<?xml encoding="UTF-8">' . $res)) { $xpath = new DOMXPath($doc); - $cache = new DiskCache("images"); + $cache = DiskCache::instance("images"); $entries = $xpath->query('(//img[@src]|//source[@src|@srcset]|//video[@poster|@src])'); @@ -377,84 +439,4 @@ class DiskCache { } return $res; } - - static function expire(): void { - $dirs = array_filter(glob(Config::get(Config::CACHE_DIR) . "/*"), "is_dir"); - - foreach ($dirs as $cache_dir) { - $num_deleted = 0; - - if (is_writable($cache_dir) && !file_exists("$cache_dir/.no-auto-expiry")) { - $files = glob("$cache_dir/*"); - - if ($files) { - foreach ($files as $file) { - if (time() - filemtime($file) > 86400*Config::get(Config::CACHE_MAX_DAYS)) { - unlink($file); - - ++$num_deleted; - } - } - } - - Debug::log("Expired $cache_dir: removed $num_deleted files."); - } - } - } - - /* */ - /** - * this is essentially a wrapper for readfile() which allows plugins to hook - * output with httpd-specific "fast" implementation i.e. X-Sendfile or whatever else - * - * hook function should return true if request was handled (or at least attempted to) - * - * note that this can be called without user context so the plugin to handle this - * should be loaded systemwide in config.php - * - * @return bool|int false if the file doesn't exist (or unreadable) or isn't audio/video, true if a plugin handled, otherwise int of bytes sent - */ - function send_local_file(string $filename) { - if (file_exists($filename)) { - - if (is_writable($filename)) touch($filename); - - $mimetype = mime_content_type($filename); - - // this is hardly ideal but 1) only media is cached in images/ and 2) seemingly only mp4 - // video files are detected as octet-stream by mime_content_type() - - if ($mimetype == "application/octet-stream") - $mimetype = "video/mp4"; - - # block SVG because of possible embedded javascript (.....) - $mimetype_blacklist = [ "image/svg+xml" ]; - - /* only serve video and images */ - if (!preg_match("/(image|audio|video)\//", (string)$mimetype) || in_array($mimetype, $mimetype_blacklist)) { - http_response_code(400); - header("Content-type: text/plain"); - - print "Stored file has disallowed content type ($mimetype)"; - return false; - } - - $tmppluginhost = new PluginHost(); - - $tmppluginhost->load(Config::get(Config::PLUGINS), PluginHost::KIND_SYSTEM); - //$tmppluginhost->load_data(); - - if ($tmppluginhost->run_hooks_until(PluginHost::HOOK_SEND_LOCAL_FILE, true, $filename)) - return true; - - header("Content-type: $mimetype"); - - $stamp = gmdate("D, d M Y H:i:s", (int)filemtime($filename)) . " GMT"; - header("Last-Modified: $stamp", true); - - return readfile($filename); - } else { - return false; - } - } } |