avatar-privacy/includes/avatar-privacy/data-storage/class-filesystem-cache.php

279 lines
7.7 KiB
PHP

<?php
/**
* This file is part of Avatar Privacy.
*
* Copyright 2018-2023 Peter Putzer.
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*
* ***
*
* @package mundschenk-at/avatar-privacy
* @license http://www.gnu.org/licenses/gpl-2.0.html
*/
namespace Avatar_Privacy\Data_Storage;
use Avatar_Privacy\Exceptions\Filesystem_Exception;
use function Avatar_Privacy\Tools\delete_file;
/**
* A filesystem caching handler.
*
* @since 1.0.0
*
* @author Peter Putzer <github@mundschenk.at>
*
* @phpstan-type UploadDir array{
* path: string,
* url: string,
* subdir: string,
* basedir: string,
* baseurl: string,
* error: string|false
* }
*/
class Filesystem_Cache {
const CACHE_DIR = 'avatar-privacy/cache/';
/**
* The base directory for the filesystem cache.
*
* @var string
*/
private string $base_dir;
/**
* The base URL for accessing cached files.
*
* @var string
*/
private string $base_url;
/**
* Information about the uploads directory.
*
* @var array
*
* @phpstan-var UploadDir
*/
private array $upload_dir;
/**
* Creates a new instance.
*/
public function __construct() {
$this->get_base_dir();
}
/**
* Retrieves the base directory for caching files.
*
* @since 2.4.0 A Filesystem_Exception is thrown instead of a generic \RuntimeException.
*
* @throws Filesystem_Exception An exception is thrown if the cache directory
* does not exist and can't be created.
*
* @return string
*/
public function get_base_dir() {
if ( empty( $this->base_dir ) ) {
$this->base_dir = "{$this->get_upload_dir()['basedir']}/" . self::CACHE_DIR;
if ( ! \wp_mkdir_p( $this->base_dir ) ) {
throw new Filesystem_Exception( "The cache directory {$this->base_dir} could not be created." );
}
}
return $this->base_dir;
}
/**
* Retrieves the base URL for accessing cached files.
*
* @return string
*/
public function get_base_url() {
if ( empty( $this->base_url ) ) {
$this->base_url = "{$this->get_upload_dir()['baseurl']}/" . self::CACHE_DIR;
}
return $this->base_url;
}
/**
* Retrieves information about the upload directory.
*
* @since 2.1.0 Visibility changed to protected.
*
* @return array
*
* @phpstan-return UploadDir
*/
protected function get_upload_dir() {
if ( ! isset( $this->upload_dir ) ) {
$multisite = \is_multisite();
if ( $multisite ) {
\switch_to_blog( \get_main_site_id() );
}
// We only need the basedir, so don't create the monthly sub-directory.
$this->upload_dir = \wp_upload_dir( null, false );
if ( $multisite ) {
\restore_current_blog();
}
}
return $this->upload_dir;
}
/**
* Stores data in the filesystem cache.
*
* @since 2.6.0 The type of the `$data` parameter has been corrected to `string`.
*
* @param string $filename The filename (including any sub directory).
* @param string $data The (possibly binary) data. Will not be cached if empty.
* @param bool $force Optional. The cached file will only be overwritten if set to true. Default false.
*
* @return bool True if the file was successfully stored in the cache, false otherwise.
*/
public function set( $filename, $data, $force = false ) {
$file = $this->get_base_dir() . $filename;
if ( \file_exists( $file ) && ! $force ) {
return true;
}
return ! (
// Don't create empty files.
0 === \strlen( $data ) ||
// Make sure that the file path is valid.
! \wp_mkdir_p( \dirname( $file ) ) ||
// Check if the file has been stored successfully.
false === \file_put_contents( $file, $data, \LOCK_EX ) // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_file_put_contents
);
}
/**
* Retrieves the URL for the cached file.
*
* @param string $filename The filename (including any sub directory).
*
* @return string
*/
public function get_url( $filename ) {
return $this->get_base_url() . $filename;
}
/**
* Removes a file from the cache.
*
* @param string $filename The filename (including any sub directory).
*
* @return bool True if the file was successfully removed from the cache, false otherwise.
*/
public function delete( $filename ) {
$file = $this->get_base_dir() . $filename;
return \wp_is_writable( $file ) && delete_file( $file );
}
/**
* Invalidate all cached elements by recursively deleting all files and directories.
*
* @param string $subdir Optional. Limit invalidation to the given subdirectory. Default ''.
* @param string $regex Optional. Limit invalidation to files matching the given regular expression. Default ''.
*
* @return void
*/
public function invalidate( $subdir = '', $regex = '' ) {
try {
$iterator = $this->get_recursive_file_iterator( $subdir, $regex );
} catch ( \UnexpectedValueException $e ) {
// Ignore non-existing subdirectories.
return;
}
foreach ( $iterator as $path => $file ) {
if ( $file->isWritable() ) {
if ( $file->isDir() ) {
\rmdir( $path ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_rmdir -- Useing the WP_Filesystem API is not an option on the frontend.
} else {
delete_file( $path );
}
}
}
}
/**
* Invalidate all cached files older than the given age.
*
* @param int $age The maximum file age in seconds.
* @param string $subdir Optional. Limit invalidation to the given subdirectory. Default ''.
* @param string $regex Optional. Limit invalidation to files matching the given regular expression. Default ''.
*
* @return void
*/
public function invalidate_files_older_than( $age, $subdir = '', $regex = '' ) {
try {
$now = \time();
$iterator = $this->get_recursive_file_iterator( $subdir, $regex );
} catch ( \UnexpectedValueException $e ) {
// Ignore non-existing subdirectories.
return;
}
foreach ( $iterator as $path => $file ) {
if ( $file->isWritable() && ! $file->isDir() && $now - $file->getMTime() > $age ) {
delete_file( $path );
}
}
}
/**
* Retrieves a recursive iterator for all files in the cache.
*
* @since 2.1.0 Visibility changed to protected.
*
* @param string $subdir Optional. Limit invalidation to the given subdirectory. Default ''.
* @param string $regex Optional. Limit invalidation to files matching the given regular expression. Default ''.
*
* @return \OuterIterator<string,\SplFileInfo>
*/
protected function get_recursive_file_iterator( $subdir = '', $regex = '' ) {
$files = new \RecursiveIteratorIterator(
new \RecursiveDirectoryIterator( "{$this->get_base_dir()}{$subdir}", \FilesystemIterator::KEY_AS_PATHNAME | \FilesystemIterator::CURRENT_AS_FILEINFO | \FilesystemIterator::SKIP_DOTS ),
\RecursiveIteratorIterator::CHILD_FIRST
);
if ( ! empty( $regex ) ) {
/**
* Further filter the collected files using the given regular expression.
*
* @phpstan-var \RegexIterator<string,\SplFileInfo,\RecursiveIteratorIterator<\RecursiveDirectoryIterator<string,\SplFileInfo>>> $files
*/
$files = new \RegexIterator( $files, $regex, \RecursiveRegexIterator::MATCH );
}
return $files;
}
}