1<?php 2/** 3 * Matomo - free/libre analytics platform 4 * 5 * @link https://matomo.org 6 * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later 7 * 8 */ 9 10namespace Piwik\Session\SaveHandler; 11 12use Piwik\Db; 13use Piwik\DbHelper; 14use Exception; 15use Piwik\SettingsPiwik; 16use Piwik\Updater\Migration; 17use Zend_Session; 18use Zend_Session_SaveHandler_Interface; 19 20/** 21 * Database-backed session save handler 22 * 23 */ 24class DbTable implements Zend_Session_SaveHandler_Interface 25{ 26 public static $wasSessionToLargeToRead = false; 27 28 protected $config; 29 protected $maxLifetime; 30 31 const TABLE_NAME = 'session'; 32 const TOKEN_HASH_ALGO = 'sha512'; 33 34 /** 35 * @param array $config 36 */ 37 public function __construct($config) 38 { 39 $this->config = $config; 40 $this->maxLifetime = ini_get('session.gc_maxlifetime'); 41 } 42 43 private function hashSessionId($id) 44 { 45 $salt = SettingsPiwik::getSalt(); 46 return hash(self::TOKEN_HASH_ALGO, $id . $salt); 47 } 48 49 50 /** 51 * Destructor 52 * 53 * @return void 54 */ 55 public function __destruct() 56 { 57 Zend_Session::writeClose(); 58 } 59 60 /** 61 * Open Session - retrieve resources 62 * 63 * @param string $save_path 64 * @param string $name 65 * @return boolean 66 */ 67 public function open($save_path, $name) 68 { 69 Db::get()->getConnection(); 70 71 return true; 72 } 73 74 /** 75 * Close Session - free resources 76 * 77 * @return boolean 78 */ 79 public function close() 80 { 81 return true; 82 } 83 84 /** 85 * Read session data 86 * 87 * @param string $id 88 * @return string 89 */ 90 public function read($id) 91 { 92 $id = $this->hashSessionId($id); 93 $sql = 'SELECT ' . $this->config['dataColumn'] . ' FROM ' . $this->config['name'] 94 . ' WHERE ' . $this->config['primary'] . ' = ?' 95 . ' AND ' . $this->config['modifiedColumn'] . ' + ' . $this->config['lifetimeColumn'] . ' >= ?'; 96 97 $result = $this->fetchOne($sql, array($id, time())); 98 99 if (!$result) { 100 $result = ''; 101 } 102 103 return $result; 104 } 105 106 private function fetchOne($sql, $bind) 107 { 108 try { 109 $result = Db::get()->fetchOne($sql, $bind); 110 } catch (Exception $e) { 111 if (Db::get()->isErrNo($e, Migration\Db::ERROR_CODE_TABLE_NOT_EXISTS)) { 112 $this->migrateToDbSessionTable(); 113 $result = Db::get()->fetchOne($sql, $bind); 114 } else { 115 throw $e; 116 } 117 } 118 return $result; 119 } 120 121 private function query($sql, $bind) 122 { 123 try { 124 $result = Db::get()->query($sql, $bind); 125 } catch (Exception $e) { 126 if (Db::get()->isErrNo($e, Migration\Db::ERROR_CODE_TABLE_NOT_EXISTS)) { 127 $this->migrateToDbSessionTable(); 128 $result = Db::get()->query($sql, $bind); 129 } else { 130 throw $e; 131 } 132 } 133 return $result; 134 } 135 136 /** 137 * Write Session - commit data to resource 138 * 139 * @param string $id 140 * @param mixed $data 141 * @return boolean 142 */ 143 public function write($id, $data) 144 { 145 $id = $this->hashSessionId($id); 146 147 $sql = 'INSERT INTO ' . $this->config['name'] 148 . ' (' . $this->config['primary'] . ',' 149 . $this->config['modifiedColumn'] . ',' 150 . $this->config['lifetimeColumn'] . ',' 151 . $this->config['dataColumn'] . ')' 152 . ' VALUES (?,?,?,?)' 153 . ' ON DUPLICATE KEY UPDATE ' 154 . $this->config['modifiedColumn'] . ' = ?,' 155 . $this->config['lifetimeColumn'] . ' = ?,' 156 . $this->config['dataColumn'] . ' = ?'; 157 158 $this->query($sql, array($id, time(), $this->maxLifetime, $data, time(), $this->maxLifetime, $data)); 159 160 return true; 161 } 162 163 /** 164 * Destroy Session - remove data from resource for 165 * given session id 166 * 167 * @param string $id 168 * @return boolean 169 */ 170 public function destroy($id) 171 { 172 $id = $this->hashSessionId($id); 173 174 $sql = 'DELETE FROM ' . $this->config['name'] . ' WHERE ' . $this->config['primary'] . ' = ?'; 175 176 $this->query($sql, array($id)); 177 178 return true; 179 } 180 181 /** 182 * Garbage Collection - remove old session data older 183 * than $maxlifetime (in seconds) 184 * 185 * @param int $maxlifetime timestamp in seconds 186 * @return bool always true 187 */ 188 public function gc($maxlifetime) 189 { 190 $sql = 'DELETE FROM ' . $this->config['name'] 191 . ' WHERE ' . $this->config['modifiedColumn'] . ' + ' . $this->config['lifetimeColumn'] . ' < ?'; 192 193 $this->query($sql, array(time())); 194 195 return true; 196 } 197 198 private function migrateToDbSessionTable() 199 { 200 // happens when updating from Piwik 1.4 or earlier to Matomo 3.7+ 201 // in this case on update it will change the session handler to dbtable, but it hasn't performed 202 // the DB updates just yet which means the session table won't be available as it was only added in 203 // Piwik 1.5 => results in a sql error the session table does not exist 204 try { 205 $sql = DbHelper::getTableCreateSql(self::TABLE_NAME); 206 Db::query($sql); 207 } catch (Exception $e) { 208 if (!Db::get()->isErrNo($e, Migration\Db::ERROR_CODE_TABLE_EXISTS)) { 209 throw $e; 210 } 211 } 212 } 213 214} 215