1 | <?php |
||
2 | |||
3 | namespace Doctrine\Common\Cache; |
||
4 | |||
5 | use FilesystemIterator; |
||
6 | use InvalidArgumentException; |
||
7 | use Iterator; |
||
8 | use RecursiveDirectoryIterator; |
||
9 | use RecursiveIteratorIterator; |
||
10 | use const DIRECTORY_SEPARATOR; |
||
11 | use const PATHINFO_DIRNAME; |
||
12 | use function bin2hex; |
||
13 | use function chmod; |
||
14 | use function defined; |
||
15 | use function disk_free_space; |
||
16 | use function file_exists; |
||
17 | use function file_put_contents; |
||
18 | use function gettype; |
||
19 | use function hash; |
||
20 | use function is_dir; |
||
21 | use function is_int; |
||
22 | use function is_writable; |
||
23 | use function mkdir; |
||
24 | use function pathinfo; |
||
25 | use function realpath; |
||
26 | use function rename; |
||
27 | use function rmdir; |
||
28 | use function sprintf; |
||
29 | use function strlen; |
||
30 | use function strrpos; |
||
31 | use function substr; |
||
32 | use function tempnam; |
||
33 | use function unlink; |
||
34 | |||
35 | /** |
||
36 | * Base file cache driver. |
||
37 | */ |
||
38 | abstract class FileCache extends CacheProvider |
||
39 | { |
||
40 | /** |
||
41 | * The cache directory. |
||
42 | * |
||
43 | * @var string |
||
44 | */ |
||
45 | protected $directory; |
||
46 | |||
47 | /** |
||
48 | * The cache file extension. |
||
49 | * |
||
50 | * @var string |
||
51 | */ |
||
52 | private $extension; |
||
53 | |||
54 | /** @var int */ |
||
55 | private $umask; |
||
56 | |||
57 | /** @var int */ |
||
58 | private $directoryStringLength; |
||
59 | |||
60 | /** @var int */ |
||
61 | private $extensionStringLength; |
||
62 | |||
63 | /** @var bool */ |
||
64 | private $isRunningOnWindows; |
||
65 | |||
66 | /** |
||
67 | * @param string $directory The cache directory. |
||
68 | * @param string $extension The cache file extension. |
||
69 | * |
||
70 | * @throws InvalidArgumentException |
||
71 | */ |
||
72 | 168 | public function __construct($directory, $extension = '', $umask = 0002) |
|
73 | { |
||
74 | // YES, this needs to be *before* createPathIfNeeded() |
||
75 | 168 | if (! is_int($umask)) { |
|
76 | 1 | throw new InvalidArgumentException(sprintf( |
|
77 | 1 | 'The umask parameter is required to be integer, was: %s', |
|
78 | 1 | gettype($umask) |
|
79 | )); |
||
80 | } |
||
81 | 167 | $this->umask = $umask; |
|
82 | |||
83 | 167 | if (! $this->createPathIfNeeded($directory)) { |
|
84 | throw new InvalidArgumentException(sprintf( |
||
85 | 'The directory "%s" does not exist and could not be created.', |
||
86 | $directory |
||
87 | )); |
||
88 | } |
||
89 | |||
90 | 167 | if (! is_writable($directory)) { |
|
91 | throw new InvalidArgumentException(sprintf( |
||
92 | 'The directory "%s" is not writable.', |
||
93 | $directory |
||
94 | )); |
||
95 | } |
||
96 | |||
97 | // YES, this needs to be *after* createPathIfNeeded() |
||
98 | 167 | $this->directory = realpath($directory); |
|
99 | 167 | $this->extension = (string) $extension; |
|
100 | |||
101 | 167 | $this->directoryStringLength = strlen($this->directory); |
|
102 | 167 | $this->extensionStringLength = strlen($this->extension); |
|
103 | 167 | $this->isRunningOnWindows = defined('PHP_WINDOWS_VERSION_BUILD'); |
|
104 | 167 | } |
|
105 | |||
106 | /** |
||
107 | * Gets the cache directory. |
||
108 | * |
||
109 | * @return string |
||
110 | */ |
||
111 | 1 | public function getDirectory() |
|
112 | { |
||
113 | 1 | return $this->directory; |
|
114 | } |
||
115 | |||
116 | /** |
||
117 | * Gets the cache file extension. |
||
118 | * |
||
119 | * @return string |
||
120 | */ |
||
121 | 1 | public function getExtension() |
|
122 | { |
||
123 | 1 | return $this->extension; |
|
124 | } |
||
125 | |||
126 | /** |
||
127 | * @param string $id |
||
128 | * |
||
129 | * @return string |
||
130 | */ |
||
131 | 164 | protected function getFilename($id) |
|
132 | { |
||
133 | 164 | $hash = hash('sha256', $id); |
|
134 | |||
135 | // This ensures that the filename is unique and that there are no invalid chars in it. |
||
136 | 164 | if ($id === '' |
|
137 | 164 | || ((strlen($id) * 2 + $this->extensionStringLength) > 255) |
|
138 | 164 | || ($this->isRunningOnWindows && ($this->directoryStringLength + 4 + strlen($id) * 2 + $this->extensionStringLength) > 258) |
|
139 | ) { |
||
140 | // Most filesystems have a limit of 255 chars for each path component. On Windows the the whole path is limited |
||
141 | // to 260 chars (including terminating null char). Using long UNC ("\\?\" prefix) does not work with the PHP API. |
||
142 | // And there is a bug in PHP (https://bugs.php.net/bug.php?id=70943) with path lengths of 259. |
||
143 | // So if the id in hex representation would surpass the limit, we use the hash instead. The prefix prevents |
||
144 | // collisions between the hash and bin2hex. |
||
145 | 12 | $filename = '_' . $hash; |
|
146 | } else { |
||
147 | 162 | $filename = bin2hex($id); |
|
148 | } |
||
149 | |||
150 | 164 | return $this->directory |
|
151 | 164 | . DIRECTORY_SEPARATOR |
|
152 | 164 | . substr($hash, 0, 2) |
|
153 | 164 | . DIRECTORY_SEPARATOR |
|
154 | 164 | . $filename |
|
155 | 164 | . $this->extension; |
|
156 | } |
||
157 | |||
158 | /** |
||
159 | * {@inheritdoc} |
||
160 | */ |
||
161 | 88 | protected function doDelete($id) |
|
162 | { |
||
163 | 88 | $filename = $this->getFilename($id); |
|
164 | |||
165 | 88 | return @unlink($filename) || ! file_exists($filename); |
|
166 | } |
||
167 | |||
168 | /** |
||
169 | * {@inheritdoc} |
||
170 | */ |
||
171 | 6 | protected function doFlush() |
|
172 | { |
||
173 | 6 | foreach ($this->getIterator() as $name => $file) { |
|
174 | 6 | if ($file->isDir()) { |
|
175 | // Remove the intermediate directories which have been created to balance the tree. It only takes effect |
||
176 | // if the directory is empty. If several caches share the same directory but with different file extensions, |
||
177 | // the other ones are not removed. |
||
178 | 6 | @rmdir($name); |
|
179 | 6 | } elseif ($this->isFilenameEndingWithExtension($name)) { |
|
180 | // If an extension is set, only remove files which end with the given extension. |
||
181 | // If no extension is set, we have no other choice than removing everything. |
||
182 | 6 | @unlink($name); |
|
183 | } |
||
184 | } |
||
185 | |||
186 | 6 | return true; |
|
187 | } |
||
188 | |||
189 | /** |
||
190 | * {@inheritdoc} |
||
191 | */ |
||
192 | 4 | protected function doGetStats() |
|
193 | { |
||
194 | 4 | $usage = 0; |
|
195 | 4 | foreach ($this->getIterator() as $name => $file) { |
|
196 | 2 | if ($file->isDir() || ! $this->isFilenameEndingWithExtension($name)) { |
|
197 | 2 | continue; |
|
198 | } |
||
199 | |||
200 | 2 | $usage += $file->getSize(); |
|
201 | } |
||
202 | |||
203 | 4 | $free = disk_free_space($this->directory); |
|
204 | |||
205 | return [ |
||
206 | 4 | Cache::STATS_HITS => null, |
|
207 | Cache::STATS_MISSES => null, |
||
208 | Cache::STATS_UPTIME => null, |
||
209 | 4 | Cache::STATS_MEMORY_USAGE => $usage, |
|
210 | 4 | Cache::STATS_MEMORY_AVAILABLE => $free, |
|
211 | ]; |
||
212 | } |
||
213 | |||
214 | /** |
||
215 | * Create path if needed. |
||
216 | * |
||
217 | * @return bool TRUE on success or if path already exists, FALSE if path cannot be created. |
||
218 | */ |
||
219 | 167 | private function createPathIfNeeded(string $path) : bool |
|
220 | { |
||
221 | 167 | if (! is_dir($path)) { |
|
222 | 163 | if (@mkdir($path, 0777 & (~$this->umask), true) === false && ! is_dir($path)) { |
|
223 | return false; |
||
224 | } |
||
225 | } |
||
226 | |||
227 | 167 | return true; |
|
228 | } |
||
229 | |||
230 | /** |
||
231 | * Writes a string content to file in an atomic way. |
||
232 | * |
||
233 | * @param string $filename Path to the file where to write the data. |
||
234 | * @param string $content The content to write |
||
235 | * |
||
236 | * @return bool TRUE on success, FALSE if path cannot be created, if path is not writable or an any other error. |
||
237 | */ |
||
238 | 155 | protected function writeFile(string $filename, string $content) : bool |
|
239 | { |
||
240 | 155 | $filepath = pathinfo($filename, PATHINFO_DIRNAME); |
|
241 | |||
242 | 155 | if (! $this->createPathIfNeeded($filepath)) { |
|
243 | return false; |
||
244 | } |
||
245 | |||
246 | 155 | if (! is_writable($filepath)) { |
|
247 | return false; |
||
248 | } |
||
249 | |||
250 | 155 | $tmpFile = tempnam($filepath, 'swap'); |
|
251 | 155 | @chmod($tmpFile, 0666 & (~$this->umask)); |
|
0 ignored issues
–
show
|
|||
252 | |||
253 | 155 | if (file_put_contents($tmpFile, $content) !== false) { |
|
254 | 155 | @chmod($tmpFile, 0666 & (~$this->umask)); |
|
255 | 155 | if (@rename($tmpFile, $filename)) { |
|
256 | 155 | return true; |
|
257 | } |
||
258 | |||
259 | @unlink($tmpFile); |
||
260 | } |
||
261 | |||
262 | return false; |
||
263 | } |
||
264 | |||
265 | 10 | private function getIterator() : Iterator |
|
266 | { |
||
267 | 10 | return new RecursiveIteratorIterator( |
|
268 | 10 | new RecursiveDirectoryIterator($this->directory, FilesystemIterator::SKIP_DOTS), |
|
269 | 10 | RecursiveIteratorIterator::CHILD_FIRST |
|
270 | ); |
||
271 | } |
||
272 | |||
273 | /** |
||
274 | * @param string $name The filename |
||
275 | */ |
||
276 | 8 | private function isFilenameEndingWithExtension(string $name) : bool |
|
277 | { |
||
278 | 8 | return $this->extension === '' |
|
279 | 8 | || strrpos($name, $this->extension) === (strlen($name) - $this->extensionStringLength); |
|
280 | } |
||
281 | } |
||
282 |
If you suppress an error, we recommend checking for the error condition explicitly: