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