1<?php
2/**
3 * Copyright 2013-2017 Horde LLC (http://www.horde.org/)
4 *
5 * See the enclosed file COPYING for license information (LGPL). If you
6 * did not receive this file, see http://www.horde.org/licenses/lgpl21.
7 *
8 * @category  Horde
9 * @copyright 2013-2017 Horde LLC
10 * @license   http://www.horde.org/licenses/lgpl21 LGPL 2.1
11 * @package   SessionHandler
12 */
13
14/**
15 * Horde_HashTable SessionHandler driver.
16 *
17 * @author    Michael Slusarz <slusarz@horde.org>
18 * @category  Horde
19 * @copyright 2013-2017 Horde LLC
20 * @license   http://www.horde.org/licenses/lgpl21 LGPL 2.1
21 * @package   SessionHandler
22 * @since     2.2.0
23 */
24class Horde_SessionHandler_Storage_Hashtable extends Horde_SessionHandler_Storage
25{
26    /**
27     * HashTable object.
28     *
29     * @var Horde_HashTable
30     */
31    protected $_hash;
32
33    /**
34     * Current session ID.
35     *
36     * @var string
37     */
38    protected $_id;
39
40    /**
41     * The ID used for session tracking.
42     *
43     * @var string
44     */
45    protected $_trackID = 'horde_sessions_track_ht';
46
47    /**
48     * Constructor.
49     *
50     * @param array $params  Parameters:
51     * <pre>
52     *   - hashtable: (Horde_HashTable) [REQUIRED] A Horde_HashTable object.
53     *   - track: (boolean) Track active sessions?
54     * </pre>
55     */
56    public function __construct(array $params = array())
57    {
58        if (empty($params['hashtable'])) {
59            throw new InvalidArgumentException('Missing hashtable parameter.');
60        }
61
62        if (!$params['hashtable']->locking) {
63            throw new InvalidArgumentException('HashTable object must support locking.');
64        }
65
66        $this->_hash = $params['hashtable'];
67        unset($params['hashtable']);
68
69        parent::__construct($params);
70
71        if (!empty($this->_params['track']) && (!rand(0, 999))) {
72            register_shutdown_function(array($this, 'trackGC'));
73        }
74    }
75
76    /**
77     */
78    public function open($save_path = null, $session_name = null)
79    {
80    }
81
82    /**
83     */
84    public function close()
85    {
86        if (isset($this->_id)) {
87            $this->_hash->unlock($this->_id);
88        }
89    }
90
91    /**
92     */
93    public function read($id)
94    {
95        if (!$this->readonly) {
96            $this->_hash->lock($id);
97        }
98
99        if (($result = $this->_hash->get($id)) === false) {
100            if (!$this->readonly) {
101                $this->_hash->unlock($id);
102            }
103
104            $result = '';
105        } elseif (!$this->readonly) {
106            $this->_id = $id;
107        }
108
109        return $result;
110    }
111
112    /**
113     */
114    public function write($id, $session_data)
115    {
116        $base = array_filter(array(
117            'timeout' => ini_get('session.gc_maxlifetime')
118        ));
119
120        if (!empty($this->_params['track'])) {
121            // Do a replace - the only time it should fail is if we are
122            // writing a session for the first time.  If that is the case,
123            // update the session tracker.
124            $res = $this->_hash->set($id, $session_data, array_merge($base, array(
125                'replace' => true,
126            )));
127            $track = !$res;
128        } else {
129            $res = $track = false;
130        }
131
132        if (!$res && !$this->_hash->set($id, $session_data, $base)) {
133            return false;
134        }
135
136        if ($track) {
137            $this->_hash->lock($this->_trackID);
138            $ids = $this->_getTrackIds();
139            $ids[$id] = 1;
140            $this->_hash->set($this->_trackID, json_encode($ids));
141            $this->_hash->unlock($this->_trackID);
142        }
143
144        return true;
145    }
146
147    /**
148     */
149    public function destroy($id)
150    {
151        $res = $this->_hash->delete($id);
152        $this->_hash->unlock($id);
153
154        if ($res === false) {
155            return false;
156        }
157
158        if (!empty($this->_params['track'])) {
159            $this->_hash->lock($this->_trackID);
160            if ($ids = $this->_getTrackIds()) {
161                unset($ids[$id]);
162                $this->_hash->set($this->_trackID, json_encode($ids));
163            }
164            $this->_hash->unlock($this->_trackID);
165        }
166
167        return true;
168    }
169
170    /**
171     */
172    public function gc($maxlifetime = 300)
173    {
174        return true;
175    }
176
177    /**
178     */
179    public function getSessionIDs()
180    {
181        if (empty($this->_params['track'])) {
182            throw new Horde_SessionHandler_Exception('Memcache session tracking not enabled.');
183        }
184
185        $this->trackGC();
186
187        return array_keys($this->_getTrackIds());
188    }
189
190    /**
191     * Do garbage collection for session tracking information.
192     */
193    public function trackGC()
194    {
195        try {
196            $this->_hash->lock($this->_trackID);
197
198            if ($ids = $this->_getTrackIds()) {
199                $alter = false;
200
201                foreach (array_keys($ids) as $key) {
202                    if (!$this->_hash->exists($key)) {
203                        unset($ids[$key]);
204                        $alter = true;
205                    }
206                }
207
208                if ($alter) {
209                    $this->_hash->set($this->_trackID, json_encode($ids));
210                }
211            }
212
213            $this->_hash->unlock($this->_trackID);
214        } catch (Horde_HashTable_Exception $e) {
215        }
216    }
217
218    /**
219     * Get the tracking IDs.
220     *
221     * @return array  Tracking IDs.
222     */
223    protected function _getTrackIds()
224    {
225        if ((($ids = $this->_hash->get($this->_trackID)) === false) ||
226            !($ids = json_decode($ids, true))) {
227            $ids = array();
228        }
229
230        return $ids;
231    }
232
233}
234