1<?php 2 3/* 4 * This file is part of the Symfony package. 5 * 6 * (c) Fabien Potencier <fabien@symfony.com> 7 * 8 * For the full copyright and license information, please view the LICENSE 9 * file that was distributed with this source code. 10 */ 11 12namespace Symfony\Component\Cache\Traits; 13 14use Symfony\Component\Cache\Exception\InvalidArgumentException; 15 16/** 17 * @author Nicolas Grekas <p@tchwork.com> 18 * 19 * @internal 20 */ 21trait FilesystemCommonTrait 22{ 23 private $directory; 24 private $tmp; 25 26 private function init(string $namespace, ?string $directory) 27 { 28 if (!isset($directory[0])) { 29 $directory = sys_get_temp_dir().\DIRECTORY_SEPARATOR.'symfony-cache'; 30 } else { 31 $directory = realpath($directory) ?: $directory; 32 } 33 if (isset($namespace[0])) { 34 if (preg_match('#[^-+_.A-Za-z0-9]#', $namespace, $match)) { 35 throw new InvalidArgumentException(sprintf('Namespace contains "%s" but only characters in [-+_.A-Za-z0-9] are allowed.', $match[0])); 36 } 37 $directory .= \DIRECTORY_SEPARATOR.$namespace; 38 } else { 39 $directory .= \DIRECTORY_SEPARATOR.'@'; 40 } 41 if (!file_exists($directory)) { 42 @mkdir($directory, 0777, true); 43 } 44 $directory .= \DIRECTORY_SEPARATOR; 45 // On Windows the whole path is limited to 258 chars 46 if ('\\' === \DIRECTORY_SEPARATOR && \strlen($directory) > 234) { 47 throw new InvalidArgumentException(sprintf('Cache directory too long (%s).', $directory)); 48 } 49 50 $this->directory = $directory; 51 } 52 53 /** 54 * {@inheritdoc} 55 */ 56 protected function doClear($namespace) 57 { 58 $ok = true; 59 60 foreach ($this->scanHashDir($this->directory) as $file) { 61 if ('' !== $namespace && !str_starts_with($this->getFileKey($file), $namespace)) { 62 continue; 63 } 64 65 $ok = ($this->doUnlink($file) || !file_exists($file)) && $ok; 66 } 67 68 return $ok; 69 } 70 71 /** 72 * {@inheritdoc} 73 */ 74 protected function doDelete(array $ids) 75 { 76 $ok = true; 77 78 foreach ($ids as $id) { 79 $file = $this->getFile($id); 80 $ok = (!file_exists($file) || $this->doUnlink($file) || !file_exists($file)) && $ok; 81 } 82 83 return $ok; 84 } 85 86 protected function doUnlink($file) 87 { 88 return @unlink($file); 89 } 90 91 private function write(string $file, string $data, int $expiresAt = null) 92 { 93 set_error_handler(__CLASS__.'::throwError'); 94 try { 95 if (null === $this->tmp) { 96 $this->tmp = $this->directory.bin2hex(random_bytes(6)); 97 } 98 try { 99 $h = fopen($this->tmp, 'x'); 100 } catch (\ErrorException $e) { 101 if (!str_contains($e->getMessage(), 'File exists')) { 102 throw $e; 103 } 104 105 $this->tmp = $this->directory.bin2hex(random_bytes(6)); 106 $h = fopen($this->tmp, 'x'); 107 } 108 fwrite($h, $data); 109 fclose($h); 110 111 if (null !== $expiresAt) { 112 touch($this->tmp, $expiresAt); 113 } 114 115 return rename($this->tmp, $file); 116 } finally { 117 restore_error_handler(); 118 } 119 } 120 121 private function getFile(string $id, bool $mkdir = false, string $directory = null) 122 { 123 // Use MD5 to favor speed over security, which is not an issue here 124 $hash = str_replace('/', '-', base64_encode(hash('md5', static::class.$id, true))); 125 $dir = ($directory ?? $this->directory).strtoupper($hash[0].\DIRECTORY_SEPARATOR.$hash[1].\DIRECTORY_SEPARATOR); 126 127 if ($mkdir && !file_exists($dir)) { 128 @mkdir($dir, 0777, true); 129 } 130 131 return $dir.substr($hash, 2, 20); 132 } 133 134 private function getFileKey(string $file): string 135 { 136 return ''; 137 } 138 139 private function scanHashDir(string $directory): \Generator 140 { 141 if (!file_exists($directory)) { 142 return; 143 } 144 145 $chars = '+-ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'; 146 147 for ($i = 0; $i < 38; ++$i) { 148 if (!file_exists($directory.$chars[$i])) { 149 continue; 150 } 151 152 for ($j = 0; $j < 38; ++$j) { 153 if (!file_exists($dir = $directory.$chars[$i].\DIRECTORY_SEPARATOR.$chars[$j])) { 154 continue; 155 } 156 157 foreach (@scandir($dir, \SCANDIR_SORT_NONE) ?: [] as $file) { 158 if ('.' !== $file && '..' !== $file) { 159 yield $dir.\DIRECTORY_SEPARATOR.$file; 160 } 161 } 162 } 163 } 164 } 165 166 /** 167 * @internal 168 */ 169 public static function throwError($type, $message, $file, $line) 170 { 171 throw new \ErrorException($message, 0, $type, $file, $line); 172 } 173 174 /** 175 * @return array 176 */ 177 public function __sleep() 178 { 179 throw new \BadMethodCallException('Cannot serialize '.__CLASS__); 180 } 181 182 public function __wakeup() 183 { 184 throw new \BadMethodCallException('Cannot unserialize '.__CLASS__); 185 } 186 187 public function __destruct() 188 { 189 if (method_exists(parent::class, '__destruct')) { 190 parent::__destruct(); 191 } 192 if (null !== $this->tmp && file_exists($this->tmp)) { 193 unlink($this->tmp); 194 } 195 } 196} 197