File indexing completed on 2025-01-19 05:20:57
0001 <?php 0002 /** 0003 * Zend Framework 0004 * 0005 * LICENSE 0006 * 0007 * This source file is subject to the new BSD license that is bundled 0008 * with this package in the file LICENSE.txt. 0009 * It is also available through the world-wide-web at this URL: 0010 * http://framework.zend.com/license/new-bsd 0011 * If you did not receive a copy of the license and are unable to 0012 * obtain it through the world-wide-web, please send an email 0013 * to license@zend.com so we can send you a copy immediately. 0014 * 0015 * @category Zend 0016 * @package Zend_Cache 0017 * @subpackage Zend_Cache_Backend 0018 * @copyright Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com) 0019 * @license http://framework.zend.com/license/new-bsd New BSD License 0020 * @version $Id$ 0021 */ 0022 0023 0024 /** 0025 * @see Zend_Cache_Backend_Interface 0026 */ 0027 // require_once 'Zend/Cache/Backend/ExtendedInterface.php'; 0028 0029 /** 0030 * @see Zend_Cache_Backend 0031 */ 0032 // require_once 'Zend/Cache/Backend.php'; 0033 0034 /** 0035 * @package Zend_Cache 0036 * @subpackage Zend_Cache_Backend 0037 * @copyright Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com) 0038 * @license http://framework.zend.com/license/new-bsd New BSD License 0039 */ 0040 class Zend_Cache_Backend_Sqlite extends Zend_Cache_Backend implements Zend_Cache_Backend_ExtendedInterface 0041 { 0042 /** 0043 * Available options 0044 * 0045 * =====> (string) cache_db_complete_path : 0046 * - the complete path (filename included) of the SQLITE database 0047 * 0048 * ====> (int) automatic_vacuum_factor : 0049 * - Disable / Tune the automatic vacuum process 0050 * - The automatic vacuum process defragment the database file (and make it smaller) 0051 * when a clean() or delete() is called 0052 * 0 => no automatic vacuum 0053 * 1 => systematic vacuum (when delete() or clean() methods are called) 0054 * x (integer) > 1 => automatic vacuum randomly 1 times on x clean() or delete() 0055 * 0056 * @var array Available options 0057 */ 0058 protected $_options = array( 0059 'cache_db_complete_path' => null, 0060 'automatic_vacuum_factor' => 10 0061 ); 0062 0063 /** 0064 * DB ressource 0065 * 0066 * @var mixed $_db 0067 */ 0068 private $_db = null; 0069 0070 /** 0071 * Boolean to store if the structure has benn checked or not 0072 * 0073 * @var boolean $_structureChecked 0074 */ 0075 private $_structureChecked = false; 0076 0077 /** 0078 * Constructor 0079 * 0080 * @param array $options Associative array of options 0081 * @throws Zend_cache_Exception 0082 * @return void 0083 */ 0084 public function __construct(array $options = array()) 0085 { 0086 parent::__construct($options); 0087 if ($this->_options['cache_db_complete_path'] === null) { 0088 Zend_Cache::throwException('cache_db_complete_path option has to set'); 0089 } 0090 if (!extension_loaded('sqlite')) { 0091 Zend_Cache::throwException("Cannot use SQLite storage because the 'sqlite' extension is not loaded in the current PHP environment"); 0092 } 0093 $this->_getConnection(); 0094 } 0095 0096 /** 0097 * Destructor 0098 * 0099 * @return void 0100 */ 0101 public function __destruct() 0102 { 0103 @sqlite_close($this->_getConnection()); 0104 } 0105 0106 /** 0107 * Test if a cache is available for the given id and (if yes) return it (false else) 0108 * 0109 * @param string $id Cache id 0110 * @param boolean $doNotTestCacheValidity If set to true, the cache validity won't be tested 0111 * @return string|false Cached datas 0112 */ 0113 public function load($id, $doNotTestCacheValidity = false) 0114 { 0115 $this->_checkAndBuildStructure(); 0116 $sql = "SELECT content FROM cache WHERE id='$id'"; 0117 if (!$doNotTestCacheValidity) { 0118 $sql = $sql . " AND (expire=0 OR expire>" . time() . ')'; 0119 } 0120 $result = $this->_query($sql); 0121 $row = @sqlite_fetch_array($result); 0122 if ($row) { 0123 return $row['content']; 0124 } 0125 return false; 0126 } 0127 0128 /** 0129 * Test if a cache is available or not (for the given id) 0130 * 0131 * @param string $id Cache id 0132 * @return mixed|false (a cache is not available) or "last modified" timestamp (int) of the available cache record 0133 */ 0134 public function test($id) 0135 { 0136 $this->_checkAndBuildStructure(); 0137 $sql = "SELECT lastModified FROM cache WHERE id='$id' AND (expire=0 OR expire>" . time() . ')'; 0138 $result = $this->_query($sql); 0139 $row = @sqlite_fetch_array($result); 0140 if ($row) { 0141 return ((int) $row['lastModified']); 0142 } 0143 return false; 0144 } 0145 0146 /** 0147 * Save some string datas into a cache record 0148 * 0149 * Note : $data is always "string" (serialization is done by the 0150 * core not by the backend) 0151 * 0152 * @param string $data Datas to cache 0153 * @param string $id Cache id 0154 * @param array $tags Array of strings, the cache record will be tagged by each string entry 0155 * @param int $specificLifetime If != false, set a specific lifetime for this cache record (null => infinite lifetime) 0156 * @throws Zend_Cache_Exception 0157 * @return boolean True if no problem 0158 */ 0159 public function save($data, $id, $tags = array(), $specificLifetime = false) 0160 { 0161 $this->_checkAndBuildStructure(); 0162 $lifetime = $this->getLifetime($specificLifetime); 0163 $data = @sqlite_escape_string($data); 0164 $mktime = time(); 0165 if ($lifetime === null) { 0166 $expire = 0; 0167 } else { 0168 $expire = $mktime + $lifetime; 0169 } 0170 $this->_query("DELETE FROM cache WHERE id='$id'"); 0171 $sql = "INSERT INTO cache (id, content, lastModified, expire) VALUES ('$id', '$data', $mktime, $expire)"; 0172 $res = $this->_query($sql); 0173 if (!$res) { 0174 $this->_log("Zend_Cache_Backend_Sqlite::save() : impossible to store the cache id=$id"); 0175 return false; 0176 } 0177 $res = true; 0178 foreach ($tags as $tag) { 0179 $res = $this->_registerTag($id, $tag) && $res; 0180 } 0181 return $res; 0182 } 0183 0184 /** 0185 * Remove a cache record 0186 * 0187 * @param string $id Cache id 0188 * @return boolean True if no problem 0189 */ 0190 public function remove($id) 0191 { 0192 $this->_checkAndBuildStructure(); 0193 $res = $this->_query("SELECT COUNT(*) AS nbr FROM cache WHERE id='$id'"); 0194 $result1 = @sqlite_fetch_single($res); 0195 $result2 = $this->_query("DELETE FROM cache WHERE id='$id'"); 0196 $result3 = $this->_query("DELETE FROM tag WHERE id='$id'"); 0197 $this->_automaticVacuum(); 0198 return ($result1 && $result2 && $result3); 0199 } 0200 0201 /** 0202 * Clean some cache records 0203 * 0204 * Available modes are : 0205 * Zend_Cache::CLEANING_MODE_ALL (default) => remove all cache entries ($tags is not used) 0206 * Zend_Cache::CLEANING_MODE_OLD => remove too old cache entries ($tags is not used) 0207 * Zend_Cache::CLEANING_MODE_MATCHING_TAG => remove cache entries matching all given tags 0208 * ($tags can be an array of strings or a single string) 0209 * Zend_Cache::CLEANING_MODE_NOT_MATCHING_TAG => remove cache entries not {matching one of the given tags} 0210 * ($tags can be an array of strings or a single string) 0211 * Zend_Cache::CLEANING_MODE_MATCHING_ANY_TAG => remove cache entries matching any given tags 0212 * ($tags can be an array of strings or a single string) 0213 * 0214 * @param string $mode Clean mode 0215 * @param array $tags Array of tags 0216 * @return boolean True if no problem 0217 */ 0218 public function clean($mode = Zend_Cache::CLEANING_MODE_ALL, $tags = array()) 0219 { 0220 $this->_checkAndBuildStructure(); 0221 $return = $this->_clean($mode, $tags); 0222 $this->_automaticVacuum(); 0223 return $return; 0224 } 0225 0226 /** 0227 * Return an array of stored cache ids 0228 * 0229 * @return array array of stored cache ids (string) 0230 */ 0231 public function getIds() 0232 { 0233 $this->_checkAndBuildStructure(); 0234 $res = $this->_query("SELECT id FROM cache WHERE (expire=0 OR expire>" . time() . ")"); 0235 $result = array(); 0236 while ($id = @sqlite_fetch_single($res)) { 0237 $result[] = $id; 0238 } 0239 return $result; 0240 } 0241 0242 /** 0243 * Return an array of stored tags 0244 * 0245 * @return array array of stored tags (string) 0246 */ 0247 public function getTags() 0248 { 0249 $this->_checkAndBuildStructure(); 0250 $res = $this->_query("SELECT DISTINCT(name) AS name FROM tag"); 0251 $result = array(); 0252 while ($id = @sqlite_fetch_single($res)) { 0253 $result[] = $id; 0254 } 0255 return $result; 0256 } 0257 0258 /** 0259 * Return an array of stored cache ids which match given tags 0260 * 0261 * In case of multiple tags, a logical AND is made between tags 0262 * 0263 * @param array $tags array of tags 0264 * @return array array of matching cache ids (string) 0265 */ 0266 public function getIdsMatchingTags($tags = array()) 0267 { 0268 $first = true; 0269 $ids = array(); 0270 foreach ($tags as $tag) { 0271 $res = $this->_query("SELECT DISTINCT(id) AS id FROM tag WHERE name='$tag'"); 0272 if (!$res) { 0273 return array(); 0274 } 0275 $rows = @sqlite_fetch_all($res, SQLITE_ASSOC); 0276 $ids2 = array(); 0277 foreach ($rows as $row) { 0278 $ids2[] = $row['id']; 0279 } 0280 if ($first) { 0281 $ids = $ids2; 0282 $first = false; 0283 } else { 0284 $ids = array_intersect($ids, $ids2); 0285 } 0286 } 0287 $result = array(); 0288 foreach ($ids as $id) { 0289 $result[] = $id; 0290 } 0291 return $result; 0292 } 0293 0294 /** 0295 * Return an array of stored cache ids which don't match given tags 0296 * 0297 * In case of multiple tags, a logical OR is made between tags 0298 * 0299 * @param array $tags array of tags 0300 * @return array array of not matching cache ids (string) 0301 */ 0302 public function getIdsNotMatchingTags($tags = array()) 0303 { 0304 $res = $this->_query("SELECT id FROM cache"); 0305 $rows = @sqlite_fetch_all($res, SQLITE_ASSOC); 0306 $result = array(); 0307 foreach ($rows as $row) { 0308 $id = $row['id']; 0309 $matching = false; 0310 foreach ($tags as $tag) { 0311 $res = $this->_query("SELECT COUNT(*) AS nbr FROM tag WHERE name='$tag' AND id='$id'"); 0312 if (!$res) { 0313 return array(); 0314 } 0315 $nbr = (int) @sqlite_fetch_single($res); 0316 if ($nbr > 0) { 0317 $matching = true; 0318 } 0319 } 0320 if (!$matching) { 0321 $result[] = $id; 0322 } 0323 } 0324 return $result; 0325 } 0326 0327 /** 0328 * Return an array of stored cache ids which match any given tags 0329 * 0330 * In case of multiple tags, a logical AND is made between tags 0331 * 0332 * @param array $tags array of tags 0333 * @return array array of any matching cache ids (string) 0334 */ 0335 public function getIdsMatchingAnyTags($tags = array()) 0336 { 0337 $first = true; 0338 $ids = array(); 0339 foreach ($tags as $tag) { 0340 $res = $this->_query("SELECT DISTINCT(id) AS id FROM tag WHERE name='$tag'"); 0341 if (!$res) { 0342 return array(); 0343 } 0344 $rows = @sqlite_fetch_all($res, SQLITE_ASSOC); 0345 $ids2 = array(); 0346 foreach ($rows as $row) { 0347 $ids2[] = $row['id']; 0348 } 0349 if ($first) { 0350 $ids = $ids2; 0351 $first = false; 0352 } else { 0353 $ids = array_merge($ids, $ids2); 0354 } 0355 } 0356 $result = array(); 0357 foreach ($ids as $id) { 0358 $result[] = $id; 0359 } 0360 return $result; 0361 } 0362 0363 /** 0364 * Return the filling percentage of the backend storage 0365 * 0366 * @throws Zend_Cache_Exception 0367 * @return int integer between 0 and 100 0368 */ 0369 public function getFillingPercentage() 0370 { 0371 $dir = dirname($this->_options['cache_db_complete_path']); 0372 $free = disk_free_space($dir); 0373 $total = disk_total_space($dir); 0374 if ($total == 0) { 0375 Zend_Cache::throwException('can\'t get disk_total_space'); 0376 } else { 0377 if ($free >= $total) { 0378 return 100; 0379 } 0380 return ((int) (100. * ($total - $free) / $total)); 0381 } 0382 } 0383 0384 /** 0385 * Return an array of metadatas for the given cache id 0386 * 0387 * The array must include these keys : 0388 * - expire : the expire timestamp 0389 * - tags : a string array of tags 0390 * - mtime : timestamp of last modification time 0391 * 0392 * @param string $id cache id 0393 * @return array array of metadatas (false if the cache id is not found) 0394 */ 0395 public function getMetadatas($id) 0396 { 0397 $tags = array(); 0398 $res = $this->_query("SELECT name FROM tag WHERE id='$id'"); 0399 if ($res) { 0400 $rows = @sqlite_fetch_all($res, SQLITE_ASSOC); 0401 foreach ($rows as $row) { 0402 $tags[] = $row['name']; 0403 } 0404 } 0405 $this->_query('CREATE TABLE cache (id TEXT PRIMARY KEY, content BLOB, lastModified INTEGER, expire INTEGER)'); 0406 $res = $this->_query("SELECT lastModified,expire FROM cache WHERE id='$id'"); 0407 if (!$res) { 0408 return false; 0409 } 0410 $row = @sqlite_fetch_array($res, SQLITE_ASSOC); 0411 return array( 0412 'tags' => $tags, 0413 'mtime' => $row['lastModified'], 0414 'expire' => $row['expire'] 0415 ); 0416 } 0417 0418 /** 0419 * Give (if possible) an extra lifetime to the given cache id 0420 * 0421 * @param string $id cache id 0422 * @param int $extraLifetime 0423 * @return boolean true if ok 0424 */ 0425 public function touch($id, $extraLifetime) 0426 { 0427 $sql = "SELECT expire FROM cache WHERE id='$id' AND (expire=0 OR expire>" . time() . ')'; 0428 $res = $this->_query($sql); 0429 if (!$res) { 0430 return false; 0431 } 0432 $expire = @sqlite_fetch_single($res); 0433 $newExpire = $expire + $extraLifetime; 0434 $res = $this->_query("UPDATE cache SET lastModified=" . time() . ", expire=$newExpire WHERE id='$id'"); 0435 if ($res) { 0436 return true; 0437 } else { 0438 return false; 0439 } 0440 } 0441 0442 /** 0443 * Return an associative array of capabilities (booleans) of the backend 0444 * 0445 * The array must include these keys : 0446 * - automatic_cleaning (is automating cleaning necessary) 0447 * - tags (are tags supported) 0448 * - expired_read (is it possible to read expired cache records 0449 * (for doNotTestCacheValidity option for example)) 0450 * - priority does the backend deal with priority when saving 0451 * - infinite_lifetime (is infinite lifetime can work with this backend) 0452 * - get_list (is it possible to get the list of cache ids and the complete list of tags) 0453 * 0454 * @return array associative of with capabilities 0455 */ 0456 public function getCapabilities() 0457 { 0458 return array( 0459 'automatic_cleaning' => true, 0460 'tags' => true, 0461 'expired_read' => true, 0462 'priority' => false, 0463 'infinite_lifetime' => true, 0464 'get_list' => true 0465 ); 0466 } 0467 0468 /** 0469 * PUBLIC METHOD FOR UNIT TESTING ONLY ! 0470 * 0471 * Force a cache record to expire 0472 * 0473 * @param string $id Cache id 0474 */ 0475 public function ___expire($id) 0476 { 0477 $time = time() - 1; 0478 $this->_query("UPDATE cache SET lastModified=$time, expire=$time WHERE id='$id'"); 0479 } 0480 0481 /** 0482 * Return the connection resource 0483 * 0484 * If we are not connected, the connection is made 0485 * 0486 * @throws Zend_Cache_Exception 0487 * @return resource Connection resource 0488 */ 0489 private function _getConnection() 0490 { 0491 if (is_resource($this->_db)) { 0492 return $this->_db; 0493 } else { 0494 $this->_db = @sqlite_open($this->_options['cache_db_complete_path']); 0495 if (!(is_resource($this->_db))) { 0496 Zend_Cache::throwException("Impossible to open " . $this->_options['cache_db_complete_path'] . " cache DB file"); 0497 } 0498 return $this->_db; 0499 } 0500 } 0501 0502 /** 0503 * Execute an SQL query silently 0504 * 0505 * @param string $query SQL query 0506 * @return mixed|false query results 0507 */ 0508 private function _query($query) 0509 { 0510 $db = $this->_getConnection(); 0511 if (is_resource($db)) { 0512 $res = @sqlite_query($db, $query); 0513 if ($res === false) { 0514 return false; 0515 } else { 0516 return $res; 0517 } 0518 } 0519 return false; 0520 } 0521 0522 /** 0523 * Deal with the automatic vacuum process 0524 * 0525 * @return void 0526 */ 0527 private function _automaticVacuum() 0528 { 0529 if ($this->_options['automatic_vacuum_factor'] > 0) { 0530 $rand = rand(1, $this->_options['automatic_vacuum_factor']); 0531 if ($rand == 1) { 0532 $this->_query('VACUUM'); 0533 } 0534 } 0535 } 0536 0537 /** 0538 * Register a cache id with the given tag 0539 * 0540 * @param string $id Cache id 0541 * @param string $tag Tag 0542 * @return boolean True if no problem 0543 */ 0544 private function _registerTag($id, $tag) { 0545 $res = $this->_query("DELETE FROM TAG WHERE name='$tag' AND id='$id'"); 0546 $res = $this->_query("INSERT INTO tag (name, id) VALUES ('$tag', '$id')"); 0547 if (!$res) { 0548 $this->_log("Zend_Cache_Backend_Sqlite::_registerTag() : impossible to register tag=$tag on id=$id"); 0549 return false; 0550 } 0551 return true; 0552 } 0553 0554 /** 0555 * Build the database structure 0556 * 0557 * @return false 0558 */ 0559 private function _buildStructure() 0560 { 0561 $this->_query('DROP INDEX tag_id_index'); 0562 $this->_query('DROP INDEX tag_name_index'); 0563 $this->_query('DROP INDEX cache_id_expire_index'); 0564 $this->_query('DROP TABLE version'); 0565 $this->_query('DROP TABLE cache'); 0566 $this->_query('DROP TABLE tag'); 0567 $this->_query('CREATE TABLE version (num INTEGER PRIMARY KEY)'); 0568 $this->_query('CREATE TABLE cache (id TEXT PRIMARY KEY, content BLOB, lastModified INTEGER, expire INTEGER)'); 0569 $this->_query('CREATE TABLE tag (name TEXT, id TEXT)'); 0570 $this->_query('CREATE INDEX tag_id_index ON tag(id)'); 0571 $this->_query('CREATE INDEX tag_name_index ON tag(name)'); 0572 $this->_query('CREATE INDEX cache_id_expire_index ON cache(id, expire)'); 0573 $this->_query('INSERT INTO version (num) VALUES (1)'); 0574 } 0575 0576 /** 0577 * Check if the database structure is ok (with the good version) 0578 * 0579 * @return boolean True if ok 0580 */ 0581 private function _checkStructureVersion() 0582 { 0583 $result = $this->_query("SELECT num FROM version"); 0584 if (!$result) return false; 0585 $row = @sqlite_fetch_array($result); 0586 if (!$row) { 0587 return false; 0588 } 0589 if (((int) $row['num']) != 1) { 0590 // old cache structure 0591 $this->_log('Zend_Cache_Backend_Sqlite::_checkStructureVersion() : old cache structure version detected => the cache is going to be dropped'); 0592 return false; 0593 } 0594 return true; 0595 } 0596 0597 /** 0598 * Clean some cache records 0599 * 0600 * Available modes are : 0601 * Zend_Cache::CLEANING_MODE_ALL (default) => remove all cache entries ($tags is not used) 0602 * Zend_Cache::CLEANING_MODE_OLD => remove too old cache entries ($tags is not used) 0603 * Zend_Cache::CLEANING_MODE_MATCHING_TAG => remove cache entries matching all given tags 0604 * ($tags can be an array of strings or a single string) 0605 * Zend_Cache::CLEANING_MODE_NOT_MATCHING_TAG => remove cache entries not {matching one of the given tags} 0606 * ($tags can be an array of strings or a single string) 0607 * Zend_Cache::CLEANING_MODE_MATCHING_ANY_TAG => remove cache entries matching any given tags 0608 * ($tags can be an array of strings or a single string) 0609 * 0610 * @param string $mode Clean mode 0611 * @param array $tags Array of tags 0612 * @return boolean True if no problem 0613 */ 0614 private function _clean($mode = Zend_Cache::CLEANING_MODE_ALL, $tags = array()) 0615 { 0616 switch ($mode) { 0617 case Zend_Cache::CLEANING_MODE_ALL: 0618 $res1 = $this->_query('DELETE FROM cache'); 0619 $res2 = $this->_query('DELETE FROM tag'); 0620 return $res1 && $res2; 0621 break; 0622 case Zend_Cache::CLEANING_MODE_OLD: 0623 $mktime = time(); 0624 $res1 = $this->_query("DELETE FROM tag WHERE id IN (SELECT id FROM cache WHERE expire>0 AND expire<=$mktime)"); 0625 $res2 = $this->_query("DELETE FROM cache WHERE expire>0 AND expire<=$mktime"); 0626 return $res1 && $res2; 0627 break; 0628 case Zend_Cache::CLEANING_MODE_MATCHING_TAG: 0629 $ids = $this->getIdsMatchingTags($tags); 0630 $result = true; 0631 foreach ($ids as $id) { 0632 $result = $this->remove($id) && $result; 0633 } 0634 return $result; 0635 break; 0636 case Zend_Cache::CLEANING_MODE_NOT_MATCHING_TAG: 0637 $ids = $this->getIdsNotMatchingTags($tags); 0638 $result = true; 0639 foreach ($ids as $id) { 0640 $result = $this->remove($id) && $result; 0641 } 0642 return $result; 0643 break; 0644 case Zend_Cache::CLEANING_MODE_MATCHING_ANY_TAG: 0645 $ids = $this->getIdsMatchingAnyTags($tags); 0646 $result = true; 0647 foreach ($ids as $id) { 0648 $result = $this->remove($id) && $result; 0649 } 0650 return $result; 0651 break; 0652 default: 0653 break; 0654 } 0655 return false; 0656 } 0657 0658 /** 0659 * Check if the database structure is ok (with the good version), if no : build it 0660 * 0661 * @throws Zend_Cache_Exception 0662 * @return boolean True if ok 0663 */ 0664 private function _checkAndBuildStructure() 0665 { 0666 if (!($this->_structureChecked)) { 0667 if (!$this->_checkStructureVersion()) { 0668 $this->_buildStructure(); 0669 if (!$this->_checkStructureVersion()) { 0670 Zend_Cache::throwException("Impossible to build cache structure in " . $this->_options['cache_db_complete_path']); 0671 } 0672 } 0673 $this->_structureChecked = true; 0674 } 0675 return true; 0676 } 0677 0678 }