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