3afac380995bb1a1bf58e211b3dc99887b6d83c7
[Packages/TYPO3.CMS.git] / typo3 / sysext / core / Classes / Cache / Backend / FileBackend.php
1 <?php
2 namespace TYPO3\CMS\Core\Cache\Backend;
3
4 /*
5 * This file is part of the TYPO3 CMS project.
6 *
7 * It is free software; you can redistribute it and/or modify it under
8 * the terms of the GNU General Public License, either version 2
9 * of the License, or any later version.
10 *
11 * For the full copyright and license information, please read the
12 * LICENSE.txt file that was distributed with this source code.
13 *
14 * The TYPO3 project - inspiring people to share!
15 */
16 use TYPO3\CMS\Core\Utility\GeneralUtility;
17 use TYPO3\CMS\Core\Service\OpcodeCacheService;
18
19 /**
20 * A caching backend which stores cache entries in files
21 *
22 * This file is a backport from FLOW3
23 *
24 * @author Robert Lemke <robert@typo3.org>
25 * @author Christian Kuhn <lolli@schwarzbu.ch>
26 * @author Karsten Dambekalns <karsten@typo3.org>
27 * @api
28 */
29 class FileBackend extends \TYPO3\CMS\Core\Cache\Backend\SimpleFileBackend implements \TYPO3\CMS\Core\Cache\Backend\PhpCapableBackendInterface, \TYPO3\CMS\Core\Cache\Backend\FreezableBackendInterface, \TYPO3\CMS\Core\Cache\Backend\TaggableBackendInterface {
30
31 const SEPARATOR = '^';
32 const EXPIRYTIME_FORMAT = 'YmdHis';
33 const EXPIRYTIME_LENGTH = 14;
34 const DATASIZE_DIGITS = 10;
35 /**
36 * A file extension to use for each cache entry.
37 *
38 * @var string
39 */
40 protected $cacheEntryFileExtension = '';
41
42 /**
43 * @var array
44 */
45 protected $cacheEntryIdentifiers = array();
46
47 /**
48 * @var bool
49 */
50 protected $frozen = FALSE;
51
52 /**
53 * Freezes this cache backend.
54 *
55 * All data in a frozen backend remains unchanged and methods which try to add
56 * or modify data result in an exception thrown. Possible expiry times of
57 * individual cache entries are ignored.
58 *
59 * On the positive side, a frozen cache backend is much faster on read access.
60 * A frozen backend can only be thawed by calling the flush() method.
61 *
62 * @return void
63 * @throws \RuntimeException
64 */
65 public function freeze() {
66 if ($this->frozen === TRUE) {
67 throw new \RuntimeException(sprintf('The cache "%s" is already frozen.', $this->cacheIdentifier), 1323353176);
68 }
69 $cacheEntryFileExtensionLength = strlen($this->cacheEntryFileExtension);
70 for ($directoryIterator = new \DirectoryIterator($this->cacheDirectory); $directoryIterator->valid(); $directoryIterator->next()) {
71 if ($directoryIterator->isDot()) {
72 continue;
73 }
74 if ($cacheEntryFileExtensionLength > 0) {
75 $entryIdentifier = substr($directoryIterator->getFilename(), 0, -$cacheEntryFileExtensionLength);
76 } else {
77 $entryIdentifier = $directoryIterator->getFilename();
78 }
79 $this->cacheEntryIdentifiers[$entryIdentifier] = TRUE;
80 file_put_contents($this->cacheDirectory . $entryIdentifier . $this->cacheEntryFileExtension, $this->get($entryIdentifier));
81 }
82 if ($this->useIgBinary === TRUE) {
83 file_put_contents($this->cacheDirectory . 'FrozenCache.data', igbinary_serialize($this->cacheEntryIdentifiers));
84 } else {
85 file_put_contents($this->cacheDirectory . 'FrozenCache.data', serialize($this->cacheEntryIdentifiers));
86 }
87 $this->frozen = TRUE;
88 }
89
90 /**
91 * Tells if this backend is frozen.
92 *
93 * @return bool
94 */
95 public function isFrozen() {
96 return $this->frozen;
97 }
98
99 /**
100 * Sets a reference to the cache frontend which uses this backend and
101 * initializes the default cache directory.
102 *
103 * This method also detects if this backend is frozen and sets the internal
104 * flag accordingly.
105 *
106 * TYPO3 v4 note: This method is different between TYPO3 v4 and FLOW3
107 * because the Environment class to get the path to a temporary directory
108 * does not exist in v4.
109 *
110 * @param \TYPO3\CMS\Core\Cache\Frontend\FrontendInterface $cache The cache frontend
111 * @return void
112 */
113 public function setCache(\TYPO3\CMS\Core\Cache\Frontend\FrontendInterface $cache) {
114 parent::setCache($cache);
115 if (file_exists($this->cacheDirectory . 'FrozenCache.data')) {
116 $this->frozen = TRUE;
117 if ($this->useIgBinary === TRUE) {
118 $this->cacheEntryIdentifiers = igbinary_unserialize(file_get_contents($this->cacheDirectory . 'FrozenCache.data'));
119 } else {
120 $this->cacheEntryIdentifiers = unserialize(file_get_contents($this->cacheDirectory . 'FrozenCache.data'));
121 }
122 }
123 }
124
125 /**
126 * Saves data in a cache file.
127 *
128 * @param string $entryIdentifier An identifier for this specific cache entry
129 * @param string $data The data to be stored
130 * @param array $tags Tags to associate with this cache entry
131 * @param int $lifetime Lifetime of this cache entry in seconds. If NULL is specified, the default lifetime is used. "0" means unlimited lifetime.
132 * @return void
133 * @throws \RuntimeException
134 * @throws \TYPO3\CMS\Core\Cache\Exception\InvalidDataException if the directory does not exist or is not writable or exceeds the maximum allowed path length, or if no cache frontend has been set.
135 * @throws \TYPO3\CMS\Core\Cache\Exception if the directory does not exist or is not writable or exceeds the maximum allowed path length, or if no cache frontend has been set.
136 * @throws \InvalidArgumentException
137 * @api
138 */
139 public function set($entryIdentifier, $data, array $tags = array(), $lifetime = NULL) {
140 if (!is_string($data)) {
141 throw new \TYPO3\CMS\Core\Cache\Exception\InvalidDataException('The specified data is of type "' . gettype($data) . '" but a string is expected.', 1204481674);
142 }
143 if ($entryIdentifier !== basename($entryIdentifier)) {
144 throw new \InvalidArgumentException('The specified entry identifier must not contain a path segment.', 1282073032);
145 }
146 if ($entryIdentifier === '') {
147 throw new \InvalidArgumentException('The specified entry identifier must not be empty.', 1298114280);
148 }
149 if ($this->frozen === TRUE) {
150 throw new \RuntimeException(sprintf('Cannot add or modify cache entry because the backend of cache "%s" is frozen.', $this->cacheIdentifier), 1323344192);
151 }
152 $this->remove($entryIdentifier);
153 $temporaryCacheEntryPathAndFilename = $this->cacheDirectory . uniqid('', TRUE) . '.temp';
154 $lifetime = $lifetime === NULL ? $this->defaultLifetime : $lifetime;
155 $expiryTime = $lifetime === 0 ? 0 : $GLOBALS['EXEC_TIME'] + $lifetime;
156 $metaData = str_pad($expiryTime, self::EXPIRYTIME_LENGTH) . implode(' ', $tags) . str_pad(strlen($data), self::DATASIZE_DIGITS);
157 $result = file_put_contents($temporaryCacheEntryPathAndFilename, $data . $metaData);
158 \TYPO3\CMS\Core\Utility\GeneralUtility::fixPermissions($temporaryCacheEntryPathAndFilename);
159 if ($result === FALSE) {
160 throw new \TYPO3\CMS\Core\Cache\Exception('The temporary cache file "' . $temporaryCacheEntryPathAndFilename . '" could not be written.', 1204026251);
161 }
162 $i = 0;
163 $cacheEntryPathAndFilename = $this->cacheDirectory . $entryIdentifier . $this->cacheEntryFileExtension;
164 while (($result = rename($temporaryCacheEntryPathAndFilename, $cacheEntryPathAndFilename)) === FALSE && $i < 5) {
165 $i++;
166 }
167 if ($result === FALSE) {
168 throw new \TYPO3\CMS\Core\Cache\Exception('The cache file "' . $cacheEntryPathAndFilename . '" could not be written.', 1222361632);
169 }
170 if ($this->cacheEntryFileExtension === '.php') {
171 GeneralUtility::makeInstance(OpcodeCacheService::class)->clearAllActive($cacheEntryPathAndFilename);
172 }
173 }
174
175 /**
176 * Loads data from a cache file.
177 *
178 * @param string $entryIdentifier An identifier which describes the cache entry to load
179 * @return mixed The cache entry's content as a string or FALSE if the cache entry could not be loaded
180 * @throws \InvalidArgumentException If identifier is invalid
181 * @api
182 */
183 public function get($entryIdentifier) {
184 if ($this->frozen === TRUE) {
185 return isset($this->cacheEntryIdentifiers[$entryIdentifier]) ? file_get_contents($this->cacheDirectory . $entryIdentifier . $this->cacheEntryFileExtension) : FALSE;
186 }
187 if ($entryIdentifier !== basename($entryIdentifier)) {
188 throw new \InvalidArgumentException('The specified entry identifier must not contain a path segment.', 1282073033);
189 }
190 $pathAndFilename = $this->cacheDirectory . $entryIdentifier . $this->cacheEntryFileExtension;
191 if ($this->isCacheFileExpired($pathAndFilename)) {
192 return FALSE;
193 }
194 $dataSize = (int)file_get_contents($pathAndFilename, NULL, NULL, (filesize($pathAndFilename) - self::DATASIZE_DIGITS), self::DATASIZE_DIGITS);
195 return file_get_contents($pathAndFilename, NULL, NULL, 0, $dataSize);
196 }
197
198 /**
199 * Checks if a cache entry with the specified identifier exists.
200 *
201 * @param string $entryIdentifier
202 * @return bool TRUE if such an entry exists, FALSE if not
203 * @throws \InvalidArgumentException
204 * @api
205 */
206 public function has($entryIdentifier) {
207 if ($this->frozen === TRUE) {
208 return isset($this->cacheEntryIdentifiers[$entryIdentifier]);
209 }
210 if ($entryIdentifier !== basename($entryIdentifier)) {
211 throw new \InvalidArgumentException('The specified entry identifier must not contain a path segment.', 1282073034);
212 }
213 return !$this->isCacheFileExpired(($this->cacheDirectory . $entryIdentifier . $this->cacheEntryFileExtension));
214 }
215
216 /**
217 * Removes all cache entries matching the specified identifier.
218 * Usually this only affects one entry.
219 *
220 * @param string $entryIdentifier Specifies the cache entry to remove
221 * @return bool TRUE if (at least) an entry could be removed or FALSE if no entry was found
222 * @throws \RuntimeException
223 * @throws \InvalidArgumentException
224 * @api
225 */
226 public function remove($entryIdentifier) {
227 if ($entryIdentifier !== basename($entryIdentifier)) {
228 throw new \InvalidArgumentException('The specified entry identifier must not contain a path segment.', 1282073035);
229 }
230 if ($entryIdentifier === '') {
231 throw new \InvalidArgumentException('The specified entry identifier must not be empty.', 1298114279);
232 }
233 if ($this->frozen === TRUE) {
234 throw new \RuntimeException(sprintf('Cannot remove cache entry because the backend of cache "%s" is frozen.', $this->cacheIdentifier), 1323344193);
235 }
236 $pathAndFilename = $this->cacheDirectory . $entryIdentifier . $this->cacheEntryFileExtension;
237 if (file_exists($pathAndFilename) === FALSE) {
238 return FALSE;
239 }
240 if (unlink($pathAndFilename) === FALSE) {
241 return FALSE;
242 }
243 return TRUE;
244 }
245
246 /**
247 * Finds and returns all cache entry identifiers which are tagged by the
248 * specified tag.
249 *
250 * @param string $searchedTag The tag to search for
251 * @return array An array with identifiers of all matching entries. An empty array if no entries matched
252 * @api
253 */
254 public function findIdentifiersByTag($searchedTag) {
255 $entryIdentifiers = array();
256 $now = $GLOBALS['EXEC_TIME'];
257 $cacheEntryFileExtensionLength = strlen($this->cacheEntryFileExtension);
258 for ($directoryIterator = \TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance(\DirectoryIterator::class, $this->cacheDirectory); $directoryIterator->valid(); $directoryIterator->next()) {
259 if ($directoryIterator->isDot()) {
260 continue;
261 }
262 $cacheEntryPathAndFilename = $directoryIterator->getPathname();
263 $index = (int)file_get_contents($cacheEntryPathAndFilename, NULL, NULL, (filesize($cacheEntryPathAndFilename) - self::DATASIZE_DIGITS), self::DATASIZE_DIGITS);
264 $metaData = file_get_contents($cacheEntryPathAndFilename, NULL, NULL, $index);
265 $expiryTime = (int)substr($metaData, 0, self::EXPIRYTIME_LENGTH);
266 if ($expiryTime !== 0 && $expiryTime < $now) {
267 continue;
268 }
269 if (in_array($searchedTag, explode(' ', substr($metaData, self::EXPIRYTIME_LENGTH, -self::DATASIZE_DIGITS)))) {
270 if ($cacheEntryFileExtensionLength > 0) {
271 $entryIdentifiers[] = substr($directoryIterator->getFilename(), 0, -$cacheEntryFileExtensionLength);
272 } else {
273 $entryIdentifiers[] = $directoryIterator->getFilename();
274 }
275 }
276 }
277 return $entryIdentifiers;
278 }
279
280 /**
281 * Removes all cache entries of this cache and sets the frozen flag to FALSE.
282 *
283 * @return void
284 * @api
285 */
286 public function flush() {
287 parent::flush();
288 if ($this->frozen === TRUE) {
289 $this->frozen = FALSE;
290 }
291 }
292
293 /**
294 * Removes all cache entries of this cache which are tagged by the specified tag.
295 *
296 * @param string $tag The tag the entries must have
297 * @return void
298 * @api
299 */
300 public function flushByTag($tag) {
301 $identifiers = $this->findIdentifiersByTag($tag);
302 if (empty($identifiers)) {
303 return;
304 }
305 foreach ($identifiers as $entryIdentifier) {
306 $this->remove($entryIdentifier);
307 }
308 }
309
310 /**
311 * Checks if the given cache entry files are still valid or if their
312 * lifetime has exceeded.
313 *
314 * @param string $cacheEntryPathAndFilename
315 * @return bool
316 * @api
317 */
318 protected function isCacheFileExpired($cacheEntryPathAndFilename) {
319 if (file_exists($cacheEntryPathAndFilename) === FALSE) {
320 return TRUE;
321 }
322 $index = (int)file_get_contents($cacheEntryPathAndFilename, NULL, NULL, (filesize($cacheEntryPathAndFilename) - self::DATASIZE_DIGITS), self::DATASIZE_DIGITS);
323 $expiryTime = (int)file_get_contents($cacheEntryPathAndFilename, NULL, NULL, $index, self::EXPIRYTIME_LENGTH);
324 return $expiryTime !== 0 && $expiryTime < $GLOBALS['EXEC_TIME'];
325 }
326
327 /**
328 * Does garbage collection
329 *
330 * @return void
331 * @api
332 */
333 public function collectGarbage() {
334 if ($this->frozen === TRUE) {
335 return;
336 }
337 for ($directoryIterator = new \DirectoryIterator($this->cacheDirectory); $directoryIterator->valid(); $directoryIterator->next()) {
338 if ($directoryIterator->isDot()) {
339 continue;
340 }
341 if ($this->isCacheFileExpired($directoryIterator->getPathname())) {
342 $cacheEntryFileExtensionLength = strlen($this->cacheEntryFileExtension);
343 if ($cacheEntryFileExtensionLength > 0) {
344 $this->remove(substr($directoryIterator->getFilename(), 0, -$cacheEntryFileExtensionLength));
345 } else {
346 $this->remove($directoryIterator->getFilename());
347 }
348 }
349 }
350 }
351
352 /**
353 * Tries to find the cache entry for the specified identifier.
354 * Usually only one cache entry should be found - if more than one exist, this
355 * is due to some error or crash.
356 *
357 * @param string $entryIdentifier The cache entry identifier
358 * @return mixed The filenames (including path) as an array if one or more entries could be found, otherwise FALSE
359 */
360 protected function findCacheFilesByIdentifier($entryIdentifier) {
361 $pattern = $this->cacheDirectory . $entryIdentifier;
362 $filesFound = glob($pattern);
363 if ($filesFound === FALSE || count($filesFound) === 0) {
364 return FALSE;
365 }
366 return $filesFound;
367 }
368
369 /**
370 * Loads PHP code from the cache and require_onces it right away.
371 *
372 * @param string $entryIdentifier An identifier which describes the cache entry to load
373 * @throws \InvalidArgumentException
374 * @return mixed Potential return value from the include operation
375 * @api
376 */
377 public function requireOnce($entryIdentifier) {
378 if ($this->frozen === TRUE) {
379 if (isset($this->cacheEntryIdentifiers[$entryIdentifier])) {
380 return require_once $this->cacheDirectory . $entryIdentifier . $this->cacheEntryFileExtension;
381 } else {
382 return FALSE;
383 }
384 } else {
385 $pathAndFilename = $this->cacheDirectory . $entryIdentifier . $this->cacheEntryFileExtension;
386 if ($entryIdentifier !== basename($entryIdentifier)) {
387 throw new \InvalidArgumentException('The specified entry identifier must not contain a path segment.', 1282073036);
388 }
389 $pathAndFilename = $this->cacheDirectory . $entryIdentifier . $this->cacheEntryFileExtension;
390 return $this->isCacheFileExpired($pathAndFilename) ? FALSE : require_once $pathAndFilename;
391 }
392 }
393
394 }