1
|
|
|
<?php |
2
|
|
|
/* |
3
|
|
|
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS |
4
|
|
|
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT |
5
|
|
|
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR |
6
|
|
|
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT |
7
|
|
|
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, |
8
|
|
|
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT |
9
|
|
|
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, |
10
|
|
|
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY |
11
|
|
|
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT |
12
|
|
|
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE |
13
|
|
|
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
14
|
|
|
* |
15
|
|
|
* This software consists of voluntary contributions made by many individuals |
16
|
|
|
* and is licensed under the MIT license. For more information, see |
17
|
|
|
* <http://www.doctrine-project.org>. |
18
|
|
|
*/ |
19
|
|
|
|
20
|
|
|
namespace Doctrine\Common\Cache; |
21
|
|
|
|
22
|
|
|
/** |
23
|
|
|
* Base file cache driver. |
24
|
|
|
* |
25
|
|
|
* @since 2.3 |
26
|
|
|
* @author Fabio B. Silva <[email protected]> |
27
|
|
|
* @author Tobias Schultze <http://tobion.de> |
28
|
|
|
*/ |
29
|
|
|
abstract class FileCache extends CacheProvider |
30
|
|
|
{ |
31
|
|
|
/** |
32
|
|
|
* The cache directory. |
33
|
|
|
* |
34
|
|
|
* @var string |
35
|
|
|
*/ |
36
|
|
|
protected $directory; |
37
|
|
|
|
38
|
|
|
/** |
39
|
|
|
* The cache file extension. |
40
|
|
|
* |
41
|
|
|
* @var string |
42
|
|
|
*/ |
43
|
|
|
private $extension; |
44
|
|
|
|
45
|
|
|
/** |
46
|
|
|
* @var int |
47
|
|
|
*/ |
48
|
|
|
private $umask; |
49
|
|
|
|
50
|
|
|
/** |
51
|
|
|
* @var int |
52
|
|
|
*/ |
53
|
|
|
private $directoryStringLength; |
54
|
|
|
|
55
|
|
|
/** |
56
|
|
|
* @var int |
57
|
|
|
*/ |
58
|
|
|
private $extensionStringLength; |
59
|
|
|
|
60
|
|
|
/** |
61
|
|
|
* @var bool |
62
|
|
|
*/ |
63
|
|
|
private $isRunningOnWindows; |
64
|
|
|
|
65
|
|
|
/** |
66
|
|
|
* Constructor. |
67
|
|
|
* |
68
|
|
|
* @param string $directory The cache directory. |
69
|
|
|
* @param string $extension The cache file extension. |
70
|
|
|
* |
71
|
|
|
* @throws \InvalidArgumentException |
72
|
|
|
*/ |
73
|
168 |
|
public function __construct($directory, $extension = '', $umask = 0002) |
74
|
|
|
{ |
75
|
|
|
// YES, this needs to be *before* createPathIfNeeded() |
76
|
168 |
|
if ( ! is_int($umask)) { |
77
|
1 |
|
throw new \InvalidArgumentException(sprintf( |
78
|
1 |
|
'The umask parameter is required to be integer, was: %s', |
79
|
1 |
|
gettype($umask) |
80
|
|
|
)); |
81
|
|
|
} |
82
|
167 |
|
$this->umask = $umask; |
83
|
|
|
|
84
|
167 |
|
if ( ! $this->createPathIfNeeded($directory)) { |
85
|
|
|
throw new \InvalidArgumentException(sprintf( |
86
|
|
|
'The directory "%s" does not exist and could not be created.', |
87
|
|
|
$directory |
88
|
|
|
)); |
89
|
|
|
} |
90
|
|
|
|
91
|
167 |
|
if ( ! is_writable($directory)) { |
92
|
|
|
throw new \InvalidArgumentException(sprintf( |
93
|
|
|
'The directory "%s" is not writable.', |
94
|
|
|
$directory |
95
|
|
|
)); |
96
|
|
|
} |
97
|
|
|
|
98
|
|
|
// YES, this needs to be *after* createPathIfNeeded() |
99
|
167 |
|
$this->directory = realpath($directory); |
100
|
167 |
|
$this->extension = (string) $extension; |
101
|
|
|
|
102
|
167 |
|
$this->directoryStringLength = strlen($this->directory); |
103
|
167 |
|
$this->extensionStringLength = strlen($this->extension); |
104
|
167 |
|
$this->isRunningOnWindows = defined('PHP_WINDOWS_VERSION_BUILD'); |
105
|
167 |
|
} |
106
|
|
|
|
107
|
|
|
/** |
108
|
|
|
* Gets the cache directory. |
109
|
|
|
* |
110
|
|
|
* @return string |
111
|
|
|
*/ |
112
|
1 |
|
public function getDirectory() |
113
|
|
|
{ |
114
|
1 |
|
return $this->directory; |
115
|
|
|
} |
116
|
|
|
|
117
|
|
|
/** |
118
|
|
|
* Gets the cache file extension. |
119
|
|
|
* |
120
|
|
|
* @return string |
121
|
|
|
*/ |
122
|
1 |
|
public function getExtension() |
123
|
|
|
{ |
124
|
1 |
|
return $this->extension; |
125
|
|
|
} |
126
|
|
|
|
127
|
|
|
/** |
128
|
|
|
* @param string $id |
129
|
|
|
* |
130
|
|
|
* @return string |
131
|
|
|
*/ |
132
|
164 |
|
protected function getFilename($id) |
133
|
|
|
{ |
134
|
164 |
|
$hash = hash('sha256', $id); |
135
|
|
|
|
136
|
|
|
// This ensures that the filename is unique and that there are no invalid chars in it. |
137
|
|
|
if ( |
138
|
164 |
|
'' === $id |
139
|
164 |
|
|| ((strlen($id) * 2 + $this->extensionStringLength) > 255) |
140
|
164 |
|
|| ($this->isRunningOnWindows && ($this->directoryStringLength + 4 + strlen($id) * 2 + $this->extensionStringLength) > 258) |
141
|
|
|
) { |
142
|
|
|
// Most filesystems have a limit of 255 chars for each path component. On Windows the the whole path is limited |
143
|
|
|
// to 260 chars (including terminating null char). Using long UNC ("\\?\" prefix) does not work with the PHP API. |
144
|
|
|
// And there is a bug in PHP (https://bugs.php.net/bug.php?id=70943) with path lengths of 259. |
145
|
|
|
// So if the id in hex representation would surpass the limit, we use the hash instead. The prefix prevents |
146
|
|
|
// collisions between the hash and bin2hex. |
147
|
12 |
|
$filename = '_' . $hash; |
148
|
|
|
} else { |
149
|
162 |
|
$filename = bin2hex($id); |
150
|
|
|
} |
151
|
|
|
|
152
|
164 |
|
return $this->directory |
153
|
164 |
|
. DIRECTORY_SEPARATOR |
154
|
164 |
|
. substr($hash, 0, 2) |
155
|
164 |
|
. DIRECTORY_SEPARATOR |
156
|
164 |
|
. $filename |
157
|
164 |
|
. $this->extension; |
158
|
|
|
} |
159
|
|
|
|
160
|
|
|
/** |
161
|
|
|
* {@inheritdoc} |
162
|
|
|
*/ |
163
|
88 |
|
protected function doDelete($id) |
164
|
|
|
{ |
165
|
88 |
|
$filename = $this->getFilename($id); |
166
|
|
|
|
167
|
88 |
|
return @unlink($filename) || ! file_exists($filename); |
168
|
|
|
} |
169
|
|
|
|
170
|
|
|
/** |
171
|
|
|
* {@inheritdoc} |
172
|
|
|
*/ |
173
|
6 |
|
protected function doFlush() |
174
|
|
|
{ |
175
|
6 |
|
foreach ($this->getIterator() as $name => $file) { |
176
|
6 |
|
if ($file->isDir()) { |
177
|
|
|
// Remove the intermediate directories which have been created to balance the tree. It only takes effect |
178
|
|
|
// if the directory is empty. If several caches share the same directory but with different file extensions, |
179
|
|
|
// the other ones are not removed. |
180
|
6 |
|
@rmdir($name); |
181
|
6 |
|
} elseif ($this->isFilenameEndingWithExtension($name)) { |
182
|
|
|
// If an extension is set, only remove files which end with the given extension. |
183
|
|
|
// If no extension is set, we have no other choice than removing everything. |
184
|
6 |
|
@unlink($name); |
185
|
|
|
} |
186
|
|
|
} |
187
|
|
|
|
188
|
6 |
|
return true; |
189
|
|
|
} |
190
|
|
|
|
191
|
|
|
/** |
192
|
|
|
* {@inheritdoc} |
193
|
|
|
*/ |
194
|
4 |
|
protected function doGetStats() |
195
|
|
|
{ |
196
|
4 |
|
$usage = 0; |
197
|
4 |
|
foreach ($this->getIterator() as $name => $file) { |
198
|
2 |
|
if (! $file->isDir() && $this->isFilenameEndingWithExtension($name)) { |
199
|
2 |
|
$usage += $file->getSize(); |
200
|
|
|
} |
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
|
|
|
* @param string $path |
218
|
|
|
* @return bool TRUE on success or if path already exists, FALSE if path cannot be created. |
219
|
|
|
*/ |
220
|
167 |
|
private function createPathIfNeeded(string $path) : bool |
221
|
|
|
{ |
222
|
167 |
|
if ( ! is_dir($path)) { |
223
|
163 |
|
if (false === @mkdir($path, 0777 & (~$this->umask), true) && !is_dir($path)) { |
224
|
|
|
return false; |
225
|
|
|
} |
226
|
|
|
} |
227
|
|
|
|
228
|
167 |
|
return true; |
229
|
|
|
} |
230
|
|
|
|
231
|
|
|
/** |
232
|
|
|
* Writes a string content to file in an atomic way. |
233
|
|
|
* |
234
|
|
|
* @param string $filename Path to the file where to write the data. |
235
|
|
|
* @param string $content The content to write |
236
|
|
|
* |
237
|
|
|
* @return bool TRUE on success, FALSE if path cannot be created, if path is not writable or an any other error. |
238
|
|
|
*/ |
239
|
155 |
|
protected function writeFile(string $filename, string $content) : bool |
240
|
|
|
{ |
241
|
155 |
|
$filepath = pathinfo($filename, PATHINFO_DIRNAME); |
242
|
|
|
|
243
|
155 |
|
if ( ! $this->createPathIfNeeded($filepath)) { |
244
|
|
|
return false; |
245
|
|
|
} |
246
|
|
|
|
247
|
155 |
|
if ( ! is_writable($filepath)) { |
248
|
|
|
return false; |
249
|
|
|
} |
250
|
|
|
|
251
|
155 |
|
$tmpFile = tempnam($filepath, 'swap'); |
252
|
155 |
|
@chmod($tmpFile, 0666 & (~$this->umask)); |
253
|
|
|
|
254
|
155 |
|
if (file_put_contents($tmpFile, $content) !== false) { |
255
|
155 |
|
@chmod($tmpFile, 0666 & (~$this->umask)); |
|
|
|
|
256
|
155 |
|
if (@rename($tmpFile, $filename)) { |
257
|
155 |
|
return true; |
258
|
|
|
} |
259
|
|
|
|
260
|
|
|
@unlink($tmpFile); |
261
|
|
|
} |
262
|
|
|
|
263
|
|
|
return false; |
264
|
|
|
} |
265
|
|
|
|
266
|
|
|
/** |
267
|
|
|
* @return \Iterator |
268
|
|
|
*/ |
269
|
10 |
|
private function getIterator() : \Iterator |
270
|
|
|
{ |
271
|
10 |
|
return new \RecursiveIteratorIterator( |
272
|
10 |
|
new \RecursiveDirectoryIterator($this->directory, \FilesystemIterator::SKIP_DOTS), |
273
|
10 |
|
\RecursiveIteratorIterator::CHILD_FIRST |
274
|
|
|
); |
275
|
|
|
} |
276
|
|
|
|
277
|
|
|
/** |
278
|
|
|
* @param string $name The filename |
279
|
|
|
* |
280
|
|
|
* @return bool |
281
|
|
|
*/ |
282
|
8 |
|
private function isFilenameEndingWithExtension(string $name) : bool |
283
|
|
|
{ |
284
|
8 |
|
return '' === $this->extension |
285
|
8 |
|
|| strrpos($name, $this->extension) === (strlen($name) - $this->extensionStringLength); |
286
|
|
|
} |
287
|
|
|
} |
288
|
|
|
|
If you suppress an error, we recommend checking for the error condition explicitly: