281 lines
12 KiB
PHP
281 lines
12 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
/*
|
|
* This file is part of the TYPO3 CMS project.
|
|
*
|
|
* It is free software; you can redistribute it and/or modify it under
|
|
* the terms of the GNU General Public License, either version 2
|
|
* of the License, or any later version.
|
|
*
|
|
* For the full copyright and license information, please read the
|
|
* LICENSE.txt file that was distributed with this source code.
|
|
*
|
|
* The TYPO3 project - inspiring people to share!
|
|
*/
|
|
|
|
namespace TYPO3\CMS\Core\Cache\Backend;
|
|
|
|
use TYPO3\CMS\Core\Cache\Exception;
|
|
use TYPO3\CMS\Core\Cache\Frontend\FrontendInterface;
|
|
use TYPO3\CMS\Core\Cache\Frontend\PhpFrontend;
|
|
use TYPO3\CMS\Core\Core\Environment;
|
|
use TYPO3\CMS\Core\Service\OpcodeCacheService;
|
|
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
|
use TYPO3\CMS\Core\Utility\PathUtility;
|
|
use TYPO3\CMS\Core\Utility\StringUtility;
|
|
|
|
/**
|
|
* A caching backend which stores cache entries in files, but does not support or
|
|
* care about expiry times and tags.
|
|
*/
|
|
class SimpleFileBackend extends AbstractBackend implements PhpCapableBackendInterface
|
|
{
|
|
/**
|
|
* Directory where the files are stored
|
|
*/
|
|
protected string $cacheDirectory = '';
|
|
|
|
/**
|
|
* Temporary path to cache directory before setCache() was called. It is
|
|
* set by setCacheDirectory() and used in setCache() method which calls
|
|
* the directory creation if needed. The variable is not used afterwards,
|
|
* the final cache directory path is stored in $this->cacheDirectory then.
|
|
*/
|
|
protected string $temporaryCacheDirectory = '';
|
|
|
|
/**
|
|
* A file extension to use for each cache entry.
|
|
*/
|
|
protected string $cacheEntryFileExtension = '';
|
|
|
|
public function setCache(FrontendInterface $cache): void
|
|
{
|
|
parent::setCache($cache);
|
|
if (empty($this->temporaryCacheDirectory)) {
|
|
// If no cache directory was given with cacheDirectory
|
|
// configuration option, set it to a path below var/ folder
|
|
$temporaryCacheDirectory = Environment::getVarPath() . '/';
|
|
} else {
|
|
$temporaryCacheDirectory = $this->temporaryCacheDirectory;
|
|
}
|
|
$codeOrData = $cache instanceof PhpFrontend ? 'code' : 'data';
|
|
$finalCacheDirectory = $temporaryCacheDirectory . 'cache/' . $codeOrData . '/' . $this->cacheIdentifier . '/';
|
|
$this->createFinalCacheDirectory($finalCacheDirectory);
|
|
$this->temporaryCacheDirectory = '';
|
|
$this->cacheDirectory = $finalCacheDirectory;
|
|
$this->cacheEntryFileExtension = $cache instanceof PhpFrontend ? '.php' : '';
|
|
if (strlen($this->cacheDirectory) + 23 > PHP_MAXPATHLEN) {
|
|
throw new Exception('The length of the temporary cache file path "' . $this->cacheDirectory . '" exceeds the maximum path length of ' . (PHP_MAXPATHLEN - 23) . '. Please consider setting the temporaryDirectoryBase option to a shorter path.', 1248710426);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Sets the directory where the cache files are stored. By default it is
|
|
* assumed that the directory is below TYPO3's Project Path. However, an
|
|
* absolute path can be selected, too.
|
|
*
|
|
* This method enables to use a cache path outside of TYPO3's Project Path. The final
|
|
* cache path is checked and created in createFinalCacheDirectory(),
|
|
* called by setCache() method, which is done _after_ the cacheDirectory
|
|
* option was handled.
|
|
*
|
|
* @internal Misused in tests
|
|
* @todo: Fix tests and protect
|
|
*/
|
|
public function setCacheDirectory(string $cacheDirectory): void
|
|
{
|
|
$documentRoot = Environment::getProjectPath() . '/';
|
|
if ($open_basedir = ini_get('open_basedir')) {
|
|
if (Environment::isWindows()) {
|
|
$delimiter = ';';
|
|
$cacheDirectory = str_replace('\\', '/', $cacheDirectory);
|
|
if (!preg_match('/[A-Z]:/', substr($cacheDirectory, 0, 2))) {
|
|
$cacheDirectory = Environment::getProjectPath() . $cacheDirectory;
|
|
}
|
|
} else {
|
|
$delimiter = ':';
|
|
if ($cacheDirectory[0] !== '/') {
|
|
// relative path to cache directory.
|
|
$cacheDirectory = Environment::getProjectPath() . $cacheDirectory;
|
|
}
|
|
}
|
|
$basedirs = explode($delimiter, $open_basedir);
|
|
$cacheDirectoryInBaseDir = false;
|
|
foreach ($basedirs as $basedir) {
|
|
if (Environment::isWindows()) {
|
|
$basedir = str_replace('\\', '/', $basedir);
|
|
}
|
|
if ($basedir[strlen($basedir) - 1] !== '/') {
|
|
$basedir .= '/';
|
|
}
|
|
if (str_starts_with($cacheDirectory, $basedir)) {
|
|
$documentRoot = $basedir;
|
|
$cacheDirectory = str_replace($basedir, '', $cacheDirectory);
|
|
$cacheDirectoryInBaseDir = true;
|
|
break;
|
|
}
|
|
}
|
|
if (!$cacheDirectoryInBaseDir) {
|
|
throw new Exception(
|
|
'Open_basedir restriction in effect. The directory "' . $cacheDirectory . '" is not in an allowed path.',
|
|
1476045417
|
|
);
|
|
}
|
|
} else {
|
|
if ($cacheDirectory[0] === '/') {
|
|
// Absolute path to cache directory.
|
|
$documentRoot = '';
|
|
}
|
|
if (Environment::isWindows() && (!empty($documentRoot) && str_starts_with($cacheDirectory, $documentRoot))) {
|
|
$documentRoot = '';
|
|
}
|
|
}
|
|
// After this point all paths have '/' as directory separator
|
|
if ($cacheDirectory[strlen($cacheDirectory) - 1] !== '/') {
|
|
$cacheDirectory .= '/';
|
|
}
|
|
$this->temporaryCacheDirectory = $documentRoot . $cacheDirectory;
|
|
}
|
|
|
|
/**
|
|
* Create the final cache directory if it does not exist.
|
|
*/
|
|
protected function createFinalCacheDirectory(string $finalCacheDirectory): void
|
|
{
|
|
if (!is_dir($finalCacheDirectory)) {
|
|
try {
|
|
GeneralUtility::mkdir_deep($finalCacheDirectory);
|
|
} catch (\RuntimeException $e) {
|
|
throw new Exception('The directory "' . $finalCacheDirectory . '" can not be created.', 1303669848, $e);
|
|
}
|
|
}
|
|
if (!is_writable($finalCacheDirectory)) {
|
|
throw new Exception('The directory "' . $finalCacheDirectory . '" is not writable.', 1203965200);
|
|
}
|
|
$tmpFilesCacheDirectory = $finalCacheDirectory . 'tmp/';
|
|
if (!is_dir($tmpFilesCacheDirectory)) {
|
|
try {
|
|
GeneralUtility::mkdir_deep($tmpFilesCacheDirectory);
|
|
} catch (\RuntimeException $e) {
|
|
throw new Exception('The temporary cache directory "' . $tmpFilesCacheDirectory . '" can not be created.', 1727176780, $e);
|
|
}
|
|
}
|
|
if (!is_writable($tmpFilesCacheDirectory)) {
|
|
throw new Exception('The temporary cache directory "' . $tmpFilesCacheDirectory . '" is not writable.', 1727176781);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns the directory where the cache files are stored
|
|
*
|
|
* @return string Full path of the cache directory
|
|
* @internal Misused in tests
|
|
* @todo: Fix tests and protect
|
|
*/
|
|
public function getCacheDirectory(): string
|
|
{
|
|
return $this->cacheDirectory;
|
|
}
|
|
|
|
public function set(string $entryIdentifier, string $data, array $tags = [], ?int $lifetime = null): void
|
|
{
|
|
if ($entryIdentifier !== PathUtility::basename($entryIdentifier)) {
|
|
throw new \InvalidArgumentException('The specified entry identifier must not contain a path segment.', 1334756735);
|
|
}
|
|
if ($entryIdentifier === '') {
|
|
throw new \InvalidArgumentException('The specified entry identifier must not be empty.', 1334756736);
|
|
}
|
|
$temporaryCacheEntryPathAndFilename = $this->cacheDirectory . 'tmp/' . StringUtility::getUniqueId() . '.temp';
|
|
$result = GeneralUtility::writeFile($temporaryCacheEntryPathAndFilename, $data, true);
|
|
if ($result === false) {
|
|
throw new Exception('The temporary cache file "' . $temporaryCacheEntryPathAndFilename . '" could not be written.', 1334756737);
|
|
}
|
|
$cacheEntryPathAndFilename = $this->cacheDirectory . $entryIdentifier . $this->cacheEntryFileExtension;
|
|
$result = @rename($temporaryCacheEntryPathAndFilename, $cacheEntryPathAndFilename);
|
|
if ($result === false) {
|
|
throw new Exception('The cache file "' . $cacheEntryPathAndFilename . '" could not be written.', 1727178709);
|
|
}
|
|
if ($this->cacheEntryFileExtension === '.php') {
|
|
GeneralUtility::makeInstance(OpcodeCacheService::class)->clearAllActive($cacheEntryPathAndFilename);
|
|
}
|
|
}
|
|
|
|
public function get(string $entryIdentifier): false|string
|
|
{
|
|
if ($entryIdentifier !== PathUtility::basename($entryIdentifier)) {
|
|
throw new \InvalidArgumentException('The specified entry identifier must not contain a path segment.', 1334756877);
|
|
}
|
|
$pathAndFilename = $this->cacheDirectory . $entryIdentifier . $this->cacheEntryFileExtension;
|
|
if (!file_exists($pathAndFilename)) {
|
|
return false;
|
|
}
|
|
return file_get_contents($pathAndFilename);
|
|
}
|
|
|
|
public function has(string $entryIdentifier): bool
|
|
{
|
|
if ($entryIdentifier !== PathUtility::basename($entryIdentifier)) {
|
|
throw new \InvalidArgumentException('The specified entry identifier must not contain a path segment.', 1334756878);
|
|
}
|
|
return file_exists($this->cacheDirectory . $entryIdentifier . $this->cacheEntryFileExtension);
|
|
}
|
|
|
|
public function remove(string $entryIdentifier): bool
|
|
{
|
|
if ($entryIdentifier !== PathUtility::basename($entryIdentifier)) {
|
|
throw new \InvalidArgumentException('The specified entry identifier must not contain a path segment.', 1334756960);
|
|
}
|
|
if ($entryIdentifier === '') {
|
|
throw new \InvalidArgumentException('The specified entry identifier must not be empty.', 1334756961);
|
|
}
|
|
$file = $this->cacheDirectory . $entryIdentifier . $this->cacheEntryFileExtension;
|
|
return @unlink($file);
|
|
}
|
|
|
|
public function flush(): void
|
|
{
|
|
$directoryIterator = new \DirectoryIterator($this->cacheDirectory);
|
|
foreach ($directoryIterator as $fileInfo) {
|
|
if (!$fileInfo->isFile()) {
|
|
continue;
|
|
}
|
|
if (@unlink($this->cacheDirectory . $fileInfo->getFilename())) {
|
|
continue;
|
|
}
|
|
$this->logger->error('Failed to unlink cache entry: {filename}', [
|
|
'filename' => $this->cacheDirectory . $fileInfo->getFilename(),
|
|
]);
|
|
}
|
|
}
|
|
|
|
protected function isCacheFileExpired(string $cacheEntryPathAndFilename): bool
|
|
{
|
|
return file_exists($cacheEntryPathAndFilename) === false;
|
|
}
|
|
|
|
/**
|
|
* No-op
|
|
*/
|
|
public function collectGarbage(): void {}
|
|
|
|
public function requireOnce(string $entryIdentifier): mixed
|
|
{
|
|
$pathAndFilename = $this->cacheDirectory . $entryIdentifier . $this->cacheEntryFileExtension;
|
|
if ($entryIdentifier !== PathUtility::basename($entryIdentifier)) {
|
|
throw new \InvalidArgumentException('The specified entry identifier must not contain a path segment.', 1282073037);
|
|
}
|
|
return file_exists($pathAndFilename) ? require_once $pathAndFilename : false;
|
|
}
|
|
|
|
public function require(string $entryIdentifier): mixed
|
|
{
|
|
$pathAndFilename = $this->cacheDirectory . $entryIdentifier . $this->cacheEntryFileExtension;
|
|
if ($entryIdentifier !== PathUtility::basename($entryIdentifier)) {
|
|
throw new \InvalidArgumentException('The specified entry identifier must not contain a path segment.', 1532528267);
|
|
}
|
|
return file_exists($pathAndFilename) ? require $pathAndFilename : false;
|
|
}
|
|
}
|