[BUGFIX] Flush opcode caches while saving PHP files.
[Packages/TYPO3.CMS.git] / typo3 / sysext / core / Classes / Cache / Backend / FileBackend.php
1 <?php
2 namespace TYPO3\CMS\Core\Cache\Backend;
3
4 /***************************************************************
5 * Copyright notice
6 *
7 * (c) 2009-2013 Ingo Renner <ingo@typo3.org>
8 * All rights reserved
9 *
10 * This script is part of the TYPO3 project. The TYPO3 project is
11 * free software; you can redistribute it and/or modify
12 * it under the terms of the GNU General Public License as published by
13 * the Free Software Foundation; either version 2 of the License, or
14 * (at your option) any later version.
15 *
16 * The GNU General Public License can be found at
17 * http://www.gnu.org/copyleft/gpl.html.
18 *
19 * This script is distributed in the hope that it will be useful,
20 * but WITHOUT ANY WARRANTY; without even the implied warranty of
21 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
22 * GNU General Public License for more details.
23 *
24 * This copyright notice MUST APPEAR in all copies of the script!
25 ***************************************************************/
26 /**
27 * A caching backend which stores cache entries in files
28 *
29 * This file is a backport from FLOW3
30 *
31 * @author Robert Lemke <robert@typo3.org>
32 * @author Christian Kuhn <lolli@schwarzbu.ch>
33 * @author Karsten Dambekalns <karsten@typo3.org>
34 * @api
35 */
36 class FileBackend extends \TYPO3\CMS\Core\Cache\Backend\SimpleFileBackend implements \TYPO3\CMS\Core\Cache\Backend\PhpCapableBackendInterface, \TYPO3\CMS\Core\Cache\Backend\FreezableBackendInterface, \TYPO3\CMS\Core\Cache\Backend\TaggableBackendInterface {
37
38 const SEPARATOR = '^';
39 const EXPIRYTIME_FORMAT = 'YmdHis';
40 const EXPIRYTIME_LENGTH = 14;
41 const DATASIZE_DIGITS = 10;
42 /**
43 * A file extension to use for each cache entry.
44 *
45 * @var string
46 */
47 protected $cacheEntryFileExtension = '';
48
49 /**
50 * @var array
51 */
52 protected $cacheEntryIdentifiers = array();
53
54 /**
55 * @var boolean
56 */
57 protected $frozen = FALSE;
58
59 /**
60 * Freezes this cache backend.
61 *
62 * All data in a frozen backend remains unchanged and methods which try to add
63 * or modify data result in an exception thrown. Possible expiry times of
64 * individual cache entries are ignored.
65 *
66 * On the positive side, a frozen cache backend is much faster on read access.
67 * A frozen backend can only be thawed by calling the flush() method.
68 *
69 * @return void
70 * @throws \RuntimeException
71 */
72 public function freeze() {
73 if ($this->frozen === TRUE) {
74 throw new \RuntimeException(sprintf('The cache "%s" is already frozen.', $this->cacheIdentifier), 1323353176);
75 }
76 $cacheEntryFileExtensionLength = strlen($this->cacheEntryFileExtension);
77 for ($directoryIterator = new \DirectoryIterator($this->cacheDirectory); $directoryIterator->valid(); $directoryIterator->next()) {
78 if ($directoryIterator->isDot()) {
79 continue;
80 }
81 if ($cacheEntryFileExtensionLength > 0) {
82 $entryIdentifier = substr($directoryIterator->getFilename(), 0, -$cacheEntryFileExtensionLength);
83 } else {
84 $entryIdentifier = $directoryIterator->getFilename();
85 }
86 $this->cacheEntryIdentifiers[$entryIdentifier] = TRUE;
87 file_put_contents($this->cacheDirectory . $entryIdentifier . $this->cacheEntryFileExtension, $this->get($entryIdentifier));
88 }
89 if ($this->useIgBinary === TRUE) {
90 file_put_contents($this->cacheDirectory . 'FrozenCache.data', igbinary_serialize($this->cacheEntryIdentifiers));
91 } else {
92 file_put_contents($this->cacheDirectory . 'FrozenCache.data', serialize($this->cacheEntryIdentifiers));
93 }
94 $this->frozen = TRUE;
95 }
96
97 /**
98 * Tells if this backend is frozen.
99 *
100 * @return boolean
101 */
102 public function isFrozen() {
103 return $this->frozen;
104 }
105
106 /**
107 * Sets a reference to the cache frontend which uses this backend and
108 * initializes the default cache directory.
109 *
110 * This method also detects if this backend is frozen and sets the internal
111 * flag accordingly.
112 *
113 * TYPO3 v4 note: This method is different between TYPO3 v4 and FLOW3
114 * because the Environment class to get the path to a temporary directory
115 * does not exist in v4.
116 *
117 * @param \TYPO3\CMS\Core\Cache\Frontend\FrontendInterface $cache The cache frontend
118 * @return void
119 */
120 public function setCache(\TYPO3\CMS\Core\Cache\Frontend\FrontendInterface $cache) {
121 parent::setCache($cache);
122 if (file_exists($this->cacheDirectory . 'FrozenCache.data')) {
123 $this->frozen = TRUE;
124 if ($this->useIgBinary === TRUE) {
125 $this->cacheEntryIdentifiers = igbinary_unserialize(file_get_contents($this->cacheDirectory . 'FrozenCache.data'));
126 } else {
127 $this->cacheEntryIdentifiers = unserialize(file_get_contents($this->cacheDirectory . 'FrozenCache.data'));
128 }
129 }
130 }
131
132 /**
133 * Saves data in a cache file.
134 *
135 * @param string $entryIdentifier An identifier for this specific cache entry
136 * @param string $data The data to be stored
137 * @param array $tags Tags to associate with this cache entry
138 * @param integer $lifetime Lifetime of this cache entry in seconds. If NULL is specified, the default lifetime is used. "0" means unlimited lifetime.
139 * @return void
140 * @throws \RuntimeException
141 * @throws \TYPO3\CMS\Core\Cache\Exception\InvalidDataException 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.
142 * @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.
143 * @throws \InvalidArgumentException
144 * @api
145 */
146 public function set($entryIdentifier, $data, array $tags = array(), $lifetime = NULL) {
147 if (!is_string($data)) {
148 throw new \TYPO3\CMS\Core\Cache\Exception\InvalidDataException('The specified data is of type "' . gettype($data) . '" but a string is expected.', 1204481674);
149 }
150 if ($entryIdentifier !== basename($entryIdentifier)) {
151 throw new \InvalidArgumentException('The specified entry identifier must not contain a path segment.', 1282073032);
152 }
153 if ($entryIdentifier === '') {
154 throw new \InvalidArgumentException('The specified entry identifier must not be empty.', 1298114280);
155 }
156 if ($this->frozen === TRUE) {
157 throw new \RuntimeException(sprintf('Cannot add or modify cache entry because the backend of cache "%s" is frozen.', $this->cacheIdentifier), 1323344192);
158 }
159 $this->remove($entryIdentifier);
160 $temporaryCacheEntryPathAndFilename = $this->cacheDirectory . uniqid() . '.temp';
161 $lifetime = $lifetime === NULL ? $this->defaultLifetime : $lifetime;
162 $expiryTime = $lifetime === 0 ? 0 : $GLOBALS['EXEC_TIME'] + $lifetime;
163 $metaData = str_pad($expiryTime, self::EXPIRYTIME_LENGTH) . implode(' ', $tags) . str_pad(strlen($data), self::DATASIZE_DIGITS);
164 $result = file_put_contents($temporaryCacheEntryPathAndFilename, $data . $metaData);
165 \TYPO3\CMS\Core\Utility\GeneralUtility::fixPermissions($temporaryCacheEntryPathAndFilename);
166 if ($result === FALSE) {
167 throw new \TYPO3\CMS\Core\Cache\Exception('The temporary cache file "' . $temporaryCacheEntryPathAndFilename . '" could not be written.', 1204026251);
168 }
169 $i = 0;
170 $cacheEntryPathAndFilename = $this->cacheDirectory . $entryIdentifier . $this->cacheEntryFileExtension;
171 while (($result = rename($temporaryCacheEntryPathAndFilename, $cacheEntryPathAndFilename)) === FALSE && $i < 5) {
172 $i++;
173 }
174 if ($result === FALSE) {
175 throw new \TYPO3\CMS\Core\Cache\Exception('The cache file "' . $cacheEntryPathAndFilename . '" could not be written.', 1222361632);
176 }
177 if ($this->cacheEntryFileExtension === '.php') {
178 \TYPO3\CMS\Core\Utility\OpcodeCacheUtility::clearAllActive($cacheEntryPathAndFilename);
179 }
180 }
181
182 /**
183 * Loads data from a cache file.
184 *
185 * @param string $entryIdentifier An identifier which describes the cache entry to load
186 * @return mixed The cache entry's content as a string or FALSE if the cache entry could not be loaded
187 * @throws \InvalidArgumentException If identifier is invalid
188 * @api
189 */
190 public function get($entryIdentifier) {
191 if ($this->frozen === TRUE) {
192 return isset($this->cacheEntryIdentifiers[$entryIdentifier]) ? file_get_contents($this->cacheDirectory . $entryIdentifier . $this->cacheEntryFileExtension) : FALSE;
193 }
194 if ($entryIdentifier !== basename($entryIdentifier)) {
195 throw new \InvalidArgumentException('The specified entry identifier must not contain a path segment.', 1282073033);
196 }
197 $pathAndFilename = $this->cacheDirectory . $entryIdentifier . $this->cacheEntryFileExtension;
198 if ($this->isCacheFileExpired($pathAndFilename)) {
199 return FALSE;
200 }
201 $dataSize = (int)file_get_contents($pathAndFilename, NULL, NULL, (filesize($pathAndFilename) - self::DATASIZE_DIGITS), self::DATASIZE_DIGITS);
202 return file_get_contents($pathAndFilename, NULL, NULL, 0, $dataSize);
203 }
204
205 /**
206 * Checks if a cache entry with the specified identifier exists.
207 *
208 * @param string $entryIdentifier
209 * @return boolean TRUE if such an entry exists, FALSE if not
210 * @throws \InvalidArgumentException
211 * @api
212 */
213 public function has($entryIdentifier) {
214 if ($this->frozen === TRUE) {
215 return isset($this->cacheEntryIdentifiers[$entryIdentifier]);
216 }
217 if ($entryIdentifier !== basename($entryIdentifier)) {
218 throw new \InvalidArgumentException('The specified entry identifier must not contain a path segment.', 1282073034);
219 }
220 return !$this->isCacheFileExpired(($this->cacheDirectory . $entryIdentifier . $this->cacheEntryFileExtension));
221 }
222
223 /**
224 * Removes all cache entries matching the specified identifier.
225 * Usually this only affects one entry.
226 *
227 * @param string $entryIdentifier Specifies the cache entry to remove
228 * @return boolean TRUE if (at least) an entry could be removed or FALSE if no entry was found
229 * @throws \RuntimeException
230 * @throws \InvalidArgumentException
231 * @api
232 */
233 public function remove($entryIdentifier) {
234 if ($entryIdentifier !== basename($entryIdentifier)) {
235 throw new \InvalidArgumentException('The specified entry identifier must not contain a path segment.', 1282073035);
236 }
237 if ($entryIdentifier === '') {
238 throw new \InvalidArgumentException('The specified entry identifier must not be empty.', 1298114279);
239 }
240 if ($this->frozen === TRUE) {
241 throw new \RuntimeException(sprintf('Cannot remove cache entry because the backend of cache "%s" is frozen.', $this->cacheIdentifier), 1323344193);
242 }
243 $pathAndFilename = $this->cacheDirectory . $entryIdentifier . $this->cacheEntryFileExtension;
244 if (file_exists($pathAndFilename) === FALSE) {
245 return FALSE;
246 }
247 if (unlink($pathAndFilename) === FALSE) {
248 return FALSE;
249 }
250 return TRUE;
251 }
252
253 /**
254 * Finds and returns all cache entry identifiers which are tagged by the
255 * specified tag.
256 *
257 * @param string $searchedTag The tag to search for
258 * @return array An array with identifiers of all matching entries. An empty array if no entries matched
259 * @api
260 */
261 public function findIdentifiersByTag($searchedTag) {
262 $entryIdentifiers = array();
263 $now = $GLOBALS['EXEC_TIME'];
264 $cacheEntryFileExtensionLength = strlen($this->cacheEntryFileExtension);
265 for ($directoryIterator = \TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance('DirectoryIterator', $this->cacheDirectory); $directoryIterator->valid(); $directoryIterator->next()) {
266 if ($directoryIterator->isDot()) {
267 continue;
268 }
269 $cacheEntryPathAndFilename = $directoryIterator->getPathname();
270 $index = (int)file_get_contents($cacheEntryPathAndFilename, NULL, NULL, (filesize($cacheEntryPathAndFilename) - self::DATASIZE_DIGITS), self::DATASIZE_DIGITS);
271 $metaData = file_get_contents($cacheEntryPathAndFilename, NULL, NULL, $index);
272 $expiryTime = (int)substr($metaData, 0, self::EXPIRYTIME_LENGTH);
273 if ($expiryTime !== 0 && $expiryTime < $now) {
274 continue;
275 }
276 if (in_array($searchedTag, explode(' ', substr($metaData, self::EXPIRYTIME_LENGTH, -self::DATASIZE_DIGITS)))) {
277 if ($cacheEntryFileExtensionLength > 0) {
278 $entryIdentifiers[] = substr($directoryIterator->getFilename(), 0, -$cacheEntryFileExtensionLength);
279 } else {
280 $entryIdentifiers[] = $directoryIterator->getFilename();
281 }
282 }
283 }
284 return $entryIdentifiers;
285 }
286
287 /**
288 * Removes all cache entries of this cache and sets the frozen flag to FALSE.
289 *
290 * @return void
291 * @api
292 */
293 public function flush() {
294 parent::flush();
295 if ($this->frozen === TRUE) {
296 $this->frozen = FALSE;
297 }
298 }
299
300 /**
301 * Removes all cache entries of this cache which are tagged by the specified tag.
302 *
303 * @param string $tag The tag the entries must have
304 * @return void
305 * @api
306 */
307 public function flushByTag($tag) {
308 $identifiers = $this->findIdentifiersByTag($tag);
309 if (count($identifiers) === 0) {
310 return;
311 }
312 foreach ($identifiers as $entryIdentifier) {
313 $this->remove($entryIdentifier);
314 }
315 }
316
317 /**
318 * Checks if the given cache entry files are still valid or if their
319 * lifetime has exceeded.
320 *
321 * @param string $cacheEntryPathAndFilename
322 * @return boolean
323 * @api
324 */
325 protected function isCacheFileExpired($cacheEntryPathAndFilename) {
326 if (file_exists($cacheEntryPathAndFilename) === FALSE) {
327 return TRUE;
328 }
329 $index = (int)file_get_contents($cacheEntryPathAndFilename, NULL, NULL, (filesize($cacheEntryPathAndFilename) - self::DATASIZE_DIGITS), self::DATASIZE_DIGITS);
330 $expiryTime = (int)file_get_contents($cacheEntryPathAndFilename, NULL, NULL, $index, self::EXPIRYTIME_LENGTH);
331 return $expiryTime !== 0 && $expiryTime < $GLOBALS['EXEC_TIME'];
332 }
333
334 /**
335 * Does garbage collection
336 *
337 * @return void
338 * @api
339 */
340 public function collectGarbage() {
341 if ($this->frozen === TRUE) {
342 return;
343 }
344 for ($directoryIterator = new \DirectoryIterator($this->cacheDirectory); $directoryIterator->valid(); $directoryIterator->next()) {
345 if ($directoryIterator->isDot()) {
346 continue;
347 }
348 if ($this->isCacheFileExpired($directoryIterator->getPathname())) {
349 $cacheEntryFileExtensionLength = strlen($this->cacheEntryFileExtension);
350 if ($cacheEntryFileExtensionLength > 0) {
351 $this->remove(substr($directoryIterator->getFilename(), 0, -$cacheEntryFileExtensionLength));
352 } else {
353 $this->remove($directoryIterator->getFilename());
354 }
355 }
356 }
357 }
358
359 /**
360 * Tries to find the cache entry for the specified identifier.
361 * Usually only one cache entry should be found - if more than one exist, this
362 * is due to some error or crash.
363 *
364 * @param string $entryIdentifier The cache entry identifier
365 * @return mixed The filenames (including path) as an array if one or more entries could be found, otherwise FALSE
366 */
367 protected function findCacheFilesByIdentifier($entryIdentifier) {
368 $pattern = $this->cacheDirectory . $entryIdentifier;
369 $filesFound = glob($pattern);
370 if ($filesFound === FALSE || count($filesFound) === 0) {
371 return FALSE;
372 }
373 return $filesFound;
374 }
375
376 /**
377 * Loads PHP code from the cache and require_onces it right away.
378 *
379 * @param string $entryIdentifier An identifier which describes the cache entry to load
380 * @throws \InvalidArgumentException
381 * @return mixed Potential return value from the include operation
382 * @api
383 */
384 public function requireOnce($entryIdentifier) {
385 if ($this->frozen === TRUE) {
386 if (isset($this->cacheEntryIdentifiers[$entryIdentifier])) {
387 return require_once $this->cacheDirectory . $entryIdentifier . $this->cacheEntryFileExtension;
388 } else {
389 return FALSE;
390 }
391 } else {
392 $pathAndFilename = $this->cacheDirectory . $entryIdentifier . $this->cacheEntryFileExtension;
393 if ($entryIdentifier !== basename($entryIdentifier)) {
394 throw new \InvalidArgumentException('The specified entry identifier must not contain a path segment.', 1282073036);
395 }
396 $pathAndFilename = $this->cacheDirectory . $entryIdentifier . $this->cacheEntryFileExtension;
397 return $this->isCacheFileExpired($pathAndFilename) ? FALSE : require_once $pathAndFilename;
398 }
399 }
400
401 }