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 }