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