[BUGFIX] Caching framework: Remove flushByTags() and findIdentifiersByTags()
[Packages/TYPO3.CMS.git] / t3lib / cache / backend / class.t3lib_cache_backend_filebackend.php
1 <?php
2 /***************************************************************
3 * Copyright notice
4 *
5 * (c) 2009-2011 Ingo Renner <ingo@typo3.org>
6 * All rights reserved
7 *
8 * This script is part of the TYPO3 project. The TYPO3 project is
9 * free software; you can redistribute it and/or modify
10 * it under the terms of the GNU General Public License as published by
11 * the Free Software Foundation; either version 2 of the License, or
12 * (at your option) any later version.
13 *
14 * The GNU General Public License can be found at
15 * http://www.gnu.org/copyleft/gpl.html.
16 *
17 * This script is distributed in the hope that it will be useful,
18 * but WITHOUT ANY WARRANTY; without even the implied warranty of
19 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
20 * GNU General Public License for more details.
21 *
22 * This copyright notice MUST APPEAR in all copies of the script!
23 ***************************************************************/
24
25 /**
26 * A caching backend which stores cache entries in files
27 *
28 * This file is a backport from FLOW3
29 *
30 * @package TYPO3
31 * @subpackage t3lib_cache
32 * @api
33 * @scope prototype
34 */
35 class t3lib_cache_backend_FileBackend extends t3lib_cache_backend_AbstractBackend implements t3lib_cache_backend_PhpCapableBackend {
36
37 const SEPARATOR = '^';
38
39 const EXPIRYTIME_FORMAT = 'YmdHis';
40 const EXPIRYTIME_LENGTH = 14;
41
42 const DATASIZE_DIGITS = 10;
43
44 /**
45 * Directory where the files are stored
46 *
47 * @var string
48 */
49 protected $cacheDirectory = '';
50
51 /**
52 * TYPO3 v4 note: This variable is only available in v5
53 * Temporary path to cache directory before setCache() was called. It is
54 * set by setCacheDirectory() and used in setCache() method which calls
55 * the directory creation if needed. The variable is not used afterwards,
56 * the final cache directory path is stored in $this->cacheDirectory then.
57 *
58 * @var string Temporary path to cache directory
59 */
60 protected $temporaryCacheDirectory = '';
61
62 /**
63 * A file extension to use for each cache entry.
64 *
65 * @var string
66 */
67 protected $cacheEntryFileExtension = '';
68
69
70 /**
71 * Sets a reference to the cache frontend which uses this backend and
72 * initializes the default cache directory.
73 *
74 * TYPO3 v4 note: This method is different between TYPO3 v4 and FLOW3
75 * because the Environment class to get the path to a temporary directory
76 * does not exist in v4.
77 *
78 * @param t3lib_cache_frontend_Frontend $cache The cache frontend
79 * @return void
80 * @author Robert Lemke <robert@typo3.org>
81 */
82 public function setCache(t3lib_cache_frontend_Frontend $cache) {
83 parent::setCache($cache);
84
85 if (empty($this->temporaryCacheDirectory)) {
86 // If no cache directory was given with cacheDirectory
87 // configuration option, set it to a path below typo3temp/
88 $temporaryCacheDirectory = PATH_site . 'typo3temp/';
89 } else {
90 $temporaryCacheDirectory = $this->temporaryCacheDirectory;
91 }
92
93 $codeOrData = ($cache instanceof t3lib_cache_frontend_PhpFrontend) ? 'Code' : 'Data';
94 $finalCacheDirectory = $temporaryCacheDirectory . 'Cache/' . $codeOrData . '/' . $this->cacheIdentifier . '/';
95
96 if (!is_dir($finalCacheDirectory)) {
97 $this->createFinalCacheDirectory($finalCacheDirectory);
98 }
99 unset($this->temporaryCacheDirectory);
100 $this->cacheDirectory = $finalCacheDirectory;
101
102 $this->cacheEntryFileExtension = ($cache instanceof t3lib_cache_frontend_PhpFrontend) ? '.php' : '';
103 }
104
105 /**
106 * Sets the directory where the cache files are stored. By default it is
107 * assumed that the directory is below the TYPO3_DOCUMENT_ROOT. However, an
108 * absolute path can be selected, too.
109 *
110 * This method does not exist in FLOW3 anymore, but it is needed in
111 * TYPO3 v4 to enable a cache path outside of document root. The final
112 * cache path is checked and created in createFinalCachDirectory(),
113 * called by setCache() method, which is done _after_ the cacheDirectory
114 * option was handled.
115 *
116 * @param string $cacheDirectory The cache base directory. If a relative path
117 * is given, it is assumed it is in TYPO3_DOCUMENT_ROOT. If an absolute
118 * path is given it is taken as is.
119 * @return void
120 * @throws t3lib_cache_Exception if the directory is not within allowed
121 * open_basedir path.
122 * @author Christian Kuhn <lolli@schwarzbu.ch>
123 */
124 public function setCacheDirectory($cacheDirectory) {
125 // Skip handling if directory is a stream ressource
126 // This is used by unit tests with vfs:// directoryies
127 if (strpos($cacheDirectory, '://')) {
128 $this->temporaryCacheDirectory = $cacheDirectory;
129 return;
130 }
131
132 $documentRoot = PATH_site;
133
134 if (($open_basedir = ini_get('open_basedir'))) {
135 if (TYPO3_OS === 'WIN') {
136 $delimiter = ';';
137 $cacheDirectory = str_replace('\\', '/', $cacheDirectory);
138 if (!(preg_match('/[A-Z]:/', substr($cacheDirectory, 0, 2)))) {
139 $cacheDirectory = PATH_site . $cacheDirectory;
140 }
141 } else {
142 $delimiter = ':';
143 if ($cacheDirectory[0] != '/') {
144 // relative path to cache directory.
145 $cacheDirectory = PATH_site . $cacheDirectory;
146 }
147 }
148
149 $basedirs = explode($delimiter, $open_basedir);
150 $cacheDirectoryInBaseDir = FALSE;
151 foreach ($basedirs as $basedir) {
152 if (TYPO3_OS === 'WIN') {
153 $basedir = str_replace('\\', '/', $basedir);
154 }
155 if ($basedir[strlen($basedir) - 1] !== '/') {
156 $basedir .= '/';
157 }
158 if (t3lib_div::isFirstPartOfStr($cacheDirectory, $basedir)) {
159 $documentRoot = $basedir;
160 $cacheDirectory = str_replace($basedir, '', $cacheDirectory);
161 $cacheDirectoryInBaseDir = TRUE;
162 break;
163 }
164 }
165 if (!$cacheDirectoryInBaseDir) {
166 throw new t3lib_cache_Exception(
167 'Open_basedir restriction in effect. The directory "' . $cacheDirectory . '" is not in an allowed path.'
168 );
169 }
170 } else {
171 if ($cacheDirectory[0] == '/') {
172 // Absolute path to cache directory.
173 $documentRoot = '/';
174 }
175 if (TYPO3_OS === 'WIN') {
176 if (substr($cacheDirectory, 0, strlen($documentRoot)) === $documentRoot) {
177 $documentRoot = '';
178 }
179 }
180 }
181
182 // After this point all paths have '/' as directory seperator
183 if ($cacheDirectory[strlen($cacheDirectory) - 1] !== '/') {
184 $cacheDirectory .= '/';
185 }
186
187 $this->temporaryCacheDirectory = $documentRoot . $cacheDirectory . $this->cacheIdentifier . '/';
188 }
189
190 /**
191 * Create the final cache directory if it does not exist. This method
192 * exists in TYPO3 v4 only.
193 *
194 * @param string $finalCacheDirectory Absolute path to final cache directory
195 * @return void
196 * @throws \t3lib_cache_Exception If directory is not writable after creation
197 */
198 protected function createFinalCacheDirectory($finalCacheDirectory) {
199 try {
200 t3lib_div::mkdir_deep($finalCacheDirectory);
201 } catch (\RuntimeException $e) {
202 throw new \t3lib_cache_Exception(
203 'The directory "' . $finalCacheDirectory . '" can not be created.',
204 1303669848,
205 $e
206 );
207 }
208 if (!is_writable($finalCacheDirectory)) {
209 throw new \t3lib_cache_Exception(
210 'The directory "' . $finalCacheDirectory . '" is not writable.',
211 1203965200
212 );
213 }
214 }
215
216 /**
217 * Returns the directory where the cache files are stored
218 *
219 * @return string Full path of the cache directory
220 * @author Robert Lemke <robert@typo3.org>
221 * @api
222 */
223 public function getCacheDirectory() {
224 return $this->cacheDirectory;
225 }
226
227 /**
228 * Saves data in a cache file.
229 *
230 * @param string $entryIdentifier An identifier for this specific cache entry
231 * @param string $data The data to be stored
232 * @param array $tags Tags to associate with this cache entry
233 * @param integer $lifetime Lifetime of this cache entry in seconds. If NULL is specified, the default lifetime is used. "0" means unlimited lifetime.
234 * @return void
235 * @throws t3lib_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.
236 * @throws t3lib_cache_exception_InvalidData if the data to bes stored is not a string.
237 * @author Robert Lemke <robert@typo3.org>
238 * @api
239 */
240 public function set($entryIdentifier, $data, array $tags = array(), $lifetime = NULL) {
241 if (!is_string($data)) {
242 throw new t3lib_cache_Exception_InvalidData(
243 'The specified data is of type "' . gettype($data) . '" but a string is expected.',
244 1204481674
245 );
246 }
247
248 if ($entryIdentifier !== basename($entryIdentifier)) {
249 throw new \InvalidArgumentException(
250 'The specified entry identifier must not contain a path segment.',
251 1282073032
252 );
253 }
254
255 if ($entryIdentifier === '') {
256 throw new \InvalidArgumentException(
257 'The specified entry identifier must not be empty.',
258 1298114280
259 );
260 }
261
262 $this->remove($entryIdentifier);
263
264 $temporaryCacheEntryPathAndFilename = $this->cacheDirectory . uniqid() . '.temp';
265 if (strlen($temporaryCacheEntryPathAndFilename) > t3lib_div::getMaximumPathLength()) {
266 throw new t3lib_cache_Exception(
267 'The length of the temporary cache file path "' . $temporaryCacheEntryPathAndFilename .
268 '" is ' . strlen($temporaryCacheEntryPathAndFilename) . ' characters long and exceeds the maximum path length of ' .
269 t3lib_div::getMaximumPathLength() . '. Please consider setting the temporaryDirectoryBase option to a shorter path. ',
270 1248710426
271 );
272 }
273
274 $expiryTime = ($lifetime === NULL) ? 0 : ($GLOBALS['EXEC_TIME'] + $lifetime);
275 $metaData = str_pad($expiryTime, self::EXPIRYTIME_LENGTH) . implode(' ', $tags) . str_pad(strlen($data), self::DATASIZE_DIGITS);
276 $result = file_put_contents($temporaryCacheEntryPathAndFilename, $data . $metaData);
277
278 if ($result === FALSE) {
279 throw new t3lib_cache_exception(
280 'The temporary cache file "' . $temporaryCacheEntryPathAndFilename . '" could not be written.',
281 1204026251
282 );
283 }
284
285 $i = 0;
286 $cacheEntryPathAndFilename = $this->cacheDirectory . $entryIdentifier . $this->cacheEntryFileExtension;
287 // @TODO: Figure out why the heck this is done and maybe find a smarter solution, report to FLOW3
288 while (!rename($temporaryCacheEntryPathAndFilename, $cacheEntryPathAndFilename) && $i < 5) {
289 $i++;
290 }
291
292 // @FIXME: At least the result of rename() should be handled here, report to FLOW3
293 if ($result === FALSE) {
294 throw new t3lib_cache_exception(
295 'The cache file "' . $cacheEntryPathAndFilename . '" could not be written.',
296 1222361632
297 );
298 }
299 }
300
301 /**
302 * Loads data from a cache file.
303 *
304 * @param string $entryIdentifier An identifier which describes the cache entry to load
305 * @return mixed The cache entry's content as a string or FALSE if the cache entry could not be loaded
306 * @throws \InvalidArgumentException If identifier is invalid
307 * @author Robert Lemke <robert@typo3.org>
308 * @author Karsten Dambekalns <karsten@typo3.org>
309 * @api
310 */
311 public function get($entryIdentifier) {
312 if ($entryIdentifier !== basename($entryIdentifier)) {
313 throw new \InvalidArgumentException(
314 'The specified entry identifier must not contain a path segment.',
315 1282073033
316 );
317 }
318
319 $pathAndFilename = $this->cacheDirectory . $entryIdentifier . $this->cacheEntryFileExtension;
320 if ($this->isCacheFileExpired($pathAndFilename)) {
321 return FALSE;
322 }
323 $dataSize = (integer) file_get_contents($pathAndFilename, NULL, NULL, filesize($pathAndFilename) - self::DATASIZE_DIGITS, self::DATASIZE_DIGITS);
324 return file_get_contents($pathAndFilename, NULL, NULL, 0, $dataSize);
325 }
326
327 /**
328 * Checks if a cache entry with the specified identifier exists.
329 *
330 * @param string $entryIdentifier
331 * @return boolean TRUE if such an entry exists, FALSE if not
332 * @author Robert Lemke <robert@typo3.org>
333 * @api
334 */
335 public function has($entryIdentifier) {
336 if ($entryIdentifier !== basename($entryIdentifier)) {
337 throw new \InvalidArgumentException(
338 'The specified entry identifier must not contain a path segment.',
339 1282073034
340 );
341 }
342
343 return !$this->isCacheFileExpired($this->cacheDirectory . $entryIdentifier . $this->cacheEntryFileExtension);
344 }
345
346 /**
347 * Removes all cache entries matching the specified identifier.
348 * Usually this only affects one entry.
349 *
350 * @param string $entryIdentifier Specifies the cache entry to remove
351 * @return boolean TRUE if (at least) an entry could be removed or FALSE if no entry was found
352 * @author Robert Lemke <robert@typo3.org>
353 * @api
354 */
355 public function remove($entryIdentifier) {
356 if ($entryIdentifier !== basename($entryIdentifier)) {
357 throw new \InvalidArgumentException(
358 'The specified entry identifier must not contain a path segment.',
359 1282073035
360 );
361 }
362 if ($entryIdentifier === '') {
363 throw new \InvalidArgumentException(
364 'The specified entry identifier must not be empty.',
365 1298114279
366 );
367 }
368
369 $pathAndFilename = $this->cacheDirectory . $entryIdentifier . $this->cacheEntryFileExtension;
370 if (file_exists($pathAndFilename) === FALSE) {
371 return FALSE;
372 }
373 if (unlink($pathAndFilename) === FALSE) {
374 return FALSE;
375 }
376 return TRUE;
377 }
378
379 /**
380 * Finds and returns all cache entry identifiers which are tagged by the
381 * specified tag.
382 *
383 * @param string $searchedTag The tag to search for
384 * @return array An array with identifiers of all matching entries. An empty array if no entries matched
385 * @author Robert Lemke <robert@typo3.org>
386 * @api
387 */
388 public function findIdentifiersByTag($searchedTag) {
389 $entryIdentifiers = array();
390 $now = $GLOBALS['EXEC_TIME'];
391 $cacheEntryFileExtensionLength = strlen($this->cacheEntryFileExtension);
392 for ($directoryIterator = t3lib_div::makeInstance('DirectoryIterator', $this->cacheDirectory); $directoryIterator->valid(); $directoryIterator->next()) {
393 if ($directoryIterator->isDot()) {
394 continue;
395 }
396 $cacheEntryPathAndFilename = $directoryIterator->getPathname();
397 $index = (integer) file_get_contents($cacheEntryPathAndFilename, NULL, NULL, filesize($cacheEntryPathAndFilename) - self::DATASIZE_DIGITS, self::DATASIZE_DIGITS);
398 $metaData = file_get_contents($cacheEntryPathAndFilename, NULL, NULL, $index);
399
400 $expiryTime = (integer) substr($metaData, 0, self::EXPIRYTIME_LENGTH);
401 if ($expiryTime !== 0 && $expiryTime < $now) {
402 continue;
403 }
404 if (in_array($searchedTag, explode(' ', substr($metaData, self::EXPIRYTIME_LENGTH, -self::DATASIZE_DIGITS)))) {
405 if ($cacheEntryFileExtensionLength > 0) {
406 $entryIdentifiers[] = substr($directoryIterator->getFilename(), 0, - $cacheEntryFileExtensionLength);
407 } else {
408 $entryIdentifiers[] = $directoryIterator->getFilename();
409 }
410 }
411 }
412 return $entryIdentifiers;
413 }
414
415 /**
416 * Removes all cache entries of this cache.
417 *
418 * @return void
419 * @author Robert Lemke <robert@typo3.org>
420 * @author Christian Kuhn <lolli@schwarzbu.ch>
421 * @api
422 */
423 public function flush() {
424 t3lib_div::rmdir($this->cacheDirectory, TRUE);
425 }
426
427 /**
428 * Removes all cache entries of this cache which are tagged by the specified tag.
429 *
430 * @param string $tag The tag the entries must have
431 * @return void
432 * @author Robert Lemke <robert@typo3.org>
433 * @author Ingo Renner <ingo@typo3.org>
434 * @api
435 */
436 public function flushByTag($tag) {
437 $identifiers = $this->findIdentifiersByTag($tag);
438 if (count($identifiers) === 0) {
439 return;
440 }
441
442 foreach ($identifiers as $entryIdentifier) {
443 $this->remove($entryIdentifier);
444 }
445 }
446
447 /**
448 * Checks if the given cache entry files are still valid or if their
449 * lifetime has exceeded.
450 *
451 * @param string $cacheEntryPathAndFilename
452 * @return boolean
453 * @author Robert Lemke <robert@typo3.org>
454 * @api
455 */
456 protected function isCacheFileExpired($cacheEntryPathAndFilename) {
457 if (file_exists($cacheEntryPathAndFilename) === FALSE) {
458 return TRUE;
459 }
460 $index = (integer) file_get_contents($cacheEntryPathAndFilename, NULL, NULL, filesize($cacheEntryPathAndFilename) - self::DATASIZE_DIGITS, self::DATASIZE_DIGITS);
461 $expiryTime = file_get_contents($cacheEntryPathAndFilename, NULL, NULL, $index, self::EXPIRYTIME_LENGTH);
462 return ($expiryTime != 0 && $expiryTime < $GLOBALS['EXEC_TIME']);
463 }
464
465 /**
466 * Does garbage collection
467 *
468 * @return void
469 * @author Karsten Dambekalns <karsten@typo3.org>
470 * @api
471 */
472 public function collectGarbage() {
473 for ($directoryIterator = new \DirectoryIterator($this->cacheDirectory); $directoryIterator->valid(); $directoryIterator->next()) {
474 if ($directoryIterator->isDot()) {
475 continue;
476 }
477
478 if ($this->isCacheFileExpired($directoryIterator->getPathname())) {
479 $cacheEntryFileExtensionLength = strlen($this->cacheEntryFileExtension);
480 if ($cacheEntryFileExtensionLength > 0) {
481 $this->remove(substr($directoryIterator->getFilename(), 0, - $cacheEntryFileExtensionLength));
482 } else {
483 $this->remove($directoryIterator->getFilename());
484 }
485 }
486 }
487 }
488
489 /**
490 * Tries to find the cache entry for the specified identifier.
491 * Usually only one cache entry should be found - if more than one exist, this
492 * is due to some error or crash.
493 *
494 * @param string $entryIdentifier The cache entry identifier
495 * @return mixed The file names (including path) as an array if one or more entries could be found, otherwise FALSE
496 * @author Robert Lemke <robert@typo3.org>
497 * @internal
498 */
499 protected function findCacheFilesByIdentifier($entryIdentifier) {
500 $pattern = $this->cacheDirectory . $entryIdentifier;
501 $filesFound = glob($pattern);
502 if ($filesFound === FALSE || count($filesFound) === 0) {
503 return FALSE;
504 }
505
506 return $filesFound;
507 }
508
509 /**
510 * Loads PHP code from the cache and require_onces it right away.
511 *
512 * @param string $entryIdentifier An identifier which describes the cache entry to load
513 * @return mixed Potential return value from the include operation
514 * @api
515 */
516 public function requireOnce($entryIdentifier) {
517 if ($entryIdentifier !== basename($entryIdentifier)) {
518 throw new \InvalidArgumentException(
519 'The specified entry identifier must not contain a path segment.',
520 1282073036
521 );
522 }
523
524 $pathAndFilename = $this->cacheDirectory . $entryIdentifier . $this->cacheEntryFileExtension;
525 return ($this->isCacheFileExpired($pathAndFilename)) ? FALSE : require_once($pathAndFilename);
526 }
527 }
528
529
530 if (defined('TYPO3_MODE') && isset($GLOBALS['TYPO3_CONF_VARS'][TYPO3_MODE]['XCLASS']['t3lib/cache/backend/class.t3lib_cache_backend_filebackend.php'])) {
531 include_once($GLOBALS['TYPO3_CONF_VARS'][TYPO3_MODE]['XCLASS']['t3lib/cache/backend/class.t3lib_cache_backend_filebackend.php']);
532 }
533
534 ?>