664f688ef6a341a69a6cde287dbef427e6e302c8
[Packages/TYPO3.CMS.git] / typo3 / sysext / core / Classes / Cache / Backend / Typo3DatabaseBackend.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 /**
18 * A caching backend which stores cache entries in database tables
19 *
20 * @author Christian Kuhn <lolli@schwarzbu.ch>
21 * @author Ingo Renner <ingo@typo3.org>
22 * @api
23 */
24 class Typo3DatabaseBackend extends \TYPO3\CMS\Core\Cache\Backend\AbstractBackend implements \TYPO3\CMS\Core\Cache\Backend\TaggableBackendInterface {
25
26 /**
27 * @var int Timestamp of 2038-01-01)
28 */
29 const FAKED_UNLIMITED_EXPIRE = 2145909600;
30 /**
31 * @var string Name of the cache data table
32 */
33 protected $cacheTable;
34
35 /**
36 * @var string Name of the cache tags table
37 */
38 protected $tagsTable;
39
40 /**
41 * @var bool Indicates whether data is compressed or not (requires php zlib)
42 */
43 protected $compression = FALSE;
44
45 /**
46 * @var int -1 to 9, indicates zlib compression level: -1 = default level 6, 0 = no compression, 9 maximum compression
47 */
48 protected $compressionLevel = -1;
49
50 /**
51 * @var string Name of the identifier field, 'table_name.identifier'
52 */
53 protected $identifierField;
54
55 /**
56 * @var string Name of the expire field, 'table_name.expires'
57 */
58 protected $expiresField;
59
60 /**
61 * @var int Maximum lifetime to stay with expire field below FAKED_UNLIMITED_LIFETIME
62 */
63 protected $maximumLifetime;
64
65 /**
66 * @var string SQL where for a not expired entry
67 */
68 protected $notExpiredStatement;
69
70 /**
71 * @var string Opposite of notExpiredStatement
72 */
73 protected $expiredStatement;
74
75 /**
76 * @var string Data and tags table name comma separated
77 */
78 protected $tableList;
79
80 /**
81 * @var string Join condition for data and tags table
82 */
83 protected $tableJoin;
84
85 /**
86 * Set cache frontend instance and calculate data and tags table name
87 *
88 * @param \TYPO3\CMS\Core\Cache\Frontend\FrontendInterface $cache The frontend for this backend
89 * @return void
90 * @api
91 */
92 public function setCache(\TYPO3\CMS\Core\Cache\Frontend\FrontendInterface $cache) {
93 parent::setCache($cache);
94 $this->cacheTable = 'cf_' . $this->cacheIdentifier;
95 $this->tagsTable = 'cf_' . $this->cacheIdentifier . '_tags';
96 $this->initializeCommonReferences();
97 }
98
99 /**
100 * Initializes common references used in this backend.
101 *
102 * @return void
103 */
104 protected function initializeCommonReferences() {
105 $this->identifierField = $this->cacheTable . '.identifier';
106 $this->expiresField = $this->cacheTable . '.expires';
107 $this->maximumLifetime = self::FAKED_UNLIMITED_EXPIRE - $GLOBALS['EXEC_TIME'];
108 $this->tableList = $this->cacheTable . ', ' . $this->tagsTable;
109 $this->tableJoin = $this->identifierField . ' = ' . $this->tagsTable . '.identifier';
110 $this->expiredStatement = $this->expiresField . ' < ' . $GLOBALS['EXEC_TIME'];
111 $this->notExpiredStatement = $this->expiresField . ' >= ' . $GLOBALS['EXEC_TIME'];
112 }
113
114 /**
115 * Saves data in a cache file.
116 *
117 * @param string $entryIdentifier An identifier for this specific cache entry
118 * @param string $data The data to be stored
119 * @param array $tags Tags to associate with this cache entry
120 * @param int $lifetime Lifetime of this cache entry in seconds. If NULL is specified, the default lifetime is used. "0" means unlimited liftime.
121 * @return void
122 * @throws \TYPO3\CMS\Core\Cache\Exception if no cache frontend has been set.
123 * @throws \TYPO3\CMS\Core\Cache\Exception\InvalidDataException if the data to be stored is not a string.
124 */
125 public function set($entryIdentifier, $data, array $tags = array(), $lifetime = NULL) {
126 $this->throwExceptionIfFrontendDoesNotExist();
127 if (!is_string($data)) {
128 throw new \TYPO3\CMS\Core\Cache\Exception\InvalidDataException(
129 'The specified data is of type "' . gettype($data) . '" but a string is expected.',
130 1236518298
131 );
132 }
133 if (is_null($lifetime)) {
134 $lifetime = $this->defaultLifetime;
135 }
136 if ($lifetime === 0 || $lifetime > $this->maximumLifetime) {
137 $lifetime = $this->maximumLifetime;
138 }
139 $expires = $GLOBALS['EXEC_TIME'] + $lifetime;
140 $this->remove($entryIdentifier);
141 if ($this->compression) {
142 $data = gzcompress($data, $this->compressionLevel);
143 }
144 $GLOBALS['TYPO3_DB']->exec_INSERTquery($this->cacheTable, array(
145 'identifier' => $entryIdentifier,
146 'expires' => $expires,
147 'content' => $data
148 ));
149 if (count($tags)) {
150 $fields = array();
151 $fields[] = 'identifier';
152 $fields[] = 'tag';
153 $tagRows = array();
154 foreach ($tags as $tag) {
155 $tagRow = array();
156 $tagRow[] = $entryIdentifier;
157 $tagRow[] = $tag;
158 $tagRows[] = $tagRow;
159 }
160 $GLOBALS['TYPO3_DB']->exec_INSERTmultipleRows($this->tagsTable, $fields, $tagRows);
161 }
162 }
163
164 /**
165 * Loads data from a cache file.
166 *
167 * @param string $entryIdentifier An identifier which describes the cache entry to load
168 * @return mixed The cache entry's data as a string or FALSE if the cache entry could not be loaded
169 */
170 public function get($entryIdentifier) {
171 $this->throwExceptionIfFrontendDoesNotExist();
172
173 $cacheEntry = $GLOBALS['TYPO3_DB']->exec_SELECTgetSingleRow(
174 'content',
175 $this->cacheTable,
176 'identifier = ' . $GLOBALS['TYPO3_DB']->fullQuoteStr($entryIdentifier, $this->cacheTable) . ' AND ' . $this->notExpiredStatement
177 );
178 if (is_array($cacheEntry)) {
179 $cacheEntry = $cacheEntry['content'];
180 }
181 if ($this->compression && strlen($cacheEntry)) {
182 $cacheEntry = gzuncompress($cacheEntry);
183 }
184 return $cacheEntry !== NULL ? $cacheEntry : FALSE;
185 }
186
187 /**
188 * Checks if a cache entry with the specified identifier exists.
189 *
190 * @param string $entryIdentifier Specifies the identifier to check for existence
191 * @return bool TRUE if such an entry exists, FALSE if not
192 */
193 public function has($entryIdentifier) {
194 $this->throwExceptionIfFrontendDoesNotExist();
195 $hasEntry = FALSE;
196 $cacheEntries = $GLOBALS['TYPO3_DB']->exec_SELECTcountRows(
197 '*',
198 $this->cacheTable,
199 'identifier = ' . $GLOBALS['TYPO3_DB']->fullQuoteStr($entryIdentifier, $this->cacheTable) . ' AND ' . $this->notExpiredStatement
200 );
201 if ($cacheEntries >= 1) {
202 $hasEntry = TRUE;
203 }
204 return $hasEntry;
205 }
206
207 /**
208 * Removes all cache entries matching the specified identifier.
209 * Usually this only affects one entry.
210 *
211 * @param string $entryIdentifier Specifies the cache entry to remove
212 * @return bool TRUE if (at least) an entry could be removed or FALSE if no entry was found
213 */
214 public function remove($entryIdentifier) {
215 $this->throwExceptionIfFrontendDoesNotExist();
216 $entryRemoved = FALSE;
217 $res = $GLOBALS['TYPO3_DB']->exec_DELETEquery(
218 $this->cacheTable,
219 'identifier = ' . $GLOBALS['TYPO3_DB']->fullQuoteStr($entryIdentifier, $this->cacheTable)
220 );
221 $GLOBALS['TYPO3_DB']->exec_DELETEquery(
222 $this->tagsTable,
223 'identifier = ' . $GLOBALS['TYPO3_DB']->fullQuoteStr($entryIdentifier, $this->tagsTable)
224 );
225 if ($GLOBALS['TYPO3_DB']->sql_affected_rows($res) == 1) {
226 $entryRemoved = TRUE;
227 }
228 return $entryRemoved;
229 }
230
231 /**
232 * Finds and returns all cache entries which are tagged by the specified tag.
233 *
234 * @param string $tag The tag to search for
235 * @return array An array with identifiers of all matching entries. An empty array if no entries matched
236 */
237 public function findIdentifiersByTag($tag) {
238 $this->throwExceptionIfFrontendDoesNotExist();
239 $cacheEntryIdentifiers = array();
240 $cacheEntryIdentifierRows = $GLOBALS['TYPO3_DB']->exec_SELECTgetRows(
241 $this->identifierField,
242 $this->tableList,
243 $this->tagsTable . '.tag = ' . $GLOBALS['TYPO3_DB']->fullQuoteStr($tag, $this->tagsTable) . ' AND ' . $this->tableJoin . ' AND ' . $this->notExpiredStatement,
244 $this->identifierField
245 );
246 foreach ($cacheEntryIdentifierRows as $cacheEntryIdentifierRow) {
247 $cacheEntryIdentifiers[$cacheEntryIdentifierRow['identifier']] = $cacheEntryIdentifierRow['identifier'];
248 }
249 return $cacheEntryIdentifiers;
250 }
251
252 /**
253 * Removes all cache entries of this cache.
254 *
255 * @return void
256 */
257 public function flush() {
258 $this->throwExceptionIfFrontendDoesNotExist();
259 $GLOBALS['TYPO3_DB']->exec_TRUNCATEquery($this->cacheTable);
260 $GLOBALS['TYPO3_DB']->exec_TRUNCATEquery($this->tagsTable);
261 }
262
263 /**
264 * Removes all cache entries of this cache which are tagged by the specified tag.
265 *
266 * @param string $tag The tag the entries must have
267 * @return void
268 */
269 public function flushByTag($tag) {
270 $this->throwExceptionIfFrontendDoesNotExist();
271 $tagsTableWhereClause = $this->tagsTable . '.tag = ' . $GLOBALS['TYPO3_DB']->fullQuoteStr($tag, $this->tagsTable);
272 $this->deleteCacheTableRowsByTagsTableWhereClause($tagsTableWhereClause);
273 }
274
275 /**
276 * Does garbage collection
277 *
278 * @return void
279 */
280 public function collectGarbage() {
281 $this->throwExceptionIfFrontendDoesNotExist();
282 // Get identifiers of expired cache entries
283 $tagsEntryIdentifierRowsResource = $GLOBALS['TYPO3_DB']->exec_SELECTquery('identifier', $this->cacheTable, $this->expiredStatement);
284 $tagsEntryIdentifiers = array();
285 while ($tagsEntryIdentifierRow = $GLOBALS['TYPO3_DB']->sql_fetch_assoc($tagsEntryIdentifierRowsResource)) {
286 $tagsEntryIdentifiers[] = $GLOBALS['TYPO3_DB']->fullQuoteStr($tagsEntryIdentifierRow['identifier'], $this->tagsTable);
287 }
288 $GLOBALS['TYPO3_DB']->sql_free_result($tagsEntryIdentifierRowsResource);
289 // Delete tag rows connected to expired cache entries
290 if (count($tagsEntryIdentifiers)) {
291 $GLOBALS['TYPO3_DB']->exec_DELETEquery($this->tagsTable, 'identifier IN (' . implode(', ', $tagsEntryIdentifiers) . ')');
292 }
293 // Delete expired cache rows
294 $GLOBALS['TYPO3_DB']->exec_DELETEquery($this->cacheTable, $this->expiredStatement);
295 }
296
297 /**
298 * Returns the table where the cache entries are stored.
299 *
300 * @return string The cache table.
301 */
302 public function getCacheTable() {
303 $this->throwExceptionIfFrontendDoesNotExist();
304 return $this->cacheTable;
305 }
306
307 /**
308 * Gets the table where cache tags are stored.
309 *
310 * @return string Name of the table storing tags
311 */
312 public function getTagsTable() {
313 $this->throwExceptionIfFrontendDoesNotExist();
314 return $this->tagsTable;
315 }
316
317 /**
318 * Enable data compression
319 *
320 * @param bool $compression TRUE to enable compression
321 */
322 public function setCompression($compression) {
323 $this->compression = $compression;
324 }
325
326 /**
327 * Set data compression level.
328 * If compression is enabled and this is not set,
329 * gzcompress default level will be used
330 *
331 * @param int -1 to 9: Compression level
332 */
333 public function setCompressionLevel($compressionLevel) {
334 if ($compressionLevel >= -1 && $compressionLevel <= 9) {
335 $this->compressionLevel = $compressionLevel;
336 }
337 }
338
339 /**
340 * Check if required frontend instance exists
341 *
342 * @throws \TYPO3\CMS\Core\Cache\Exception If there is no frontend instance in $this->cache
343 * @return void
344 */
345 protected function throwExceptionIfFrontendDoesNotExist() {
346 if (!$this->cache instanceof \TYPO3\CMS\Core\Cache\Frontend\FrontendInterface) {
347 throw new \TYPO3\CMS\Core\Cache\Exception('No cache frontend has been set via setCache() yet.', 1236518288);
348 }
349 }
350
351 /**
352 * Calculate needed table definitions for this cache.
353 * This helper method is used by install tool and extension manager
354 * and is not part of the public API!
355 *
356 * @return string SQL of table definitions
357 */
358 public function getTableDefinitions() {
359 $cacheTableSql = file_get_contents(
360 \TYPO3\CMS\Core\Utility\ExtensionManagementUtility::extPath('core') .
361 'Resources/Private/Sql/Cache/Backend/Typo3DatabaseBackendCache.sql'
362 );
363 $requiredTableStructures = str_replace('###CACHE_TABLE###', $this->cacheTable, $cacheTableSql) . LF . LF;
364 $tagsTableSql = file_get_contents(
365 \TYPO3\CMS\Core\Utility\ExtensionManagementUtility::extPath('core') .
366 'Resources/Private/Sql/Cache/Backend/Typo3DatabaseBackendTags.sql'
367 );
368 $requiredTableStructures .= str_replace('###TAGS_TABLE###', $this->tagsTable, $tagsTableSql) . LF;
369 return $requiredTableStructures;
370 }
371
372 /**
373 * Deletes rows in cache table found by where clause on tags table
374 *
375 * @param string $tagsTableWhereClause The where clause for the tags table
376 * @return void
377 */
378 protected function deleteCacheTableRowsByTagsTableWhereClause($tagsTableWhereClause) {
379 $cacheEntryIdentifierRowsResource = $GLOBALS['TYPO3_DB']->exec_SELECTquery('DISTINCT identifier', $this->tagsTable, $tagsTableWhereClause);
380 $cacheEntryIdentifiers = array();
381 while ($cacheEntryIdentifierRow = $GLOBALS['TYPO3_DB']->sql_fetch_assoc($cacheEntryIdentifierRowsResource)) {
382 $cacheEntryIdentifiers[] = $GLOBALS['TYPO3_DB']->fullQuoteStr($cacheEntryIdentifierRow['identifier'], $this->cacheTable);
383 }
384 $GLOBALS['TYPO3_DB']->sql_free_result($cacheEntryIdentifierRowsResource);
385 if (count($cacheEntryIdentifiers)) {
386 $GLOBALS['TYPO3_DB']->exec_DELETEquery($this->cacheTable, 'identifier IN (' . implode(', ', $cacheEntryIdentifiers) . ')');
387 $GLOBALS['TYPO3_DB']->exec_DELETEquery($this->tagsTable, 'identifier IN (' . implode(', ', $cacheEntryIdentifiers) . ')');
388 }
389 }
390
391 }