Fixed bug #14638: Update caching framework to its according FLOW3 revision 4443
[Packages/TYPO3.CMS.git] / t3lib / cache / backend / class.t3lib_cache_backend_filebackend.php
1 <?php
2 /***************************************************************
3 * Copyright notice
4 *
5 * (c) 2009 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 /**
27 * A caching backend which stores cache entries in files
28 *
29 * This file is a backport from FLOW3
30 *
31 * @package TYPO3
32 * @subpackage t3lib_cache
33 * @api
34 * @version $Id$
35 */
36 class t3lib_cache_backend_FileBackend extends t3lib_cache_backend_AbstractBackend {
37
38 const SEPARATOR = '^';
39
40 const EXPIRYTIME_FORMAT = 'YmdHis';
41 const EXPIRYTIME_LENGTH = 14;
42
43 /**
44 * @var string Directory where the files are stored
45 */
46 protected $cacheDirectory = '';
47
48 protected $root = '/';
49
50 /**
51 * Maximum allowed file path length in the current environment.
52 * Will be set in initializeObject()
53 *
54 * @var integer
55 */
56 protected $maximumPathLength = null;
57
58 /**
59 * Constructs this backend
60 *
61 * @param mixed Configuration options - depends on the actual backend
62 */
63 public function __construct(array $options = array()) {
64 parent::__construct($options);
65
66 if (empty($this->cacheDirectory)) {
67 $cacheDirectory = 'typo3temp/cache/';
68 try {
69 $this->setCacheDirectory($cacheDirectory);
70 } catch(t3lib_cache_Exception $exception) {
71
72 }
73 }
74
75 if (is_null($this->maximumPathLength)) {
76 $this->maximumPathLength = t3lib_div::getMaximumPathLength();
77 }
78 }
79
80 /**
81 * Sets the directory where the cache files are stored. By default it is
82 * assumed that the directory is below the TYPO3_DOCUMENT_ROOT. However, an
83 * absolute path can be selected, too.
84 *
85 * @param string The directory. If a relative path is given, it's assumed it's in TYPO3_DOCUMENT_ROOT. If an absolute path is given it is taken as is.
86 * @return void
87 * @throws t3lib_cache_Exception if the directory does not exist, is not writable or could not be created.
88 * @author Robert Lemke <robert@typo3.org>
89 * @author Ingo Renner <ingo@typo3.org>
90 */
91 public function setCacheDirectory($cacheDirectory) {
92 $documentRoot = PATH_site;
93
94 if (($open_basedir = ini_get('open_basedir'))) {
95
96
97 if (TYPO3_OS === 'WIN') {
98 $delimiter = ';';
99 $cacheDirectory = str_replace('\\', '/', $cacheDirectory);
100 if (!(preg_match('/[A-Z]:/', substr($cacheDirectory,0,2)))) {
101 $cacheDirectory = PATH_site . $cacheDirectory;
102 }
103 } else {
104 $delimiter = ':';
105 if ($cacheDirectory[0] != '/') {
106 // relative path to cache directory.
107 $cacheDirectory = PATH_site . $cacheDirectory;
108 }
109 }
110 $basedirs = explode($delimiter, $open_basedir);
111
112 $cacheDirectoryInBaseDir = FALSE;
113
114 foreach ($basedirs as $basedir) {
115 if (TYPO3_OS === 'WIN') {
116 $basedir = str_replace('\\', '/', $basedir);
117 }
118 if ($basedir[strlen($basedir) - 1] !== '/') {
119 $basedir .= '/';
120 }
121 if (t3lib_div::isFirstPartOfStr($cacheDirectory, $basedir)) {
122 $documentRoot = $basedir;
123 $cacheDirectory = str_replace($basedir, '', $cacheDirectory);
124 $cacheDirectoryInBaseDir = TRUE;
125 break;
126 }
127 }
128
129 if (!$cacheDirectoryInBaseDir) {
130 throw new t3lib_cache_Exception(
131 'Open_basedir restriction in effect. The directory "' . $cacheDirectory . '" is not in an allowed path.'
132 );
133 }
134 } else {
135 if ($cacheDirectory[0] == '/') {
136 // absolute path to cache directory.
137 $documentRoot = '/';
138 }
139
140 if (TYPO3_OS === 'WIN') {
141 $documentRoot = '';
142 }
143 }
144
145 // after this point all paths have '/' as directory seperator
146
147 if ($cacheDirectory[strlen($cacheDirectory) - 1] !== '/') {
148 $cacheDirectory .= '/';
149 }
150
151 if (!is_writable($documentRoot . $cacheDirectory)) {
152 t3lib_div::mkdir_deep(
153 $documentRoot,
154 $cacheDirectory
155 );
156 }
157
158 if (!is_dir($documentRoot . $cacheDirectory)) {
159 throw new t3lib_cache_Exception(
160 'The directory "' . $documentRoot . $cacheDirectory . '" does not exist.',
161 1203965199
162 );
163 }
164
165 if (!is_writable($documentRoot . $cacheDirectory)) {
166 throw new t3lib_cache_Exception(
167 'The directory "' . $documentRoot . $cacheDirectory . '" is not writable.',
168 1203965200
169 );
170 }
171
172 $tagsDirectory = $cacheDirectory . 'tags/';
173
174
175
176 if (!is_writable($documentRoot . $tagsDirectory)) {
177 t3lib_div::mkdir_deep(
178 $documentRoot,
179 $tagsDirectory
180 );
181 }
182 $this->root = $documentRoot;
183 $this->cacheDirectory = $cacheDirectory;
184 }
185
186 /**
187 * Returns the directory where the cache files are stored
188 *
189 * @return string Full path of the cache directory
190 * @author Robert Lemke <robert@typo3.org>
191 */
192 public function getCacheDirectory() {
193 return $this->root . $this->cacheDirectory;
194 }
195
196 /**
197 * Saves data in a cache file.
198 *
199 * @param string An identifier for this specific cache entry
200 * @param string The data to be stored
201 * @param array Tags to associate with this cache entry
202 * @param integer Lifetime of this cache entry in seconds. If NULL is specified, the default lifetime is used. "0" means unlimited lifetime.
203 * @return void
204 * @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.
205 * @throws t3lib_cache_exception_InvalidData if the data to bes stored is not a string.
206 * @author Robert Lemke <robert@typo3.org>
207 */
208 public function set($entryIdentifier, $data, array $tags = array(), $lifetime = NULL) {
209 if (!$this->cache instanceof t3lib_cache_frontend_Frontend) {
210 throw new t3lib_cache_Exception(
211 'No cache frontend has been set yet via setCache().',
212 1204111375
213 );
214 }
215
216 if (!is_string($data)) {
217 throw new t3lib_cache_Exception_InvalidData(
218 'The specified data is of type "' . gettype($data) . '" but a string is expected.',
219 1204481674
220 );
221 }
222
223 $expirytime = $this->calculateExpiryTime($lifetime);
224 $cacheEntryPath = $this->renderCacheEntryPath($entryIdentifier);
225 $absCacheEntryPath = $this->root . $cacheEntryPath;
226
227
228 if (!is_writable($absCacheEntryPath)) {
229 try {
230 t3lib_div::mkdir_deep(
231 $this->root,
232 $cacheEntryPath
233 );
234 } catch(Exception $exception) {
235
236 }
237
238 if (!is_writable($absCacheEntryPath)) {
239 throw new t3lib_cache_Exception(
240 'The cache directory "' . $absCacheEntryPath . '" could not be created.',
241 1204026250
242 );
243 }
244 }
245
246 $this->remove($entryIdentifier);
247
248 $data = $expirytime->format(self::EXPIRYTIME_FORMAT) . $data;
249 $cacheEntryPathAndFilename = $absCacheEntryPath . uniqid() . '.temp';
250 if (strlen($cacheEntryPathAndFilename) > $this->maximumPathLength) {
251 throw new t3lib_cache_Exception(
252 'The length of the temporary cache file path "' . $cacheEntryPathAndFilename .
253 '" is ' . strlen($cacheEntryPathAndFilename) . ' characters long and exceeds the maximum path length of ' .
254 $this->maximumPathLength . '. Please consider setting the temporaryDirectoryBase option to a shorter path. ',
255 1248710426
256 );
257 }
258 $result = file_put_contents($cacheEntryPathAndFilename, $data);
259 if ($result === FALSE) {
260 throw new t3lib_cache_Exception(
261 'The temporary cache file "' . $cacheEntryPathAndFilename . '" could not be written.',
262 1204026251
263 );
264 }
265
266 for ($i = 0; $i < 5; $i++) {
267 $result = rename($cacheEntryPathAndFilename, $absCacheEntryPath . $entryIdentifier);
268 if ($result === TRUE) {
269 break;
270 }
271 }
272
273 if ($result === FALSE) {
274 throw new t3lib_cache_Exception(
275 'The cache file "' . $entryIdentifier . '" could not be written.',
276 1222361632
277 );
278 }
279
280 foreach ($tags as $tag) {
281 $this->setTag($entryIdentifier, $tag);
282 }
283 }
284
285 /**
286 * Creates a tag that is associated with the given cache identifier
287 *
288 * @param string $entryIdentifier An identifier for this specific cache entry
289 * @param string $tag Tag to associate with this cache entry
290 * @return void
291 * @throws t3lib_cache_Exception if the tag path is not writable or exceeds the maximum allowed path length
292 * @author Bastian Waidelich <bastian@typo3.org>
293 * @author Ingo Renner <ingo@typo3.org>
294 */
295 protected function setTag($entryIdentifier, $tag) {
296 $tagPath = $this->cacheDirectory . 'tags/' . $tag . '/';
297 $absTagPath = $this->root . $tagPath;
298 if (!is_writable($absTagPath)) {
299 t3lib_div::mkdir_deep($this->root, $tagPath);
300 if (!is_writable($absTagPath)) {
301 throw new t3lib_cache_Exception(
302 'The tag directory "' . $absTagPath . '" could not be created.',
303 1238242144
304 );
305 }
306 }
307
308 $tagPathAndFilename = $absTagPath . $this->cacheIdentifier
309 . self::SEPARATOR . $entryIdentifier;
310 if (strlen($tagPathAndFilename) > $this->maximumPathLength) {
311 throw new t3lib_cache_Exception(
312 'The length of the tag path "' . $tagPathAndFilename . '" is ' . strlen($tagPathAndFilename) .
313 ' characters long and exceeds the maximum path length of ' . $this->maximumPathLength .
314 '. Please consider setting the temporaryDirectoryBase option to a shorter path. ',
315 1248710426
316 );
317 }
318 touch($tagPathAndFilename);
319 }
320
321 /**
322 * Loads data from a cache file.
323 *
324 * @param string An identifier which describes the cache entry to load
325 * @return mixed The cache entry's content as a string or FALSE if the cache entry could not be loaded
326 * @author Robert Lemke <robert@typo3.org>
327 * @author Karsten Dambekalns <karsten@typo3.org>
328 */
329 public function get($entryIdentifier) {
330 $pathAndFilename = $this->root . $this->renderCacheEntryPath($entryIdentifier) . $entryIdentifier;
331 return ($this->isCacheFileExpired($pathAndFilename)) ? FALSE : file_get_contents($pathAndFilename, NULL, NULL, self::EXPIRYTIME_LENGTH);
332 }
333
334 /**
335 * Checks if a cache entry with the specified identifier exists.
336 *
337 * @param string $entryIdentifier
338 * @return boolean TRUE if such an entry exists, FALSE if not
339 * @author Robert Lemke <robert@typo3.org>
340 */
341 public function has($entryIdentifier) {
342 return !$this->isCacheFileExpired($this->root . $this->renderCacheEntryPath($entryIdentifier) . $entryIdentifier);
343 }
344
345 /**
346 * Removes all cache entries matching the specified identifier.
347 * Usually this only affects one entry.
348 *
349 * @param string Specifies the cache entry to remove
350 * @return boolean TRUE if (at least) an entry could be removed or FALSE if no entry was found
351 * @author Robert Lemke <robert@typo3.org>
352 */
353 public function remove($entryIdentifier) {
354 $pathAndFilename = $this->root . $this->renderCacheEntryPath($entryIdentifier) . $entryIdentifier;
355
356 if (!file_exists($pathAndFilename)) {
357 return FALSE;
358 }
359
360 if (unlink($pathAndFilename) === FALSE) {
361 return FALSE;
362 }
363
364 foreach($this->findTagFilesByEntry($entryIdentifier) as $pathAndFilename) {
365 if (!file_exists($pathAndFilename)) {
366 return FALSE;
367 }
368
369 if (unlink($pathAndFilename) === FALSE) {
370 return FALSE;
371 }
372 }
373
374 return TRUE;
375 }
376
377 /**
378 * Finds and returns all cache entry identifiers which are tagged by the
379 * specified tag.
380 *
381 * @param string The tag to search for
382 * @return array An array with identifiers of all matching entries. An empty array if no entries matched
383 * @author Robert Lemke <robert@typo3.org>
384 * @author Karsten Dambekalns <karsten@typo3.org>
385 */
386 public function findIdentifiersByTag($tag) {
387 if (!$this->cache instanceof t3lib_cache_frontend_Frontend) {
388 throw new t3lib_cache_Exception(
389 'Yet no cache frontend has been set via setCache().',
390 1204111376
391 );
392 }
393
394 $path = $this->root . $this->cacheDirectory . 'tags/';
395 $pattern = $path . $tag . '/' . $this->cacheIdentifier . self::SEPARATOR . '*';
396 $filesFound = glob($pattern);
397
398 if ($filesFound === FALSE || count($filesFound) === 0) {
399 return array();
400 }
401
402 $cacheEntries = array();
403 foreach ($filesFound as $filename) {
404 list(,$entryIdentifier) = explode(self::SEPARATOR, basename($filename));
405 if ($this->has($entryIdentifier)) {
406 $cacheEntries[$entryIdentifier] = $entryIdentifier;
407 }
408 }
409
410 return array_values($cacheEntries);
411 }
412
413 /**
414 * Finds and returns all cache entry identifiers which are tagged by the
415 * specified tags.
416 *
417 * @param array Array of tags to search for
418 * @return array An array with identifiers of all matching entries. An empty array if no entries matched
419 * @author Ingo Renner <ingo@typo3.org>
420 */
421 public function findIdentifiersByTags(array $tags) {
422 $taggedEntries = array();
423 $foundEntries = array();
424
425 foreach ($tags as $tag) {
426 $taggedEntries[$tag] = $this->findIdentifiersByTag($tag);
427 }
428
429 $intersectedTaggedEntries = call_user_func_array('array_intersect', $taggedEntries);
430
431 foreach ($intersectedTaggedEntries as $entryIdentifier) {
432 if ($this->has($entryIdentifier)) {
433 $foundEntries[$entryIdentifier] = $entryIdentifier;
434 }
435 }
436
437 return $foundEntries;
438 }
439
440 /**
441 * Removes all cache entries of this cache.
442 *
443 * @return void
444 * @author Robert Lemke <robert@typo3.org>
445 */
446 public function flush() {
447 if (!$this->cache instanceof t3lib_cache_frontend_Frontend) {
448 throw new t3lib_cache_Exception(
449 'Yet no cache frontend has been set via setCache().',
450 1204111376
451 );
452 }
453
454 $dataPath = $this->root . $this->cacheDirectory . 'data/' . $this->cacheIdentifier . '/';
455 $tagsPath = $this->root . $this->cacheDirectory . 'tags/';
456
457 t3lib_div::rmdir($dataPath, true);
458 t3lib_div::rmdir($tagsPath, true);
459 }
460
461 /**
462 * Removes all cache entries of this cache which are tagged by the specified tag.
463 *
464 * @param string The tag the entries must have
465 * @return void
466 * @author Ingo Renner <ingo@typo3.org>
467 */
468 public function flushByTag($tag) {
469 $identifiers = $this->findIdentifiersByTag($tag);
470 if (count($identifiers) === 0) {
471 return;
472 }
473
474 foreach ($identifiers as $entryIdentifier) {
475 $this->remove($entryIdentifier);
476 }
477 }
478
479 /**
480 * Removes all cache entries of this cache which are tagged by the specified tag.
481 *
482 * @param array The tags the entries must have
483 * @return void
484 * @author Ingo Renner <ingo@typo3.org>
485 */
486 public function flushByTags(array $tags) {
487 foreach ($tags as $tag) {
488 $this->flushByTag($tag);
489 }
490 }
491
492 /**
493 * Checks if the given cache entry files are still valid or if their
494 * lifetime has exceeded.
495 *
496 * @param string $cacheFilename
497 * @return boolean
498 * @author Robert Lemke <robert@typo3.org>
499 */
500 protected function isCacheFileExpired($cacheFilename) {
501 $timestamp = (file_exists($cacheFilename)) ? file_get_contents($cacheFilename, NULL, NULL, 0, self::EXPIRYTIME_LENGTH) : 1;
502 return $timestamp < gmdate(self::EXPIRYTIME_FORMAT);
503 }
504
505 /**
506 * Does garbage collection
507 *
508 * @return void
509 * @author Karsten Dambekalns <karsten@typo3.org>
510 */
511 public function collectGarbage() {
512 if (!$this->cache instanceof t3lib_cache_frontend_Frontend) {
513 throw new t3lib_cache_Exception(
514 'Yet no cache frontend has been set via setCache().',
515 1222686150
516 );
517 }
518
519 $pattern = $this->root . $this->cacheDirectory . 'data/' . $this->cacheIdentifier . '/*/*/*';
520 $filesFound = glob($pattern);
521
522 foreach ($filesFound as $cacheFilename) {
523 if ($this->isCacheFileExpired($cacheFilename)) {
524 $this->remove(basename($cacheFilename));
525 }
526 }
527 }
528
529 /**
530 * Renders the full path (excluding file name) leading to the given cache entry.
531 * Doesn't check if such a cache entry really exists.
532 *
533 * @param string $identifier Identifier for the cache entry
534 * @return string Absolute path leading to the directory containing the cache entry
535 * @author Robert Lemke <robert@typo3.org>
536 * @internal
537 */
538 protected function renderCacheEntryPath($identifier) {
539 $identifierHash = sha1($identifier);
540 return $this->cacheDirectory . 'data/' . $this->cacheIdentifier . '/' . $identifierHash[0] . '/' . $identifierHash[1] . '/';
541 }
542
543 /**
544 * Tries to find the cache entry for the specified identifier.
545 * Usually only one cache entry should be found - if more than one exist, this
546 * is due to some error or crash.
547 *
548 * @param string $entryIdentifier The cache entry identifier
549 * @return mixed The file names (including path) as an array if one or more entries could be found, otherwise FALSE
550 * @author Robert Lemke <robert@typo3.org>
551 * @throws t3lib_cache_Exception if no frontend has been set
552 * @internal
553 */
554 protected function findCacheFilesByIdentifier($entryIdentifier) {
555 if (!$this->cache instanceof t3lib_cache_frontend_Frontend) {
556 throw new t3lib_cache_Exception(
557 'Yet no cache frontend has been set via setCache().',
558 1204111376
559 );
560 }
561
562 $pattern = $this->root . $this->renderCacheEntryPath($entryIdentifier) . $entryIdentifier;
563 $filesFound = glob($pattern);
564 if ($filesFound === FALSE || count($filesFound) === 0) {
565 return FALSE;
566 }
567
568 return $filesFound;
569 }
570
571
572 /**
573 * Tries to find the tag entries for the specified cache entry.
574 *
575 * @param string $entryIdentifier The cache entry identifier to find tag files for
576 * @return array The file names (including path)
577 * @author Robert Lemke <robert@typo3.org>
578 * @throws t3lib_cache_Exception if no frontend has been set
579 * @internal
580 */
581 protected function findTagFilesByEntry($entryIdentifier) {
582 if (!$this->cache instanceof t3lib_cache_frontend_Frontend) {
583 throw new t3lib_cache_Exception(
584 'Yet no cache frontend has been set via setCache().',
585 1204111376
586 );
587 }
588
589 $path = $this->root . $this->cacheDirectory . 'tags/';
590 $pattern = $path . '*/' . $this->cacheIdentifier . self::SEPARATOR . $entryIdentifier;
591 $tagFilesFound = glob($pattern);
592 return ($tagFilesFound ? $tagFilesFound : array());
593 }
594 }
595
596
597 if (defined('TYPO3_MODE') && $TYPO3_CONF_VARS[TYPO3_MODE]['XCLASS']['t3lib/cache/backend/class.t3lib_cache_backend_filebackend.php']) {
598 include_once($TYPO3_CONF_VARS[TYPO3_MODE]['XCLASS']['t3lib/cache/backend/class.t3lib_cache_backend_filebackend.php']);
599 }
600
601 ?>