Completed
Push — master ( acff9d...ad01e4 )
by
unknown
38:21 queued 23:29
created

SimpleFileBackend::setCache()   A

Complexity

Conditions 6
Paths 32

Size

Total Lines 20
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 6
eloc 14
nc 32
nop 1
dl 0
loc 20
rs 9.2222
c 0
b 0
f 0
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
class SimpleFileBackend extends AbstractBackend implements PhpCapableBackendInterface
32
{
33
    const SEPARATOR = '^';
34
    const EXPIRYTIME_FORMAT = 'YmdHis';
35
    const EXPIRYTIME_LENGTH = 14;
36
    const DATASIZE_DIGITS = 10;
37
    /**
38
     * Directory where the files are stored
39
     *
40
     * @var string
41
     */
42
    protected $cacheDirectory = '';
43
44
    /**
45
     * Temporary path to cache directory before setCache() was called. It is
46
     * set by setCacheDirectory() and used in setCache() method which calls
47
     * the directory creation if needed. The variable is not used afterwards,
48
     * the final cache directory path is stored in $this->cacheDirectory then.
49
     *
50
     * @var string Temporary path to cache directory
51
     */
52
    protected $temporaryCacheDirectory = '';
53
54
    /**
55
     * A file extension to use for each cache entry.
56
     *
57
     * @var string
58
     */
59
    protected $cacheEntryFileExtension = '';
60
61
    /**
62
     * @var array
63
     */
64
    protected $cacheEntryIdentifiers = [];
65
66
    /**
67
     * @var bool
68
     */
69
    protected $frozen = false;
70
71
    /**
72
     * Sets a reference to the cache frontend which uses this backend and
73
     * initializes the default cache directory.
74
     *
75
     * @param FrontendInterface $cache The cache frontend
76
     * @throws Exception
77
     */
78
    public function setCache(FrontendInterface $cache)
79
    {
80
        parent::setCache($cache);
81
        if (empty($this->temporaryCacheDirectory)) {
82
            // If no cache directory was given with cacheDirectory
83
            // configuration option, set it to a path below var/ folder
84
            $temporaryCacheDirectory = Environment::getVarPath() . '/';
85
        } else {
86
            $temporaryCacheDirectory = $this->temporaryCacheDirectory;
87
        }
88
        $codeOrData = $cache instanceof PhpFrontend ? 'code' : 'data';
89
        $finalCacheDirectory = $temporaryCacheDirectory . 'cache/' . $codeOrData . '/' . $this->cacheIdentifier . '/';
90
        if (!is_dir($finalCacheDirectory)) {
91
            $this->createFinalCacheDirectory($finalCacheDirectory);
92
        }
93
        unset($this->temporaryCacheDirectory);
94
        $this->cacheDirectory = $finalCacheDirectory;
95
        $this->cacheEntryFileExtension = $cache instanceof PhpFrontend ? '.php' : '';
96
        if (strlen($this->cacheDirectory) + 23 > PHP_MAXPATHLEN) {
97
            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);
98
        }
99
    }
100
101
    /**
102
     * Sets the directory where the cache files are stored. By default it is
103
     * assumed that the directory is below TYPO3's Project Path. However, an
104
     * absolute path can be selected, too.
105
     *
106
     * This method enables to use a cache path outside of TYPO3's Project Path. The final
107
     * cache path is checked and created in createFinalCacheDirectory(),
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
     * @throws Exception if the directory is not within allowed
113
     */
114
    public function setCacheDirectory($cacheDirectory)
115
    {
116
        // Skip handling if directory is a stream resource
117
        // This is used by unit tests with vfs:// directories
118
        if (strpos($cacheDirectory, '://')) {
119
            $this->temporaryCacheDirectory = $cacheDirectory;
120
            return;
121
        }
122
        $documentRoot = Environment::getProjectPath() . '/';
123
        if ($open_basedir = ini_get('open_basedir')) {
124
            if (Environment::isWindows()) {
125
                $delimiter = ';';
126
                $cacheDirectory = str_replace('\\', '/', $cacheDirectory);
127
                if (!preg_match('/[A-Z]:/', substr($cacheDirectory, 0, 2))) {
128
                    $cacheDirectory = Environment::getProjectPath() . $cacheDirectory;
129
                }
130
            } else {
131
                $delimiter = ':';
132
                if ($cacheDirectory[0] !== '/') {
133
                    // relative path to cache directory.
134
                    $cacheDirectory = Environment::getProjectPath() . $cacheDirectory;
135
                }
136
            }
137
            $basedirs = explode($delimiter, $open_basedir);
138
            $cacheDirectoryInBaseDir = false;
139
            foreach ($basedirs as $basedir) {
140
                if (Environment::isWindows()) {
141
                    $basedir = str_replace('\\', '/', $basedir);
142
                }
143
                if ($basedir[strlen($basedir) - 1] !== '/') {
144
                    $basedir .= '/';
145
                }
146
                if (GeneralUtility::isFirstPartOfStr($cacheDirectory, $basedir)) {
147
                    $documentRoot = $basedir;
148
                    $cacheDirectory = str_replace($basedir, '', $cacheDirectory);
149
                    $cacheDirectoryInBaseDir = true;
150
                    break;
151
                }
152
            }
153
            if (!$cacheDirectoryInBaseDir) {
154
                throw new Exception(
155
                    'Open_basedir restriction in effect. The directory "' . $cacheDirectory . '" is not in an allowed path.',
156
                    1476045417
157
                );
158
            }
159
        } else {
160
            if ($cacheDirectory[0] === '/') {
161
                // Absolute path to cache directory.
162
                $documentRoot = '';
163
            }
164
            if (Environment::isWindows()) {
165
                if (!empty($documentRoot) && strpos($cacheDirectory, $documentRoot) === 0) {
166
                    $documentRoot = '';
167
                }
168
            }
169
        }
170
        // After this point all paths have '/' as directory separator
171
        if ($cacheDirectory[strlen($cacheDirectory) - 1] !== '/') {
172
            $cacheDirectory .= '/';
173
        }
174
        $this->temporaryCacheDirectory = $documentRoot . $cacheDirectory;
175
    }
176
177
    /**
178
     * Create the final cache directory if it does not exist.
179
     *
180
     * @param string $finalCacheDirectory Absolute path to final cache directory
181
     * @throws Exception If directory is not writable after creation
182
     */
183
    protected function createFinalCacheDirectory($finalCacheDirectory)
184
    {
185
        try {
186
            GeneralUtility::mkdir_deep($finalCacheDirectory);
187
        } catch (\RuntimeException $e) {
188
            throw new Exception('The directory "' . $finalCacheDirectory . '" can not be created.', 1303669848, $e);
189
        }
190
        if (!is_writable($finalCacheDirectory)) {
191
            throw new Exception('The directory "' . $finalCacheDirectory . '" is not writable.', 1203965200);
192
        }
193
    }
194
195
    /**
196
     * Returns the directory where the cache files are stored
197
     *
198
     * @return string Full path of the cache directory
199
     */
200
    public function getCacheDirectory()
201
    {
202
        return $this->cacheDirectory;
203
    }
204
205
    /**
206
     * Saves data in a cache file.
207
     *
208
     * @param string $entryIdentifier An identifier for this specific cache entry
209
     * @param string $data The data to be stored
210
     * @param array $tags Tags to associate with this cache entry
211
     * @param int $lifetime This cache backend does not support life times
212
     * @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.
213
     * @throws InvalidDataException if the data to bes stored is not a string.
214
     * @throws \InvalidArgumentException
215
     */
216
    public function set($entryIdentifier, $data, array $tags = [], $lifetime = null)
217
    {
218
        if (!is_string($data)) {
0 ignored issues
show
introduced by
The condition is_string($data) is always true.
Loading history...
219
            throw new 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
        GeneralUtility::fixPermissions($temporaryCacheEntryPathAndFilename);
230
        if ($result === false) {
231
            throw new 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
     */
247
    public function get($entryIdentifier)
248
    {
249
        if ($entryIdentifier !== PathUtility::basename($entryIdentifier)) {
250
            throw new \InvalidArgumentException('The specified entry identifier must not contain a path segment.', 1334756877);
251
        }
252
        $pathAndFilename = $this->cacheDirectory . $entryIdentifier . $this->cacheEntryFileExtension;
253
        if (!file_exists($pathAndFilename)) {
254
            return false;
255
        }
256
        return file_get_contents($pathAndFilename);
257
    }
258
259
    /**
260
     * Checks if a cache entry with the specified identifier exists.
261
     *
262
     * @param string $entryIdentifier
263
     * @return bool TRUE if such an entry exists, FALSE if not
264
     * @throws \InvalidArgumentException
265
     */
266
    public function has($entryIdentifier)
267
    {
268
        if ($entryIdentifier !== PathUtility::basename($entryIdentifier)) {
269
            throw new \InvalidArgumentException('The specified entry identifier must not contain a path segment.', 1334756878);
270
        }
271
        return file_exists($this->cacheDirectory . $entryIdentifier . $this->cacheEntryFileExtension);
272
    }
273
274
    /**
275
     * Removes all cache entries matching the specified identifier.
276
     * Usually this only affects one entry.
277
     *
278
     * @param string $entryIdentifier Specifies the cache entry to remove
279
     * @return bool TRUE if (at least) an entry could be removed or FALSE if no entry was found
280
     * @throws \InvalidArgumentException
281
     */
282
    public function remove($entryIdentifier)
283
    {
284
        if ($entryIdentifier !== PathUtility::basename($entryIdentifier)) {
285
            throw new \InvalidArgumentException('The specified entry identifier must not contain a path segment.', 1334756960);
286
        }
287
        if ($entryIdentifier === '') {
288
            throw new \InvalidArgumentException('The specified entry identifier must not be empty.', 1334756961);
289
        }
290
        try {
291
            unlink($this->cacheDirectory . $entryIdentifier . $this->cacheEntryFileExtension);
292
        } catch (\Exception $e) {
293
            return false;
294
        }
295
        return true;
296
    }
297
298
    /**
299
     * Removes all cache entries of this cache.
300
     * Flushes a directory by first moving to a temporary resource, and then
301
     * triggering the remove process. This way directories can be flushed faster
302
     * to prevent race conditions on concurrent processes accessing the same directory.
303
     */
304
    public function flush()
305
    {
306
        $directory = $this->cacheDirectory;
307
        if (is_link($directory)) {
308
            // Avoid attempting to rename the symlink see #87367
309
            $directory = realpath($directory);
310
        }
311
312
        if (is_dir($directory)) {
313
            $temporaryDirectory = rtrim($directory, '/') . '.' . StringUtility::getUniqueId('remove');
314
            if (rename($directory, $temporaryDirectory)) {
315
                GeneralUtility::mkdir($directory);
316
                clearstatcache();
317
                GeneralUtility::rmdir($temporaryDirectory, true);
318
            }
319
        }
320
    }
321
322
    /**
323
     * Checks if the given cache entry files are still valid or if their
324
     * lifetime has exceeded.
325
     *
326
     * @param string $cacheEntryPathAndFilename
327
     * @return bool
328
     */
329
    protected function isCacheFileExpired($cacheEntryPathAndFilename)
330
    {
331
        return file_exists($cacheEntryPathAndFilename) === false;
332
    }
333
334
    /**
335
     * Not necessary
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
     */
360
    public function requireOnce($entryIdentifier)
361
    {
362
        $pathAndFilename = $this->cacheDirectory . $entryIdentifier . $this->cacheEntryFileExtension;
363
        if ($entryIdentifier !== PathUtility::basename($entryIdentifier)) {
364
            throw new \InvalidArgumentException('The specified entry identifier must not contain a path segment.', 1282073037);
365
        }
366
        return file_exists($pathAndFilename) ? require_once $pathAndFilename : false;
367
    }
368
369
    /**
370
     * Loads PHP code from the cache and require it right away.
371
     *
372
     * @param string $entryIdentifier An identifier which describes the cache entry to load
373
     * @return mixed Potential return value from the include operation
374
     * @throws \InvalidArgumentException
375
     */
376
    public function require(string $entryIdentifier)
377
    {
378
        $pathAndFilename = $this->cacheDirectory . $entryIdentifier . $this->cacheEntryFileExtension;
379
        if ($entryIdentifier !== PathUtility::basename($entryIdentifier)) {
380
            throw new \InvalidArgumentException('The specified entry identifier must not contain a path segment.', 1532528267);
381
        }
382
        return file_exists($pathAndFilename) ? require $pathAndFilename : false;
383
    }
384
}
385