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