6b5e6d18216f6187f0bd158b75dae2b32e3e78f5
[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 * @author Robert Lemke <robert@typo3.org>
33 * @author Christian Kuhn <lolli@schwarzbu.ch>
34 * @author Karsten Dambekalns <karsten@typo3.org>
35 * @api
36 * @scope prototype
37 */
38 class t3lib_cache_backend_FileBackend extends t3lib_cache_backend_AbstractBackend implements t3lib_cache_backend_PhpCapableBackend {
39
40 const SEPARATOR = '^';
41
42 const EXPIRYTIME_FORMAT = 'YmdHis';
43 const EXPIRYTIME_LENGTH = 14;
44
45 const DATASIZE_DIGITS = 10;
46
47 /**
48 * Directory where the files are stored
49 *
50 * @var string
51 */
52 protected $cacheDirectory = '';
53
54 /**
55 * TYPO3 v4 note: This variable is only available in v5
56 * Temporary path to cache directory before setCache() was called. It is
57 * set by setCacheDirectory() and used in setCache() method which calls
58 * the directory creation if needed. The variable is not used afterwards,
59 * the final cache directory path is stored in $this->cacheDirectory then.
60 *
61 * @var string Temporary path to cache directory
62 */
63 protected $temporaryCacheDirectory = '';
64
65 /**
66 * A file extension to use for each cache entry.
67 *
68 * @var string
69 */
70 protected $cacheEntryFileExtension = '';
71
72
73 /**
74 * Sets a reference to the cache frontend which uses this backend and
75 * initializes the default cache directory.
76 *
77 * TYPO3 v4 note: This method is different between TYPO3 v4 and FLOW3
78 * because the Environment class to get the path to a temporary directory
79 * does not exist in v4.
80 *
81 * @param t3lib_cache_frontend_Frontend $cache The cache frontend
82 * @return void
83 */
84 public function setCache(t3lib_cache_frontend_Frontend $cache) {
85 parent::setCache($cache);
86
87 if (empty($this->temporaryCacheDirectory)) {
88 // If no cache directory was given with cacheDirectory
89 // configuration option, set it to a path below typo3temp/
90 $temporaryCacheDirectory = PATH_site . 'typo3temp/';
91 } else {
92 $temporaryCacheDirectory = $this->temporaryCacheDirectory;
93 }
94
95 $codeOrData = ($cache instanceof t3lib_cache_frontend_PhpFrontend) ? 'Code' : 'Data';
96 $finalCacheDirectory = $temporaryCacheDirectory . 'Cache/' . $codeOrData . '/' . $this->cacheIdentifier . '/';
97
98 if (!is_dir($finalCacheDirectory)) {
99 $this->createFinalCacheDirectory($finalCacheDirectory);
100 }
101 unset($this->temporaryCacheDirectory);
102 $this->cacheDirectory = $finalCacheDirectory;
103
104 $this->cacheEntryFileExtension = ($cache instanceof t3lib_cache_frontend_PhpFrontend) ? '.php' : '';
105 }
106
107 /**
108 * Sets the directory where the cache files are stored. By default it is
109 * assumed that the directory is below the TYPO3_DOCUMENT_ROOT. However, an
110 * absolute path can be selected, too.
111 *
112 * This method does not exist in FLOW3 anymore, but it is needed in
113 * TYPO3 v4 to enable a cache path outside of document root. The final
114 * cache path is checked and created in createFinalCachDirectory(),
115 * called by setCache() method, which is done _after_ the cacheDirectory
116 * option was handled.
117 *
118 * @param string $cacheDirectory The cache base directory. If a relative path
119 * is given, it is assumed it is in TYPO3_DOCUMENT_ROOT. If an absolute
120 * path is given it is taken as is.
121 * @return void
122 * @throws t3lib_cache_Exception if the directory is not within allowed
123 * open_basedir path.
124 */
125 public function setCacheDirectory($cacheDirectory) {
126 // Skip handling if directory is a stream ressource
127 // This is used by unit tests with vfs:// directoryies
128 if (strpos($cacheDirectory, '://')) {
129 $this->temporaryCacheDirectory = $cacheDirectory;
130 return;
131 }
132
133 $documentRoot = PATH_site;
134
135 if (($open_basedir = ini_get('open_basedir'))) {
136 if (TYPO3_OS === 'WIN') {
137 $delimiter = ';';
138 $cacheDirectory = str_replace('\\', '/', $cacheDirectory);
139 if (!(preg_match('/[A-Z]:/', substr($cacheDirectory, 0, 2)))) {
140 $cacheDirectory = PATH_site . $cacheDirectory;
141 }
142 } else {
143 $delimiter = ':';
144 if ($cacheDirectory[0] != '/') {
145 // relative path to cache directory.
146 $cacheDirectory = PATH_site . $cacheDirectory;
147 }
148 }
149
150 $basedirs = explode($delimiter, $open_basedir);
151 $cacheDirectoryInBaseDir = FALSE;
152 foreach ($basedirs as $basedir) {
153 if (TYPO3_OS === 'WIN') {
154 $basedir = str_replace('\\', '/', $basedir);
155 }
156 if ($basedir[strlen($basedir) - 1] !== '/') {
157 $basedir .= '/';
158 }
159 if (t3lib_div::isFirstPartOfStr($cacheDirectory, $basedir)) {
160 $documentRoot = $basedir;
161 $cacheDirectory = str_replace($basedir, '', $cacheDirectory);
162 $cacheDirectoryInBaseDir = TRUE;
163 break;
164 }
165 }
166 if (!$cacheDirectoryInBaseDir) {
167 throw new t3lib_cache_Exception(
168 'Open_basedir restriction in effect. The directory "' . $cacheDirectory . '" is not in an allowed path.'
169 );
170 }
171 } else {
172 if ($cacheDirectory[0] == '/') {
173 // Absolute path to cache directory.
174 $documentRoot = '/';
175 }
176 if (TYPO3_OS === 'WIN') {
177 if (substr($cacheDirectory, 0, strlen($documentRoot)) === $documentRoot) {
178 $documentRoot = '';
179 }
180 }
181 }
182
183 // After this point all paths have '/' as directory seperator
184 if ($cacheDirectory[strlen($cacheDirectory) - 1] !== '/') {
185 $cacheDirectory .= '/';
186 }
187
188 $this->temporaryCacheDirectory = $documentRoot . $cacheDirectory . $this->cacheIdentifier . '/';
189 }
190
191 /**
192 * Create the final cache directory if it does not exist. This method
193 * exists in TYPO3 v4 only.
194 *
195 * @param string $finalCacheDirectory Absolute path to final cache directory
196 * @return void
197 * @throws \t3lib_cache_Exception If directory is not writable after creation
198 */
199 protected function createFinalCacheDirectory($finalCacheDirectory) {
200 try {
201 t3lib_div::mkdir_deep($finalCacheDirectory);
202 } catch (\RuntimeException $e) {
203 throw new \t3lib_cache_Exception(
204 'The directory "' . $finalCacheDirectory . '" can not be created.',
205 1303669848,
206 $e
207 );
208 }
209 if (!is_writable($finalCacheDirectory)) {
210 throw new \t3lib_cache_Exception(
211 'The directory "' . $finalCacheDirectory . '" is not writable.',
212 1203965200
213 );
214 }
215 }
216
217 /**
218 * Returns the directory where the cache files are stored
219 *
220 * @return string Full path of the cache directory
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 * @api
238 */
239 public function set($entryIdentifier, $data, array $tags = array(), $lifetime = NULL) {
240 if (!is_string($data)) {
241 throw new t3lib_cache_Exception_InvalidData(
242 'The specified data is of type "' . gettype($data) . '" but a string is expected.',
243 1204481674
244 );
245 }
246
247 if ($entryIdentifier !== basename($entryIdentifier)) {
248 throw new \InvalidArgumentException(
249 'The specified entry identifier must not contain a path segment.',
250 1282073032
251 );
252 }
253
254 if ($entryIdentifier === '') {
255 throw new \InvalidArgumentException(
256 'The specified entry identifier must not be empty.',
257 1298114280
258 );
259 }
260
261 $this->remove($entryIdentifier);
262
263 $temporaryCacheEntryPathAndFilename = $this->cacheDirectory . uniqid() . '.temp';
264 if (strlen($temporaryCacheEntryPathAndFilename) > t3lib_div::getMaximumPathLength()) {
265 throw new t3lib_cache_Exception(
266 'The length of the temporary cache file path "' . $temporaryCacheEntryPathAndFilename .
267 '" is ' . strlen($temporaryCacheEntryPathAndFilename) . ' characters long and exceeds the maximum path length of ' .
268 t3lib_div::getMaximumPathLength() . '. Please consider setting the temporaryDirectoryBase option to a shorter path. ',
269 1248710426
270 );
271 }
272
273 $expiryTime = ($lifetime === NULL) ? 0 : ($GLOBALS['EXEC_TIME'] + $lifetime);
274 $metaData = str_pad($expiryTime, self::EXPIRYTIME_LENGTH) . implode(' ', $tags) . str_pad(strlen($data), self::DATASIZE_DIGITS);
275 $result = file_put_contents($temporaryCacheEntryPathAndFilename, $data . $metaData);
276 t3lib_div::fixPermissions($temporaryCacheEntryPathAndFilename);
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 * @api
308 */
309 public function get($entryIdentifier) {
310 if ($entryIdentifier !== basename($entryIdentifier)) {
311 throw new \InvalidArgumentException(
312 'The specified entry identifier must not contain a path segment.',
313 1282073033
314 );
315 }
316
317 $pathAndFilename = $this->cacheDirectory . $entryIdentifier . $this->cacheEntryFileExtension;
318 if ($this->isCacheFileExpired($pathAndFilename)) {
319 return FALSE;
320 }
321 $dataSize = (integer) file_get_contents($pathAndFilename, NULL, NULL, filesize($pathAndFilename) - self::DATASIZE_DIGITS, self::DATASIZE_DIGITS);
322 return file_get_contents($pathAndFilename, NULL, NULL, 0, $dataSize);
323 }
324
325 /**
326 * Checks if a cache entry with the specified identifier exists.
327 *
328 * @param string $entryIdentifier
329 * @return boolean TRUE if such an entry exists, FALSE if not
330 * @api
331 */
332 public function has($entryIdentifier) {
333 if ($entryIdentifier !== basename($entryIdentifier)) {
334 throw new \InvalidArgumentException(
335 'The specified entry identifier must not contain a path segment.',
336 1282073034
337 );
338 }
339
340 return !$this->isCacheFileExpired($this->cacheDirectory . $entryIdentifier . $this->cacheEntryFileExtension);
341 }
342
343 /**
344 * Removes all cache entries matching the specified identifier.
345 * Usually this only affects one entry.
346 *
347 * @param string $entryIdentifier Specifies the cache entry to remove
348 * @return boolean TRUE if (at least) an entry could be removed or FALSE if no entry was found
349 * @api
350 */
351 public function remove($entryIdentifier) {
352 if ($entryIdentifier !== basename($entryIdentifier)) {
353 throw new \InvalidArgumentException(
354 'The specified entry identifier must not contain a path segment.',
355 1282073035
356 );
357 }
358 if ($entryIdentifier === '') {
359 throw new \InvalidArgumentException(
360 'The specified entry identifier must not be empty.',
361 1298114279
362 );
363 }
364
365 $pathAndFilename = $this->cacheDirectory . $entryIdentifier . $this->cacheEntryFileExtension;
366 if (file_exists($pathAndFilename) === FALSE) {
367 return FALSE;
368 }
369 if (unlink($pathAndFilename) === FALSE) {
370 return FALSE;
371 }
372 return TRUE;
373 }
374
375 /**
376 * Finds and returns all cache entry identifiers which are tagged by the
377 * specified tag.
378 *
379 * @param string $searchedTag The tag to search for
380 * @return array An array with identifiers of all matching entries. An empty array if no entries matched
381 * @api
382 */
383 public function findIdentifiersByTag($searchedTag) {
384 $entryIdentifiers = array();
385 $now = $GLOBALS['EXEC_TIME'];
386 $cacheEntryFileExtensionLength = strlen($this->cacheEntryFileExtension);
387 for ($directoryIterator = t3lib_div::makeInstance('DirectoryIterator', $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 < $now) {
397 continue;
398 }
399 if (in_array($searchedTag, explode(' ', substr($metaData, self::EXPIRYTIME_LENGTH, -self::DATASIZE_DIGITS)))) {
400 if ($cacheEntryFileExtensionLength > 0) {
401 $entryIdentifiers[] = substr($directoryIterator->getFilename(), 0, - $cacheEntryFileExtensionLength);
402 } else {
403 $entryIdentifiers[] = $directoryIterator->getFilename();
404 }
405 }
406 }
407 return $entryIdentifiers;
408 }
409
410 /**
411 * Removes all cache entries of this cache.
412 *
413 * @return void
414 * @api
415 */
416 public function flush() {
417 t3lib_div::rmdir($this->cacheDirectory, TRUE);
418 $this->createFinalCacheDirectory($this->cacheDirectory);
419 }
420
421 /**
422 * Removes all cache entries of this cache which are tagged by the specified tag.
423 *
424 * @param string $tag The tag the entries must have
425 * @return void
426 * @api
427 */
428 public function flushByTag($tag) {
429 $identifiers = $this->findIdentifiersByTag($tag);
430 if (count($identifiers) === 0) {
431 return;
432 }
433
434 foreach ($identifiers as $entryIdentifier) {
435 $this->remove($entryIdentifier);
436 }
437 }
438
439 /**
440 * Checks if the given cache entry files are still valid or if their
441 * lifetime has exceeded.
442 *
443 * @param string $cacheEntryPathAndFilename
444 * @return boolean
445 * @api
446 */
447 protected function isCacheFileExpired($cacheEntryPathAndFilename) {
448 if (file_exists($cacheEntryPathAndFilename) === FALSE) {
449 return TRUE;
450 }
451 $index = (integer) file_get_contents($cacheEntryPathAndFilename, NULL, NULL, filesize($cacheEntryPathAndFilename) - self::DATASIZE_DIGITS, self::DATASIZE_DIGITS);
452 $expiryTime = file_get_contents($cacheEntryPathAndFilename, NULL, NULL, $index, self::EXPIRYTIME_LENGTH);
453 return ($expiryTime != 0 && $expiryTime < $GLOBALS['EXEC_TIME']);
454 }
455
456 /**
457 * Does garbage collection
458 *
459 * @return void
460 * @api
461 */
462 public function collectGarbage() {
463 for ($directoryIterator = new \DirectoryIterator($this->cacheDirectory); $directoryIterator->valid(); $directoryIterator->next()) {
464 if ($directoryIterator->isDot()) {
465 continue;
466 }
467
468 if ($this->isCacheFileExpired($directoryIterator->getPathname())) {
469 $cacheEntryFileExtensionLength = strlen($this->cacheEntryFileExtension);
470 if ($cacheEntryFileExtensionLength > 0) {
471 $this->remove(substr($directoryIterator->getFilename(), 0, - $cacheEntryFileExtensionLength));
472 } else {
473 $this->remove($directoryIterator->getFilename());
474 }
475 }
476 }
477 }
478
479 /**
480 * Tries to find the cache entry for the specified identifier.
481 * Usually only one cache entry should be found - if more than one exist, this
482 * is due to some error or crash.
483 *
484 * @param string $entryIdentifier The cache entry identifier
485 * @return mixed The file names (including path) as an array if one or more entries could be found, otherwise FALSE
486 * @internal
487 */
488 protected function findCacheFilesByIdentifier($entryIdentifier) {
489 $pattern = $this->cacheDirectory . $entryIdentifier;
490 $filesFound = glob($pattern);
491 if ($filesFound === FALSE || count($filesFound) === 0) {
492 return FALSE;
493 }
494
495 return $filesFound;
496 }
497
498 /**
499 * Loads PHP code from the cache and require_onces it right away.
500 *
501 * @param string $entryIdentifier An identifier which describes the cache entry to load
502 * @return mixed Potential return value from the include operation
503 * @api
504 */
505 public function requireOnce($entryIdentifier) {
506 if ($entryIdentifier !== basename($entryIdentifier)) {
507 throw new \InvalidArgumentException(
508 'The specified entry identifier must not contain a path segment.',
509 1282073036
510 );
511 }
512
513 $pathAndFilename = $this->cacheDirectory . $entryIdentifier . $this->cacheEntryFileExtension;
514 return ($this->isCacheFileExpired($pathAndFilename)) ? FALSE : require_once($pathAndFilename);
515 }
516 }
517 ?>