/
githubmirror
/
framework
Обзор
Документация
Войти
/
githubmirror
/
framework
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
13.x
src/Illuminate/Cache/FileStore.php
552 строки
14 KB
Hamdy Elbatal
[13.x] Fix FileStore cache deserialization with short timestamps (#60543)
19 июн 2026, 16:38
Не верифицирован
19 июн 2026, 16:38
4e94e77
Код
Авторство
О чём код?
<?php namespace Illuminate\Cache; use Exception; use Illuminate\Contracts\Cache\CanFlushLocks; use Illuminate\Contracts\Cache\LockProvider; use Illuminate\Contracts\Cache\Store; use Illuminate\Contracts\Filesystem\LockTimeoutException; use Illuminate\Filesystem\Filesystem; use Illuminate\Filesystem\LockableFile; use Illuminate\Support\InteractsWithTime; use RuntimeException; class FileStore implements CanFlushLocks, LockProvider, Store { use InteractsWithTime, RetrievesMultipleKeys; /** * The Illuminate Filesystem instance. * * @var \Illuminate\Filesystem\Filesystem */ protected $files; /** * The file cache directory. * * @var string */ protected $directory; /** * The file cache lock directory. * * @var string|null */ protected $lockDirectory; /** * Octal representation of the cache file permissions. * * @var int|null */ protected $filePermission; /** * The classes that should be allowed during unserialization. * * @var array|bool|null */ protected $serializableClasses; /** * Create a new file cache store instance. * * @param \Illuminate\Filesystem\Filesystem $files * @param string $directory * @param int|null $filePermission * @param array|bool|null $serializableClasses */ public function __construct(Filesystem $files, $directory, $filePermission = null, $serializableClasses = null) { $this->files = $files; $this->directory = $directory; $this->filePermission = $filePermission; $this->serializableClasses = $serializableClasses; } /** * Retrieve an item from the cache by key. * * @param string $key * @return mixed */ public function get($key) { return $this->getPayload($key)['data'] ?? null; } /** * Store an item in the cache for a given number of seconds. * * @param string $key * @param mixed $value * @param int $seconds * @return bool */ public function put($key, $value, $seconds) { $this->ensureCacheDirectoryExists($path = $this->path($key)); $result = $this->files->put( $path, str_pad((string) $this->expiration($seconds), 10, '0', STR_PAD_LEFT).serialize($value), true ); if ($result !== false && $result > 0) { $this->ensurePermissionsAreCorrect($path); return true; } return false; } /** * Store an item in the cache if the key doesn't exist. * * @param string $key * @param mixed $value * @param int $seconds * @return bool */ public function add($key, $value, $seconds) { $this->ensureCacheDirectoryExists($path = $this->path($key)); $file = new LockableFile($path, 'c+'); try { $file->getExclusiveLock(); } catch (LockTimeoutException) { $file->close(); return false; } $expire = $file->read(10); if (empty($expire) || $this->currentTime() >= $expire) { $file->truncate() ->write(str_pad((string) $this->expiration($seconds), 10, '0', STR_PAD_LEFT).serialize($value)) ->close(); $this->ensurePermissionsAreCorrect($path); return true; } $file->close(); return false; } /** * Create the file cache directory if necessary. * * @param string $path * @return void */ protected function ensureCacheDirectoryExists($path) { $directory = dirname($path); if (! $this->files->exists($directory)) { $this->files->makeDirectory($directory, 0777, true, true); // We're creating two levels of directories (e.g. 7e/24), so we check them both... $this->ensurePermissionsAreCorrect($directory); $this->ensurePermissionsAreCorrect(dirname($directory)); } } /** * Ensure the created node has the correct permissions. * * @param string $path * @return void */ protected function ensurePermissionsAreCorrect($path) { if (is_null($this->filePermission) || intval($this->files->chmod($path), 8) == $this->filePermission) { return; } $this->files->chmod($path, $this->filePermission); } /** * Increment the value of an item in the cache. * * @param string $key * @param mixed $value * @return int */ public function increment($key, $value = 1) { $raw = $this->getPayload($key); return tap(((int) $raw['data']) + $value, function ($newValue) use ($key, $raw) { $this->put($key, $newValue, $raw['time'] ?? 0); }); } /** * Decrement the value of an item in the cache. * * @param string $key * @param mixed $value * @return int */ public function decrement($key, $value = 1) { return $this->increment($key, $value * -1); } /** * Store an item in the cache indefinitely. * * @param string $key * @param mixed $value * @return bool */ public function forever($key, $value) { return $this->put($key, $value, 0); } /** * Get a lock instance. * * @param string $name * @param int $seconds * @param string|null $owner * @return \Illuminate\Contracts\Cache\Lock */ public function lock($name, $seconds = 0, $owner = null) { $this->ensureCacheDirectoryExists($this->lockDirectory ?? $this->directory); return new FileLock( new static($this->files, $this->lockDirectory ?? $this->directory, $this->filePermission, $this->serializableClasses), "file-store-lock:{$name}", $seconds, $owner ); } /** * Restore a lock instance using the owner identifier. * * @param string $name * @param string $owner * @return \Illuminate\Contracts\Cache\Lock */ public function restoreLock($name, $owner) { return $this->lock($name, 0, $owner); } /** * Atomically refresh the expiration of a cache key if it matches the expected owner. * * @param string $key * @param mixed $expectedOwner * @param int $seconds * @return bool */ public function refreshIfOwned($key, $expectedOwner, $seconds) { $this->ensureCacheDirectoryExists($path = $this->path($key)); $file = new LockableFile($path, 'c+'); try { $file->getExclusiveLock(); } catch (LockTimeoutException) { $file->close(); return false; } $contents = $file->read(); if (strlen($contents) < 10) { $file->close(); return false; } $expire = substr($contents, 0, 10); $currentOwner = $this->unserialize(substr($contents, 10)); if ($currentOwner !== $expectedOwner || $this->currentTime() >= $expire) { $file->close(); return false; } $file->truncate() ->write($this->expiration($seconds).serialize($expectedOwner)) ->close(); $this->ensurePermissionsAreCorrect($path); return true; } /** * Adjust the expiration time of a cached item. * * @param string $key * @param int $seconds * @return bool */ public function touch($key, $seconds) { $payload = $this->getPayload($this->getPrefix().$key); if (is_null($payload['data'])) { return false; } return $this->put($key, $payload['data'], $seconds); } /** * Remove an item from the cache. * * @param string $key * @return bool */ public function forget($key) { if ($this->files->exists($file = $this->path($key))) { return tap($this->files->delete($file), function ($forgotten) use ($key) { if ($forgotten && $this->files->exists($file = $this->path(Repository::FLEXIBLE_CREATED_KEY_PREFIX.$key))) { $this->files->delete($file); } }); } return false; } /** * Remove all items from the cache. * * @return bool */ public function flush() { if (! $this->files->isDirectory($this->directory)) { return false; } foreach ($this->files->directories($this->directory) as $directory) { $deleted = $this->files->deleteDirectory($directory); if (! $deleted || $this->files->exists($directory)) { return false; } } return true; } /** * Remove all locks from the store. * * @return bool * * @throws \RuntimeException */ public function flushLocks(): bool { if (! $this->hasSeparateLockStore()) { throw new RuntimeException('Flushing locks is only supported when the lock store is separate from the cache store.'); } if (! $this->files->isDirectory($this->lockDirectory)) { return false; } foreach ($this->files->directories($this->lockDirectory) as $lockDirectory) { $deleted = $this->files->deleteDirectory($lockDirectory); if (! $deleted || $this->files->exists($lockDirectory)) { return false; } } return true; } /** * Retrieve an item and expiry time from the cache by key. * * @param string $key * @return array */ protected function getPayload($key) { $path = $this->path($key); // If the file doesn't exist, we obviously cannot return the cache so we will // just return null. Otherwise, we'll get the contents of the file and get // the expiration UNIX timestamps from the start of the file's contents. try { if (is_null($contents = $this->files->get($path, true))) { return $this->emptyPayload(); } $expire = substr($contents, 0, 10); } catch (Exception) { return $this->emptyPayload(); } // If the current time is greater than expiration timestamps we will delete // the file and return null. This helps clean up the old files and keeps // this directory much cleaner for us as old files aren't hanging out. if ($this->currentTime() >= $expire) { $this->forget($key); return $this->emptyPayload(); } try { $data = $this->unserialize(substr($contents, 10)); } catch (Exception) { $this->forget($key); return $this->emptyPayload(); } // Next, we'll extract the number of seconds that are remaining for a cache // so that we can properly retain the time for things like the increment // operation that may be performed on this cache on a later operation. $time = $expire - $this->currentTime(); return ['data' => $data, 'time' => $time]; } /** * Unserialize the given value. * * @param string $value * @return mixed */ protected function unserialize($value) { if ($this->serializableClasses !== null) { return unserialize($value, ['allowed_classes' => $this->serializableClasses]); } return unserialize($value); } /** * Get a default empty payload for the cache. * * @return array */ protected function emptyPayload() { return ['data' => null, 'time' => null]; } /** * Get the full path for the given cache key. * * @param string $key * @return string */ public function path($key) { $parts = array_slice(str_split($hash = sha1($key), 2), 0, 2); return $this->directory.'/'.implode('/', $parts).'/'.$hash; } /** * Get the expiration time based on the given seconds. * * @param int $seconds * @return int */ protected function expiration($seconds) { $time = $this->availableAt($seconds); return $seconds === 0 || $time > 9999999999 ? 9999999999 : $time; } /** * Get the Filesystem instance. * * @return \Illuminate\Filesystem\Filesystem */ public function getFilesystem() { return $this->files; } /** * Get the working directory of the cache. * * @return string */ public function getDirectory() { return $this->directory; } /** * Set the working directory of the cache. * * @param string $directory * @return $this */ public function setDirectory($directory) { $this->directory = $directory; return $this; } /** * Set the cache directory where locks should be stored. * * @param string|null $lockDirectory * @return $this */ public function setLockDirectory($lockDirectory) { $this->lockDirectory = $lockDirectory; return $this; } /** * Get the cache key prefix. * * @return string */ public function getPrefix() { return ''; } /** * Determine if the lock store is separate from the cache store. * * @return bool */ public function hasSeparateLockStore(): bool { return $this->lockDirectory !== null && $this->lockDirectory !== $this->directory; } }