Fixed bug #18016
[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 /**
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 */
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 * @var string Directory where the files are stored
46 */
47 protected $cacheDirectory = '';
48
49 /**
50 * @var string Absolute path to root, usually document root of website
51 */
52 protected $root = '/';
53
54 /**
55 * Maximum allowed file path length in the current environment.
56 *
57 * @var integer
58 */
59 protected $maximumPathLength = null;
60
61 /**
62 * Constructs this backend
63 *
64 * @param array $options Configuration options - depends on the actual backend
65 */
66 public function __construct(array $options = array()) {
67 parent::__construct($options);
68
69 if (is_null($this->maximumPathLength)) {
70 $this->maximumPathLength = t3lib_div::getMaximumPathLength();
71 }
72 }
73
74 /**
75 * Sets a reference to the cache frontend which uses this backend and
76 * initializes the default cache directory
77 *
78 * @void
79 * @author Robert Lemke <robert@typo3.org>
80 */
81 public function setCache(t3lib_cache_frontend_Frontend $cache) {
82 parent::setCache($cache);
83
84 if (empty($this->cacheDirectory)) {
85 $cacheDirectory = 'typo3temp/cache/';
86 try {
87 $this->setCacheDirectory($cacheDirectory);
88 } catch (t3lib_cache_Exception $exception) {
89 }
90 }
91 }
92
93 /**
94 * Sets the directory where the cache files are stored. By default it is
95 * assumed that the directory is below the TYPO3_DOCUMENT_ROOT. However, an
96 * absolute path can be selected, too.
97 *
98 * @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.
99 * @return void
100 * @throws t3lib_cache_Exception if the directory does not exist, is not writable or could not be created.
101 * @author Robert Lemke <robert@typo3.org>
102 * @author Ingo Renner <ingo@typo3.org>
103 */
104 public function setCacheDirectory($cacheDirectory) {
105 $documentRoot = PATH_site;
106
107 if (($open_basedir = ini_get('open_basedir'))) {
108 if (TYPO3_OS === 'WIN') {
109 $delimiter = ';';
110 $cacheDirectory = str_replace('\\', '/', $cacheDirectory);
111 if (!(preg_match('/[A-Z]:/', substr($cacheDirectory, 0, 2)))) {
112 $cacheDirectory = PATH_site . $cacheDirectory;
113 }
114 } else {
115 $delimiter = ':';
116 if ($cacheDirectory[0] != '/') {
117 // relative path to cache directory.
118 $cacheDirectory = PATH_site . $cacheDirectory;
119 }
120 }
121
122 $basedirs = explode($delimiter, $open_basedir);
123 $cacheDirectoryInBaseDir = FALSE;
124 foreach ($basedirs as $basedir) {
125 if (TYPO3_OS === 'WIN') {
126 $basedir = str_replace('\\', '/', $basedir);
127 }
128 if ($basedir[strlen($basedir) - 1] !== '/') {
129 $basedir .= '/';
130 }
131 if (t3lib_div::isFirstPartOfStr($cacheDirectory, $basedir)) {
132 $documentRoot = $basedir;
133 $cacheDirectory = str_replace($basedir, '', $cacheDirectory);
134 $cacheDirectoryInBaseDir = TRUE;
135 break;
136 }
137 }
138 if (!$cacheDirectoryInBaseDir) {
139 throw new t3lib_cache_Exception(
140 'Open_basedir restriction in effect. The directory "' . $cacheDirectory . '" is not in an allowed path.'
141 );
142 }
143 } else {
144 if ($cacheDirectory[0] == '/') {
145 // Absolute path to cache directory.
146 $documentRoot = '/';
147 }
148 if (TYPO3_OS === 'WIN') {
149 if (substr($cacheDirectory, 0, strlen($documentRoot)) === $documentRoot) {
150 $documentRoot = '';
151 }
152 }
153 }
154
155 // After this point all paths have '/' as directory seperator
156 if ($cacheDirectory[strlen($cacheDirectory) - 1] !== '/') {
157 $cacheDirectory .= '/';
158 }
159 $cacheDirectory .= $this->cacheIdentifier;
160 if ($cacheDirectory[strlen($cacheDirectory) - 1] !== '/') {
161 $cacheDirectory .= '/';
162 }
163
164 if (!is_writable($documentRoot . $cacheDirectory)) {
165 t3lib_div::mkdir_deep(
166 $documentRoot,
167 $cacheDirectory
168 );
169 }
170 if (!is_dir($documentRoot . $cacheDirectory)) {
171 throw new t3lib_cache_Exception(
172 'The directory "' . $documentRoot . $cacheDirectory . '" does not exist.',
173 1203965199
174 );
175 }
176 if (!is_writable($documentRoot . $cacheDirectory)) {
177 throw new t3lib_cache_Exception(
178 'The directory "' . $documentRoot . $cacheDirectory . '" is not writable.',
179 1203965200
180 );
181 }
182
183 $this->root = $documentRoot;
184 $this->cacheDirectory = $cacheDirectory;
185 }
186
187 /**
188 * Returns the directory where the cache files are stored
189 *
190 * @return string Full path of the cache directory
191 * @author Robert Lemke <robert@typo3.org>
192 * @api
193 */
194 public function getCacheDirectory() {
195 return $this->root . $this->cacheDirectory;
196 }
197
198 /**
199 * Saves data in a cache file.
200 *
201 * @param string $entryIdentifier An identifier for this specific cache entry
202 * @param string $data The data to be stored
203 * @param array $tags Tags to associate with this cache entry
204 * @param integer $lifetime Lifetime of this cache entry in seconds. If NULL is specified, the default lifetime is used. "0" means unlimited lifetime.
205 * @return void
206 * @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.
207 * @throws t3lib_cache_exception_InvalidData if the data to bes stored is not a string.
208 * @author Robert Lemke <robert@typo3.org>
209 * @api
210 */
211 public function set($entryIdentifier, $data, array $tags = array(), $lifetime = NULL) {
212 if (!$this->cache instanceof t3lib_cache_frontend_Frontend) {
213 throw new t3lib_cache_Exception(
214 'No cache frontend has been set yet via setCache().',
215 1204111375
216 );
217 }
218
219 if (!is_string($data)) {
220 throw new t3lib_cache_Exception_InvalidData(
221 'The specified data is of type "' . gettype($data) . '" but a string is expected.',
222 1204481674
223 );
224 }
225
226 if ($entryIdentifier !== basename($entryIdentifier)) {
227 throw new InvalidArgumentException(
228 'The specified entry identifier must not contain a path segment.',
229 1282073032
230 );
231 }
232
233 $this->remove($entryIdentifier);
234
235 $temporaryCacheEntryPathAndFilename = $this->root . $this->cacheDirectory . uniqid() . '.temp';
236 if (strlen($temporaryCacheEntryPathAndFilename) > $this->maximumPathLength) {
237 throw new t3lib_cache_Exception(
238 'The length of the temporary cache file path "' . $temporaryCacheEntryPathAndFilename .
239 '" is ' . strlen($temporaryCacheEntryPathAndFilename) . ' characters long and exceeds the maximum path length of ' .
240 $this->maximumPathLength . '. Please consider setting the temporaryDirectoryBase option to a shorter path. ',
241 1248710426
242 );
243 }
244
245 $expiryTime = ($lifetime === NULL) ? 0 : ($GLOBALS['EXEC_TIME'] + $lifetime);
246 $metaData = str_pad($expiryTime, self::EXPIRYTIME_LENGTH) . implode(' ', $tags) . str_pad(strlen($data), self::DATASIZE_DIGITS);
247 $result = file_put_contents($temporaryCacheEntryPathAndFilename, $data . $metaData);
248
249 if ($result === FALSE) {
250 throw new t3lib_cache_exception(
251 'The temporary cache file "' . $temporaryCacheEntryPathAndFilename . '" could not be written.',
252 1204026251
253 );
254 }
255
256 $i = 0;
257 $cacheEntryPathAndFilename = $this->root . $this->cacheDirectory . $entryIdentifier;
258 // @TODO: Figure out why the heck this is done and maybe find a smarter solution, report to FLOW3
259 while (!rename($temporaryCacheEntryPathAndFilename, $cacheEntryPathAndFilename) && $i < 5) {
260 $i++;
261 }
262
263 // @FIXME: At least the result of rename() should be handled here, report to FLOW3
264 if ($result === FALSE) {
265 throw new t3lib_cache_exception(
266 'The cache file "' . $cacheEntryPathAndFilename . '" could not be written.',
267 1222361632
268 );
269 }
270 }
271
272 /**
273 * Loads data from a cache file.
274 *
275 * @param string $entryIdentifier An identifier which describes the cache entry to load
276 * @return mixed The cache entry's content as a string or FALSE if the cache entry could not be loaded
277 * @author Robert Lemke <robert@typo3.org>
278 * @author Karsten Dambekalns <karsten@typo3.org>
279 * @api
280 */
281 public function get($entryIdentifier) {
282 if ($entryIdentifier !== basename($entryIdentifier)) {
283 throw new InvalidArgumentException(
284 'The specified entry identifier must not contain a path segment.',
285 1282073033
286 );
287 }
288
289 $pathAndFilename = $this->root . $this->cacheDirectory . $entryIdentifier;
290 if ($this->isCacheFileExpired($pathAndFilename)) {
291 return FALSE;
292 }
293 $dataSize = (integer) file_get_contents($pathAndFilename, NULL, NULL, filesize($pathAndFilename) - self::DATASIZE_DIGITS, self::DATASIZE_DIGITS);
294 return file_get_contents($pathAndFilename, NULL, NULL, 0, $dataSize);
295 }
296
297 /**
298 * Checks if a cache entry with the specified identifier exists.
299 *
300 * @param string $entryIdentifier Specifies the cache entry to remove
301 * @return boolean TRUE if (at least) an entry could be removed or FALSE if no entry was found
302 * @author Robert Lemke <robert@typo3.org>
303 * @api
304 */
305 public function has($entryIdentifier) {
306 if ($entryIdentifier !== basename($entryIdentifier)) {
307 throw new InvalidArgumentException(
308 'The specified entry identifier must not contain a path segment.',
309 1282073034
310 );
311 }
312
313 return !$this->isCacheFileExpired($this->root . $this->cacheDirectory . $entryIdentifier);
314 }
315
316 /**
317 * Removes all cache entries matching the specified identifier.
318 * Usually this only affects one entry.
319 *
320 * @param string $entryIdentifier Specifies the cache entry to remove
321 * @return boolean TRUE if (at least) an entry could be removed or FALSE if no entry was found
322 * @author Robert Lemke <robert@typo3.org>
323 * @api
324 */
325 public function remove($entryIdentifier) {
326 if ($entryIdentifier !== basename($entryIdentifier)) {
327 throw new InvalidArgumentException(
328 'The specified entry identifier must not contain a path segment.',
329 1282073035
330 );
331 }
332
333 $pathAndFilename = $this->root . $this->cacheDirectory . $entryIdentifier;
334 if (!file_exists($pathAndFilename)) {
335 return FALSE;
336 }
337 if (unlink($pathAndFilename) === FALSE) {
338 return FALSE;
339 }
340 return TRUE;
341 }
342
343 /**
344 * Finds and returns all cache entry identifiers which are tagged by the
345 * specified tag.
346 *
347 * @param string $searchedTag The tag to search for
348 * @return array An array with identifiers of all matching entries. An empty array if no entries matched
349 * @author Robert Lemke <robert@typo3.org>
350 * @author Karsten Dambekalns <karsten@typo3.org>
351 * @api
352 */
353 public function findIdentifiersByTag($searchedTag) {
354 $entryIdentifiers = array();
355 $now = $GLOBALS['EXEC_TIME'];
356 for ($directoryIterator = t3lib_div::makeInstance('DirectoryIterator', $this->root . $this->cacheDirectory); $directoryIterator->valid(); $directoryIterator->next()) {
357 if ($directoryIterator->isDot()) {
358 continue;
359 }
360 $cacheEntryPathAndFilename = $directoryIterator->getPathname();
361 $index = (integer) file_get_contents($cacheEntryPathAndFilename, NULL, NULL, filesize($cacheEntryPathAndFilename) - self::DATASIZE_DIGITS, self::DATASIZE_DIGITS);
362 $metaData = file_get_contents($cacheEntryPathAndFilename, NULL, NULL, $index);
363
364 $expiryTime = (integer) substr($metaData, 0, self::EXPIRYTIME_LENGTH);
365 if ($expiryTime !== 0 && $expiryTime < $now) {
366 continue;
367 }
368 if (in_array($searchedTag, explode(' ', substr($metaData, self::EXPIRYTIME_LENGTH, -self::DATASIZE_DIGITS)))) {
369 $entryIdentifiers[] = $directoryIterator->getFilename();
370 }
371 }
372 return $entryIdentifiers;
373 }
374
375 /**
376 * Finds and returns all cache entry identifiers which are tagged by the
377 * specified tags.
378 *
379 * @param array $searchedTags Array of tags to search for
380 * @return array An array with identifiers of all matching entries. An empty array if no entries matched
381 * @author Ingo Renner <ingo@typo3.org>
382 * @author Christian Kuhn <lolli@schwarzbu.ch>
383 * @api
384 */
385 public function findIdentifiersByTags(array $searchedTags) {
386 $entryIdentifiers = array();
387 for ($directoryIterator = t3lib_div::makeInstance('DirectoryIterator', $this->root . $this->cacheDirectory); $directoryIterator->valid(); $directoryIterator->next()) {
388 if ($directoryIterator->isDot()) {
389 continue;
390 }
391 $cacheEntryPathAndFilename = $directoryIterator->getPathname();
392 $index = (integer) file_get_contents($cacheEntryPathAndFilename, NULL, NULL, filesize($cacheEntryPathAndFilename) - self::DATASIZE_DIGITS, self::DATASIZE_DIGITS);
393 $metaData = file_get_contents($cacheEntryPathAndFilename, NULL, NULL, $index);
394
395 $expiryTime = (integer) substr($metaData, 0, self::EXPIRYTIME_LENGTH);
396 if ($expiryTime !== 0 && $expiryTime < $GLOBALS['EXEC_TIME']) {
397 continue;
398 }
399 if (in_array($searchedTags, explode(' ', substr($metaData, self::EXPIRYTIME_LENGTH, -self::DATASIZE_DIGITS)))) {
400 $entryIdentifiers[] = $directoryIterator->getFilename();
401 }
402 }
403 return $entryIdentifiers;
404 }
405
406 /**
407 * Removes all cache entries of this cache.
408 *
409 * @return void
410 * @author Robert Lemke <robert@typo3.org>
411 * @author Christian Kuhn <lolli@schwarzbu.ch>
412 * @api
413 */
414 public function flush() {
415 t3lib_div::rmdir($this->root . $this->cacheDirectory, TRUE);
416 }
417
418 /**
419 * Removes all cache entries of this cache which are tagged by the specified tag.
420 *
421 * @param string $tag The tag the entries must have
422 * @return void
423 * @author Ingo Renner <ingo@typo3.org>
424 * @api
425 */
426 public function flushByTag($tag) {
427 $identifiers = $this->findIdentifiersByTag($tag);
428 if (count($identifiers) === 0) {
429 return;
430 }
431
432 foreach ($identifiers as $entryIdentifier) {
433 $this->remove($entryIdentifier);
434 }
435 }
436
437 /**
438 * Removes all cache entries of this cache which are tagged by the specified tag.
439 *
440 * @param array $tags The tags the entries must have
441 * @return void
442 * @author Ingo Renner <ingo@typo3.org>
443 * @api
444 */
445 public function flushByTags(array $tags) {
446 foreach ($tags as $tag) {
447 $this->flushByTag($tag);
448 }
449 }
450
451 /**
452 * Checks if the given cache entry files are still valid or if their
453 * lifetime has exceeded.
454 *
455 * @param string $cacheEntryPathAndFilename
456 * @return boolean
457 * @author Robert Lemke <robert@typo3.org>
458 */
459 protected function isCacheFileExpired($cacheEntryPathAndFilename) {
460 if (!file_exists($cacheEntryPathAndFilename)) {
461 return TRUE;
462 }
463 $index = (integer) file_get_contents($cacheEntryPathAndFilename, NULL, NULL, filesize($cacheEntryPathAndFilename) - self::DATASIZE_DIGITS, self::DATASIZE_DIGITS);
464 $expiryTime = file_get_contents($cacheEntryPathAndFilename, NULL, NULL, $index, self::EXPIRYTIME_LENGTH);
465 return ($expiryTime != 0 && $expiryTime < $GLOBALS['EXEC_TIME']);
466 }
467
468 /**
469 * Does garbage collection
470 *
471 * @return void
472 * @author Karsten Dambekalns <karsten@typo3.org>
473 * @api
474 */
475 public function collectGarbage() {
476 if (!$this->cache instanceof t3lib_cache_frontend_Frontend) {
477 throw new t3lib_cache_Exception(
478 'Yet no cache frontend has been set via setCache().',
479 1222686150
480 );
481 }
482
483 $pattern = $this->root . $this->cacheDirectory . '*';
484 $filesFound = glob($pattern);
485
486 if (is_array($filesFound)) {
487 foreach ($filesFound as $cacheFilename) {
488 if ($this->isCacheFileExpired($cacheFilename)) {
489 $this->remove(basename($cacheFilename));
490 }
491 }
492 }
493 }
494
495 /**
496 * Tries to find the cache entry for the specified identifier.
497 * Usually only one cache entry should be found - if more than one exist, this
498 * is due to some error or crash.
499 *
500 * @param string $entryIdentifier The cache entry identifier
501 * @return mixed The file names (including path) as an array if one or more entries could be found, otherwise FALSE
502 * @author Robert Lemke <robert@typo3.org>
503 * @throws t3lib_cache_Exception if no frontend has been set
504 * @internal
505 */
506 protected function findCacheFilesByIdentifier($entryIdentifier) {
507 if (!$this->cache instanceof t3lib_cache_frontend_Frontend) {
508 throw new t3lib_cache_Exception(
509 'Yet no cache frontend has been set via setCache().',
510 1204111376
511 );
512 }
513
514 $pattern = $this->root . $this->cacheDirectory . $entryIdentifier;
515 $filesFound = glob($pattern);
516 if ($filesFound === FALSE || count($filesFound) === 0) {
517 return FALSE;
518 }
519
520 return $filesFound;
521 }
522
523 /**
524 * Loads PHP code from the cache and require_onces it right away.
525 *
526 * @param string $entryIdentifier An identifier which describes the cache entry to load
527 * @return mixed Potential return value from the include operation
528 * @api
529 */
530 public function requireOnce($entryIdentifier) {
531 if ($entryIdentifier !== basename($entryIdentifier)) {
532 throw new InvalidArgumentException(
533 'The specified entry identifier must not contain a path segment.',
534 1282073036
535 );
536 }
537
538 $pathAndFilename = $this->root . $this->cacheDirectory . $entryIdentifier;
539 return ($this->isCacheFileExpired($pathAndFilename)) ? FALSE : require_once($pathAndFilename);
540 }
541 }
542
543
544 if (defined('TYPO3_MODE') && isset($GLOBALS['TYPO3_CONF_VARS'][TYPO3_MODE]['XCLASS']['t3lib/cache/backend/class.t3lib_cache_backend_filebackend.php'])) {
545 include_once($GLOBALS['TYPO3_CONF_VARS'][TYPO3_MODE]['XCLASS']['t3lib/cache/backend/class.t3lib_cache_backend_filebackend.php']);
546 }
547
548 ?>