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