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