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