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