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