[DOC] Clarify phpdoc of SimpleFileBackend that lifetime is unsupported
[Packages/TYPO3.CMS.git] / typo3 / sysext / core / Classes / Cache / Backend / SimpleFileBackend.php
1 <?php
2 namespace TYPO3\CMS\Core\Cache\Backend;
3
4 /* *
5 * This script belongs to the FLOW3 framework. *
6 * *
7 * It is free software; you can redistribute it and/or modify it under *
8 * the terms of the GNU Lesser General Public License, either version 3 *
9 * of the License, or (at your option) any later version. *
10 * *
11 * The TYPO3 project - inspiring people to share! *
12 * */
13
14 use TYPO3\CMS\Core\Service\OpcodeCacheService;
15 use TYPO3\CMS\Core\Utility\GeneralUtility;
16 use TYPO3\CMS\Core\Utility\StringUtility;
17
18 /**
19 * A caching backend which stores cache entries in files, but does not support or
20 * care about expiry times and tags.
21 *
22 * @api
23 */
24 class SimpleFileBackend extends \TYPO3\CMS\Core\Cache\Backend\AbstractBackend implements \TYPO3\CMS\Core\Cache\Backend\PhpCapableBackendInterface
25 {
26 const SEPARATOR = '^';
27 const EXPIRYTIME_FORMAT = 'YmdHis';
28 const EXPIRYTIME_LENGTH = 14;
29 const DATASIZE_DIGITS = 10;
30 /**
31 * Directory where the files are stored
32 *
33 * @var string
34 */
35 protected $cacheDirectory = '';
36
37 /**
38 * TYPO3 v4 note: This variable is only available in v5
39 * Temporary path to cache directory before setCache() was called. It is
40 * set by setCacheDirectory() and used in setCache() method which calls
41 * the directory creation if needed. The variable is not used afterwards,
42 * the final cache directory path is stored in $this->cacheDirectory then.
43 *
44 * @var string Temporary path to cache directory
45 */
46 protected $temporaryCacheDirectory = '';
47
48 /**
49 * A file extension to use for each cache entry.
50 *
51 * @var string
52 */
53 protected $cacheEntryFileExtension = '';
54
55 /**
56 * @var array
57 */
58 protected $cacheEntryIdentifiers = [];
59
60 /**
61 * @var bool
62 */
63 protected $frozen = false;
64
65 /**
66 * If the extension "igbinary" is installed, use it for increased performance.
67 * Caching the result of extension_loaded() here is faster than calling extension_loaded() multiple times.
68 *
69 * @var bool
70 */
71 protected $useIgBinary = false;
72
73 /**
74 * Initializes this cache frontend
75 *
76 * @return void
77 */
78 public function initializeObject()
79 {
80 $this->useIgBinary = extension_loaded('igbinary');
81 }
82
83 /**
84 * Sets a reference to the cache frontend which uses this backend and
85 * initializes the default cache directory.
86 *
87 * TYPO3 v4 note: This method is different between TYPO3 v4 and FLOW3
88 * because the Environment class to get the path to a temporary directory
89 * does not exist in v4.
90 *
91 * @param \TYPO3\CMS\Core\Cache\Frontend\FrontendInterface $cache The cache frontend
92 * @throws \TYPO3\CMS\Core\Cache\Exception
93 * @return void
94 */
95 public function setCache(\TYPO3\CMS\Core\Cache\Frontend\FrontendInterface $cache)
96 {
97 parent::setCache($cache);
98 if (empty($this->temporaryCacheDirectory)) {
99 // If no cache directory was given with cacheDirectory
100 // configuration option, set it to a path below typo3temp/
101 $temporaryCacheDirectory = PATH_site . 'typo3temp/';
102 } else {
103 $temporaryCacheDirectory = $this->temporaryCacheDirectory;
104 }
105 $codeOrData = $cache instanceof \TYPO3\CMS\Core\Cache\Frontend\PhpFrontend ? 'Code' : 'Data';
106 $finalCacheDirectory = $temporaryCacheDirectory . 'Cache/' . $codeOrData . '/' . $this->cacheIdentifier . '/';
107 if (!is_dir($finalCacheDirectory)) {
108 $this->createFinalCacheDirectory($finalCacheDirectory);
109 }
110 unset($this->temporaryCacheDirectory);
111 $this->cacheDirectory = $finalCacheDirectory;
112 $this->cacheEntryFileExtension = $cache instanceof \TYPO3\CMS\Core\Cache\Frontend\PhpFrontend ? '.php' : '';
113 if (strlen($this->cacheDirectory) + 23 > \TYPO3\CMS\Core\Utility\GeneralUtility::getMaximumPathLength()) {
114 throw new \TYPO3\CMS\Core\Cache\Exception('The length of the temporary cache file path "' . $this->cacheDirectory . '" exceeds the ' . 'maximum path length of ' . (\TYPO3\CMS\Core\Utility\GeneralUtility::getMaximumPathLength() - 23) . '. Please consider ' . 'setting the temporaryDirectoryBase option to a shorter path.', 1248710426);
115 }
116 }
117
118 /**
119 * Sets the directory where the cache files are stored. By default it is
120 * assumed that the directory is below the TYPO3_DOCUMENT_ROOT. However, an
121 * absolute path can be selected, too.
122 *
123 * This method does not exist in FLOW3 anymore, but it is needed in
124 * TYPO3 v4 to enable a cache path outside of document root. The final
125 * cache path is checked and created in createFinalCachDirectory(),
126 * called by setCache() method, which is done _after_ the cacheDirectory
127 * option was handled.
128 *
129 * @param string $cacheDirectory The cache base directory. If a relative path
130 * @return void
131 * @throws \TYPO3\CMS\Core\Cache\Exception if the directory is not within allowed
132 */
133 public function setCacheDirectory($cacheDirectory)
134 {
135 // Skip handling if directory is a stream ressource
136 // This is used by unit tests with vfs:// directoryies
137 if (strpos($cacheDirectory, '://')) {
138 $this->temporaryCacheDirectory = $cacheDirectory;
139 return;
140 }
141 $documentRoot = PATH_site;
142 if ($open_basedir = ini_get('open_basedir')) {
143 if (TYPO3_OS === 'WIN') {
144 $delimiter = ';';
145 $cacheDirectory = str_replace('\\', '/', $cacheDirectory);
146 if (!preg_match('/[A-Z]:/', substr($cacheDirectory, 0, 2))) {
147 $cacheDirectory = PATH_site . $cacheDirectory;
148 }
149 } else {
150 $delimiter = ':';
151 if ($cacheDirectory[0] != '/') {
152 // relative path to cache directory.
153 $cacheDirectory = PATH_site . $cacheDirectory;
154 }
155 }
156 $basedirs = explode($delimiter, $open_basedir);
157 $cacheDirectoryInBaseDir = false;
158 foreach ($basedirs as $basedir) {
159 if (TYPO3_OS === 'WIN') {
160 $basedir = str_replace('\\', '/', $basedir);
161 }
162 if ($basedir[strlen($basedir) - 1] !== '/') {
163 $basedir .= '/';
164 }
165 if (\TYPO3\CMS\Core\Utility\GeneralUtility::isFirstPartOfStr($cacheDirectory, $basedir)) {
166 $documentRoot = $basedir;
167 $cacheDirectory = str_replace($basedir, '', $cacheDirectory);
168 $cacheDirectoryInBaseDir = true;
169 break;
170 }
171 }
172 if (!$cacheDirectoryInBaseDir) {
173 throw new \TYPO3\CMS\Core\Cache\Exception('Open_basedir restriction in effect. The directory "' . $cacheDirectory . '" is not in an allowed path.');
174 }
175 } else {
176 if ($cacheDirectory[0] == '/') {
177 // Absolute path to cache directory.
178 $documentRoot = '';
179 }
180 if (TYPO3_OS === 'WIN') {
181 if (substr($cacheDirectory, 0, strlen($documentRoot)) === $documentRoot) {
182 $documentRoot = '';
183 }
184 }
185 }
186 // After this point all paths have '/' as directory separator
187 if ($cacheDirectory[strlen($cacheDirectory) - 1] !== '/') {
188 $cacheDirectory .= '/';
189 }
190 $this->temporaryCacheDirectory = $documentRoot . $cacheDirectory . $this->cacheIdentifier . '/';
191 }
192
193 /**
194 * Create the final cache directory if it does not exist. This method
195 * exists in TYPO3 v4 only.
196 *
197 * @param string $finalCacheDirectory Absolute path to final cache directory
198 * @return void
199 * @throws \TYPO3\CMS\Core\Cache\Exception If directory is not writable after creation
200 */
201 protected function createFinalCacheDirectory($finalCacheDirectory)
202 {
203 try {
204 \TYPO3\CMS\Core\Utility\GeneralUtility::mkdir_deep($finalCacheDirectory);
205 } catch (\RuntimeException $e) {
206 throw new \TYPO3\CMS\Core\Cache\Exception('The directory "' . $finalCacheDirectory . '" can not be created.', 1303669848, $e);
207 }
208 if (!is_writable($finalCacheDirectory)) {
209 throw new \TYPO3\CMS\Core\Cache\Exception('The directory "' . $finalCacheDirectory . '" is not writable.', 1203965200);
210 }
211 }
212
213 /**
214 * Returns the directory where the cache files are stored
215 *
216 * @return string Full path of the cache directory
217 * @api
218 */
219 public function getCacheDirectory()
220 {
221 return $this->cacheDirectory;
222 }
223
224 /**
225 * Saves data in a cache file.
226 *
227 * @param string $entryIdentifier An identifier for this specific cache entry
228 * @param string $data The data to be stored
229 * @param array $tags Tags to associate with this cache entry
230 * @param int $lifetime This cache backend does not support life times
231 * @return void
232 * @throws \TYPO3\CMS\Core\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.
233 * @throws \TYPO3\CMS\Core\Cache\Exception\InvalidDataException if the data to bes stored is not a string.
234 * @throws \InvalidArgumentException
235 * @api
236 */
237 public function set($entryIdentifier, $data, array $tags = [], $lifetime = null)
238 {
239 if (!is_string($data)) {
240 throw new \TYPO3\CMS\Core\Cache\Exception\InvalidDataException('The specified data is of type "' . gettype($data) . '" but a string is expected.', 1334756734);
241 }
242 if ($entryIdentifier !== basename($entryIdentifier)) {
243 throw new \InvalidArgumentException('The specified entry identifier must not contain a path segment.', 1334756735);
244 }
245 if ($entryIdentifier === '') {
246 throw new \InvalidArgumentException('The specified entry identifier must not be empty.', 1334756736);
247 }
248 $temporaryCacheEntryPathAndFilename = $this->cacheDirectory . StringUtility::getUniqueId() . '.temp';
249 $result = file_put_contents($temporaryCacheEntryPathAndFilename, $data);
250 \TYPO3\CMS\Core\Utility\GeneralUtility::fixPermissions($temporaryCacheEntryPathAndFilename);
251 if ($result === false) {
252 throw new \TYPO3\CMS\Core\Cache\Exception('The temporary cache file "' . $temporaryCacheEntryPathAndFilename . '" could not be written.', 1334756737);
253 }
254 $cacheEntryPathAndFilename = $this->cacheDirectory . $entryIdentifier . $this->cacheEntryFileExtension;
255 rename($temporaryCacheEntryPathAndFilename, $cacheEntryPathAndFilename);
256 if ($this->cacheEntryFileExtension === '.php') {
257 GeneralUtility::makeInstance(OpcodeCacheService::class)->clearAllActive($cacheEntryPathAndFilename);
258 }
259 }
260
261 /**
262 * Loads data from a cache file.
263 *
264 * @param string $entryIdentifier An identifier which describes the cache entry to load
265 * @return mixed The cache entry's content as a string or FALSE if the cache entry could not be loaded
266 * @throws \InvalidArgumentException If identifier is invalid
267 * @api
268 */
269 public function get($entryIdentifier)
270 {
271 if ($entryIdentifier !== basename($entryIdentifier)) {
272 throw new \InvalidArgumentException('The specified entry identifier must not contain a path segment.', 1334756877);
273 }
274 $pathAndFilename = $this->cacheDirectory . $entryIdentifier . $this->cacheEntryFileExtension;
275 if (!file_exists($pathAndFilename)) {
276 return false;
277 }
278 return file_get_contents($pathAndFilename);
279 }
280
281 /**
282 * Checks if a cache entry with the specified identifier exists.
283 *
284 * @param string $entryIdentifier
285 * @return bool TRUE if such an entry exists, FALSE if not
286 * @throws \InvalidArgumentException
287 * @api
288 */
289 public function has($entryIdentifier)
290 {
291 if ($entryIdentifier !== basename($entryIdentifier)) {
292 throw new \InvalidArgumentException('The specified entry identifier must not contain a path segment.', 1334756878);
293 }
294 return file_exists($this->cacheDirectory . $entryIdentifier . $this->cacheEntryFileExtension);
295 }
296
297 /**
298 * Removes all cache entries matching the specified identifier.
299 * Usually this only affects one entry.
300 *
301 * @param string $entryIdentifier Specifies the cache entry to remove
302 * @return bool TRUE if (at least) an entry could be removed or FALSE if no entry was found
303 * @throws \InvalidArgumentException
304 * @api
305 */
306 public function remove($entryIdentifier)
307 {
308 if ($entryIdentifier !== basename($entryIdentifier)) {
309 throw new \InvalidArgumentException('The specified entry identifier must not contain a path segment.', 1334756960);
310 }
311 if ($entryIdentifier === '') {
312 throw new \InvalidArgumentException('The specified entry identifier must not be empty.', 1334756961);
313 }
314 try {
315 unlink($this->cacheDirectory . $entryIdentifier . $this->cacheEntryFileExtension);
316 } catch (\Exception $e) {
317 return false;
318 }
319 return true;
320 }
321
322 /**
323 * Removes all cache entries of this cache.
324 *
325 * @return void
326 * @api
327 */
328 public function flush()
329 {
330 \TYPO3\CMS\Core\Utility\GeneralUtility::flushDirectory($this->cacheDirectory, true);
331 }
332
333 /**
334 * Checks if the given cache entry files are still valid or if their
335 * lifetime has exceeded.
336 *
337 * @param string $cacheEntryPathAndFilename
338 * @return bool
339 * @api
340 */
341 protected function isCacheFileExpired($cacheEntryPathAndFilename)
342 {
343 return file_exists($cacheEntryPathAndFilename) === false;
344 }
345
346 /**
347 * Not necessary
348 *
349 * @return void
350 * @api
351 */
352 public function collectGarbage()
353 {
354 }
355
356 /**
357 * Tries to find the cache entry for the specified identifier.
358 *
359 * @param string $entryIdentifier The cache entry identifier
360 * @return mixed The file names (including path) as an array if one or more entries could be found, otherwise FALSE
361 */
362 protected function findCacheFilesByIdentifier($entryIdentifier)
363 {
364 $pathAndFilename = $this->cacheDirectory . $entryIdentifier . $this->cacheEntryFileExtension;
365 return file_exists($pathAndFilename) ? [$pathAndFilename] : false;
366 }
367
368 /**
369 * Loads PHP code from the cache and require_onces it right away.
370 *
371 * @param string $entryIdentifier An identifier which describes the cache entry to load
372 * @return mixed Potential return value from the include operation
373 * @throws \InvalidArgumentException
374 * @api
375 */
376 public function requireOnce($entryIdentifier)
377 {
378 $pathAndFilename = $this->cacheDirectory . $entryIdentifier . $this->cacheEntryFileExtension;
379 if ($entryIdentifier !== basename($entryIdentifier)) {
380 throw new \InvalidArgumentException('The specified entry identifier must not contain a path segment.', 1282073037);
381 }
382 return file_exists($pathAndFilename) ? require_once $pathAndFilename : false;
383 }
384 }