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