Fixed bug #18016
[Packages/TYPO3.CMS.git] / t3lib / cache / backend / class.t3lib_cache_backend_pdobackend.php
1 <?php
2 /***************************************************************
3 * Copyright notice
4 *
5 * (c) 2010-2011 Christian Kuhn <lolli@schwarzbu.ch>
6 * All rights reserved
7 *
8 * This script is part of the TYPO3 project. The TYPO3 project is
9 * free software; you can redistribute it and/or modify
10 * it under the terms of the GNU General Public License as published by
11 * the Free Software Foundation; either version 2 of the License, or
12 * (at your option) any later version.
13 *
14 * The GNU General Public License can be found at
15 * http://www.gnu.org/copyleft/gpl.html.
16 *
17 * This script is distributed in the hope that it will be useful,
18 * but WITHOUT ANY WARRANTY; without even the implied warranty of
19 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
20 * GNU General Public License for more details.
21 *
22 * This copyright notice MUST APPEAR in all copies of the script!
23 ***************************************************************/
24
25 /**
26 * A PDO database cache backend
27 *
28 * @package TYPO3
29 * @subpackage t3lib_cache
30 * @api
31 * @scope prototype
32 * @author Christian Kuhn <lolli@schwarzbu.ch>
33 */
34 class t3lib_cache_backend_PdoBackend extends t3lib_cache_backend_AbstractBackend {
35
36 /**
37 * @var string
38 */
39 protected $dataSourceName;
40
41 /**
42 * @var string
43 */
44 protected $username;
45
46 /**
47 * @var string
48 */
49 protected $password;
50
51 /**
52 * Used to seperate stored data by user, SAPI, context, ...
53 * @var string
54 */
55 protected $scope;
56
57 /**
58 * @var PDO
59 */
60 protected $databaseHandle;
61
62 /**
63 * @var string
64 */
65 protected $pdoDriver;
66
67 /**
68 * Constructs this backend
69 *
70 * @param array $options Configuration options - depends on the actual backend
71 * @author Christian Kuhn <lolli@schwarzbu.ch>
72 */
73 public function __construct(array $options = array()) {
74 parent::__construct($options);
75
76 $this->connect();
77 }
78
79 /**
80 * Sets the DSN to use
81 *
82 * @param string $DSN The DSN to use for connecting to the DB
83 * @return void
84 * @author Karsten Dambekalns <karsten@typo3.org>
85 * @api
86 */
87 public function setDataSourceName($DSN) {
88 $this->dataSourceName = $DSN;
89 }
90
91 /**
92 * Sets the username to use
93 *
94 * @param string $username The username to use for connecting to the DB
95 * @return void
96 * @author Karsten Dambekalns <karsten@typo3.org>
97 * @api
98 */
99 public function setUsername($username) {
100 $this->username = $username;
101 }
102
103 /**
104 * Sets the password to use
105 *
106 * @param string $password The password to use for connecting to the DB
107 * @return void
108 * @author Karsten Dambekalns <karsten@typo3.org>
109 * @api
110 */
111 public function setPassword($password) {
112 $this->password = $password;
113 }
114
115 /**
116 * Initializes the identifier prefix when setting the cache.
117 *
118 * @param t3lib_cache_frontend_Frontend $cache
119 * @return void
120 * @author Robert Lemke <robert@typo3.org>
121 * @author Karsten Dambekalns <karsten@typo3.org>
122 */
123 public function setCache(t3lib_cache_frontend_Frontend $cache) {
124 parent::setCache($cache);
125 $processUser = extension_loaded('posix') ? posix_getpwuid(posix_geteuid()) : array('name' => 'default');
126 $this->scope = t3lib_div::shortMD5(PATH_site . $processUser['name'], 12);
127 }
128
129 /**
130 * Saves data in the cache.
131 *
132 * @param string $entryIdentifier An identifier for this specific cache entry
133 * @param string $data The data to be stored
134 * @param array $tags Tags to associate with this cache entry
135 * @param integer $lifetime Lifetime of this cache entry in seconds. If NULL is specified, the default lifetime is used. "0" means unlimited liftime.
136 * @return void
137 * @throws t3lib_cache_Exception if no cache frontend has been set.
138 * @throws t3lib_cache_exception_InvalidData if $data is not a string
139 * @author Karsten Dambekalns <karsten@typo3.org>
140 * @api
141 */
142 public function set($entryIdentifier, $data, array $tags = array(), $lifetime = NULL) {
143 if (!$this->cache instanceof t3lib_cache_frontend_Frontend) {
144 throw new t3lib_cache_Exception(
145 'No cache frontend has been set yet via setCache().',
146 1259515600
147 );
148 }
149
150 if (!is_string($data)) {
151 throw new t3lib_cache_exception_InvalidData(
152 'The specified data is of type "' . gettype($data) . '" but a string is expected.',
153 1259515601
154 );
155 }
156
157 if ($this->has($entryIdentifier)) {
158 $this->remove($entryIdentifier);
159 }
160
161 $lifetime = ($lifetime === NULL) ? $this->defaultLifetime : $lifetime;
162
163 $statementHandle = $this->databaseHandle->prepare(
164 'INSERT INTO "cache" ("identifier", "scope", "cache", "created", "lifetime", "content") VALUES (?, ?, ?, ?, ?, ?)'
165 );
166 $result = $statementHandle->execute(
167 array($entryIdentifier, $this->scope, $this->cacheIdentifier, $GLOBALS['EXEC_TIME'], $lifetime, $data)
168 );
169
170 if ($result === FALSE) {
171 throw new t3lib_cache_Exception(
172 'The cache entry "' . $entryIdentifier . '" could not be written.',
173 1259530791
174 );
175 }
176
177 $statementHandle = $this->databaseHandle->prepare(
178 'INSERT INTO "tags" ("identifier", "scope", "cache", "tag") VALUES (?, ?, ?, ?)'
179 );
180
181 foreach ($tags as $tag) {
182 $result = $statementHandle->execute(
183 array($entryIdentifier, $this->scope, $this->cacheIdentifier, $tag)
184 );
185 if ($result === FALSE) {
186 throw new t3lib_cache_Exception(
187 'The tag "' . $tag . ' for cache entry "' . $entryIdentifier . '" could not be written.',
188 1259530751
189 );
190 }
191 }
192 }
193
194 /**
195 * Loads data from the cache.
196 *
197 * @param string $entryIdentifier An identifier which describes the cache entry to load
198 * @return mixed The cache entry's content as a string or FALSE if the cache entry could not be loaded
199 * @author Karsten Dambekalns <karsten@typo3.org>
200 * @api
201 */
202 public function get($entryIdentifier) {
203 $statementHandle = $this->databaseHandle->prepare(
204 'SELECT "content" FROM "cache" WHERE "identifier"=? AND "scope"=? AND "cache"=?' . $this->getNotExpiredStatement()
205 );
206 $statementHandle->execute(
207 array($entryIdentifier, $this->scope, $this->cacheIdentifier)
208 );
209 return $statementHandle->fetchColumn();
210 }
211
212 /**
213 * Checks if a cache entry with the specified identifier exists.
214 *
215 * @param string $entryIdentifier An identifier specifying the cache entry
216 * @return boolean TRUE if such an entry exists, FALSE if not
217 * @author Karsten Dambekalns <karsten@typo3.org>
218 * @api
219 */
220 public function has($entryIdentifier) {
221 $statementHandle = $this->databaseHandle->prepare(
222 'SELECT COUNT("identifier") FROM "cache" WHERE "identifier"=? AND "scope"=? AND "cache"=?' . $this->getNotExpiredStatement()
223 );
224 $statementHandle->execute(
225 array($entryIdentifier, $this->scope, $this->cacheIdentifier)
226 );
227 return ($statementHandle->fetchColumn() > 0);
228 }
229
230 /**
231 * Removes all cache entries matching the specified identifier.
232 * Usually this only affects one entry but if - for what reason ever -
233 * old entries for the identifier still exist, they are removed as well.
234 *
235 * @param string $entryIdentifier Specifies the cache entry to remove
236 * @return boolean TRUE if (at least) an entry could be removed or FALSE if no entry was found
237 * @author Karsten Dambekalns <karsten@typo3.org>
238 * @api
239 */
240 public function remove($entryIdentifier) {
241 $statementHandle = $this->databaseHandle->prepare(
242 'DELETE FROM "tags" WHERE "identifier"=? AND "scope"=? AND "cache"=?'
243 );
244 $statementHandle->execute(
245 array($entryIdentifier, $this->scope, $this->cacheIdentifier)
246 );
247
248 $statementHandle = $this->databaseHandle->prepare(
249 'DELETE FROM "cache" WHERE "identifier"=? AND "scope"=? AND "cache"=?'
250 );
251 $statementHandle->execute(
252 array($entryIdentifier, $this->scope, $this->cacheIdentifier)
253 );
254
255 return ($statementHandle->rowCount() > 0);
256 }
257
258 /**
259 * Removes all cache entries of this cache.
260 *
261 * @return void
262 * @author Karsten Dambekalns <karsten@typo3.org>
263 * @api
264 */
265 public function flush() {
266 $statementHandle = $this->databaseHandle->prepare(
267 'DELETE FROM "tags" WHERE "scope"=? AND "cache"=?'
268 );
269 $statementHandle->execute(
270 array($this->scope, $this->cacheIdentifier)
271 );
272
273 $statementHandle = $this->databaseHandle->prepare(
274 'DELETE FROM "cache" WHERE "scope"=? AND "cache"=?'
275 );
276 $statementHandle->execute(
277 array($this->scope, $this->cacheIdentifier)
278 );
279 }
280
281 /**
282 * Removes all cache entries of this cache which are tagged by the specified tag.
283 *
284 * @param string $tag The tag the entries must have
285 * @return void
286 * @author Robert Lemke <robert@typo3.org>
287 * @api
288 */
289 public function flushByTag($tag) {
290 $statementHandle = $this->databaseHandle->prepare(
291 'DELETE FROM "cache" WHERE "scope"=? AND "cache"=? AND "identifier" IN (SELECT "identifier" FROM "tags" WHERE "scope"=? AND "cache"=? AND "tag"=?)'
292 );
293 $statementHandle->execute(
294 array($this->scope, $this->cacheIdentifier, $this->scope, $this->cacheIdentifier, $tag)
295 );
296
297 $statementHandle = $this->databaseHandle->prepare(
298 'DELETE FROM "tags" WHERE "scope"=? AND "cache"=? AND "tag"=?'
299 );
300 $statementHandle->execute(
301 array($this->scope, $this->cacheIdentifier, $tag)
302 );
303 }
304
305 /**
306 * Removes all cache entries of this cache which are tagged by the specified tags.
307 * This method doesn't exist in FLOW3, but is mandatory for TYPO3v4.
308 *
309 * @TODO: Make smarter
310 * @param array $tags The tags the entries must have
311 * @return void
312 * @author Christian Kuhn <lolli@schwarzbu.ch>
313 */
314 public function flushBytags(array $tags) {
315 foreach ($tags as $tag) {
316 $this->flushByTag($tag);
317 }
318 }
319
320 /**
321 * Finds and returns all cache entry identifiers which are tagged by the
322 * specified tag.
323 *
324 * @param string $tag The tag to search for
325 * @return array An array with identifiers of all matching entries. An empty array if no entries matched
326 * @author Karsten Dambekalns <karsten@typo3.org>
327 * @api
328 */
329 public function findIdentifiersByTag($tag) {
330 $statementHandle = $this->databaseHandle->prepare(
331 'SELECT "identifier" FROM "tags" WHERE "scope"=? AND "cache"=? AND "tag"=?'
332 );
333 $statementHandle->execute(
334 array($this->scope, $this->cacheIdentifier, $tag)
335 );
336 return $statementHandle->fetchAll(PDO::FETCH_COLUMN);
337 }
338
339 /**
340 * Finds and returns all cache entry identifiers which are tagged with
341 * all of the specified tags.
342 * This method doesn't exist in FLOW3, but is mandatory for TYPO3v4.
343 *
344 * @TODO: Make smarter
345 * @param array $tags Tags to search for
346 * @return array An array with identifiers of all matching entries. An empty array if no entries matched
347 * @author Christian Kuhn <lolli@schwarzbu.ch>
348 */
349 public function findIdentifiersByTags(array $tags) {
350 $taggedEntries = array();
351 $foundEntries = array();
352
353 foreach ($tags as $tag) {
354 $taggedEntries[$tag] = $this->findIdentifiersByTag($tag);
355 }
356
357 $intersectedTaggedEntries = call_user_func_array('array_intersect', $taggedEntries);
358
359 foreach ($intersectedTaggedEntries as $entryIdentifier) {
360 if ($this->has($entryIdentifier)) {
361 $foundEntries[$entryIdentifier] = $entryIdentifier;
362 }
363 }
364
365 return $foundEntries;
366 }
367
368 /**
369 * Does garbage collection
370 *
371 * @return void
372 * @author Karsten Dambekalns <karsten@typo3.org>
373 * @api
374 */
375 public function collectGarbage() {
376 $statementHandle = $this->databaseHandle->prepare(
377 'DELETE FROM "tags" WHERE "scope"=? AND "cache"=? AND "identifier" IN ' .
378 '(SELECT "identifier" FROM "cache" WHERE "scope"=? AND "cache"=? AND "lifetime" > 0 AND "created" + "lifetime" < ' . $GLOBALS['EXEC_TIME'] . ')'
379 );
380 $statementHandle->execute(
381 array($this->scope, $this->cacheIdentifier, $this->scope, $this->cacheIdentifier)
382 );
383
384 $statementHandle = $this->databaseHandle->prepare(
385 'DELETE FROM "cache" WHERE "scope"=? AND "cache"=? AND "lifetime" > 0 AND "created" + "lifetime" < ' . $GLOBALS['EXEC_TIME']
386 );
387 $statementHandle->execute(
388 array($this->scope, $this->cacheIdentifier)
389 );
390 }
391
392 /**
393 * Returns an SQL statement that evaluates to true if the entry is not expired.
394 *
395 * @return string
396 * @author Karsten Dambekalns <karsten@typo3.org>
397 */
398 protected function getNotExpiredStatement() {
399 return ' AND ("lifetime" = 0 OR "created" + "lifetime" >= ' . $GLOBALS['EXEC_TIME'] . ')';
400 }
401
402 /**
403 * Connect to the database
404 *
405 * @return void
406 * @author Karsten Dambekalns <karsten@typo3.org>
407 */
408 protected function connect() {
409 try {
410 $splitdsn = explode(':', $this->dataSourceName, 2);
411 $this->pdoDriver = $splitdsn[0];
412
413 if ($this->pdoDriver === 'sqlite' && !file_exists($splitdsn[1])) {
414 $this->databaseHandle = t3lib_div::makeInstance('PDO', $this->dataSourceName, $this->username, $this->password);
415 $this->createCacheTables();
416 } else {
417 $this->databaseHandle = t3lib_div::makeInstance('PDO', $this->dataSourceName, $this->username, $this->password);
418 }
419
420 $this->databaseHandle->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
421
422 if ($this->pdoDriver === 'mysql') {
423 $this->databaseHandle->exec('SET SESSION sql_mode=\'ANSI\';');
424 }
425 } catch (PDOException $e) {
426 }
427 }
428
429 /**
430 * Creates the tables needed for the cache backend.
431 *
432 * @return void
433 * @throws RuntimeException if something goes wrong
434 * @author Karsten Dambekalns <karsten@typo3.org>
435 */
436 protected function createCacheTables() {
437 try {
438 t3lib_PdoHelper::importSql($this->databaseHandle, $this->pdoDriver, PATH_t3lib . 'cache/backend/resources/ddl.sql');
439 } catch (PDOException $e) {
440 throw new RuntimeException(
441 'Could not create cache tables with DSN "' . $this->dataSourceName . '". PDO error: ' . $e->getMessage(),
442 1259576985
443 );
444 }
445 }
446 }
447
448 if (defined('TYPO3_MODE') && $GLOBALS['TYPO3_CONF_VARS'][TYPO3_MODE]['XCLASS']['t3lib/cache/backend/class.t3lib_cache_backend_pdobackend.php']) {
449 include_once($GLOBALS['TYPO3_CONF_VARS'][TYPO3_MODE]['XCLASS']['t3lib/cache/backend/class.t3lib_cache_backend_pdobackend.php']);
450 }
451 ?>