blob: 6652addee41854874b70adf3865427cbf300676b [file] [log] [blame]
Andrey Andreevc9eface2014-09-02 15:19:01 +03001<?php
2/**
3 * CodeIgniter
4 *
5 * An open source application development framework for PHP 5.2.4 or newer
6 *
7 * NOTICE OF LICENSE
8 *
9 * Licensed under the Open Software License version 3.0
10 *
11 * This source file is subject to the Open Software License (OSL 3.0) that is
12 * bundled with this package in the files license.txt / license.rst. It is
13 * also available through the world wide web at this URL:
14 * http://opensource.org/licenses/OSL-3.0
15 * If you did not receive a copy of the license and are unable to obtain it
16 * through the world wide web, please send an email to
17 * licensing@ellislab.com so we can send you a copy immediately.
18 *
19 * @package CodeIgniter
20 * @author Andrey Andreev
21 * @copyright Copyright (c) 2008 - 2014, EllisLab, Inc. (http://ellislab.com/)
22 * @license http://opensource.org/licenses/OSL-3.0 Open Software License (OSL 3.0)
23 * @link http://codeigniter.com
24 * @since Version 3.0
25 * @filesource
26 */
27defined('BASEPATH') OR exit('No direct script access allowed');
28
29/**
30 * CodeIgniter Session Memcached Driver
31 *
32 * @package CodeIgniter
33 * @subpackage Libraries
34 * @category Sessions
35 * @author Andrey Andreev
36 * @link http://codeigniter.com/user_guide/libraries/sessions.html
37 */
38class CI_Session_memcached_driver extends CI_Session_driver implements SessionHandlerInterface {
39
40 /**
Andrey Andreevc9eface2014-09-02 15:19:01 +030041 * Memcached instance
42 *
43 * @var Memcached
44 */
45 protected $_memcached;
46
47 /**
48 * Key prefix
49 *
50 * @var string
51 */
52 protected $_key_prefix = 'ci_session:';
53
54 /**
55 * Lock key
56 *
57 * @var string
58 */
59 protected $_lock_key;
60
61 // ------------------------------------------------------------------------
62
63 /**
64 * Class constructor
65 *
66 * @param array $params Configuration parameters
67 * @return void
68 */
69 public function __construct(&$params)
70 {
71 parent::__construct($params);
72
Andrey Andreevdfb39be2014-10-06 01:50:14 +030073 if (empty($this->_config['save_path']))
Andrey Andreevc9eface2014-09-02 15:19:01 +030074 {
75 log_message('error', 'Session: No Memcached save path configured.');
76 }
77
Andrey Andreevdfb39be2014-10-06 01:50:14 +030078 if ($this->_config['match_ip'] === TRUE)
Andrey Andreevc9eface2014-09-02 15:19:01 +030079 {
80 $this->_key_prefix .= $_SERVER['REMOTE_ADDR'].':';
81 }
82 }
83
84 // ------------------------------------------------------------------------
85
86 public function open($save_path, $name)
87 {
88 $this->_memcached = new Memcached();
Andrey Andreev4f502562014-11-10 19:18:33 +020089 $this->_memcached->setOption(Memcached::OPT_BINARY_PROTOCOL, TRUE); // required for touch() usage
Andrey Andreevc9eface2014-09-02 15:19:01 +030090 $server_list = array();
91 foreach ($this->_memcached->getServerList() as $server)
92 {
93 $server_list[] = $server['host'].':'.$server['port'];
94 }
95
Andrey Andreevdfb39be2014-10-06 01:50:14 +030096 if ( ! preg_match_all('#,?([^,:]+)\:(\d{1,5})(?:\:(\d+))?#', $this->_config['save_path'], $matches, PREG_SET_ORDER))
Andrey Andreevc9eface2014-09-02 15:19:01 +030097 {
98 $this->_memcached = NULL;
Andrey Andreevdfb39be2014-10-06 01:50:14 +030099 log_message('error', 'Session: Invalid Memcached save path format: '.$this->_config['save_path']);
Andrey Andreevc9eface2014-09-02 15:19:01 +0300100 return FALSE;
101 }
102
103 foreach ($matches as $match)
104 {
105 // If Memcached already has this server (or if the port is invalid), skip it
106 if (in_array($match[1].':'.$match[2], $server_list, TRUE))
107 {
108 log_message('debug', 'Session: Memcached server pool already has '.$match[1].':'.$match[2]);
109 continue;
110 }
111
112 if ( ! $this->_memcached->addServer($match[1], $match[2], isset($match[3]) ? $match[3] : 0))
113 {
114 log_message('error', 'Could not add '.$match[1].':'.$match[2].' to Memcached server pool.');
115 }
116 else
117 {
Andrey Andreeva8f29f92014-11-10 18:55:55 +0200118 $server_list[] = $match[1].':'.$match[2];
Andrey Andreevc9eface2014-09-02 15:19:01 +0300119 }
120 }
121
122 if (empty($server_list))
123 {
124 log_message('error', 'Session: Memcached server pool is empty.');
125 return FALSE;
126 }
127
128 return TRUE;
129 }
130
131 // ------------------------------------------------------------------------
132
133 public function read($session_id)
134 {
135 if (isset($this->_memcached) && $this->_get_lock($session_id))
136 {
Andrey Andreev7474a672014-10-31 23:35:32 +0200137 // Needed by write() to detect session_regenerate_id() calls
138 $this->_session_id = $session_id;
139
Andrey Andreevc9eface2014-09-02 15:19:01 +0300140 $session_data = (string) $this->_memcached->get($this->_key_prefix.$session_id);
141 $this->_fingerprint = md5($session_data);
142 return $session_data;
143 }
144
145 return FALSE;
146 }
147
148 public function write($session_id, $session_data)
149 {
Andrey Andreev7474a672014-10-31 23:35:32 +0200150 if ( ! isset($this->_memcached))
151 {
152 return FALSE;
153 }
154 // Was the ID regenerated?
155 elseif ($session_id !== $this->_session_id)
156 {
157 if ( ! $this->_release_lock() OR ! $this->_get_lock($session_id))
158 {
159 return FALSE;
160 }
161
162 $this->_fingerprint = md5('');
163 $this->_session_id = $session_id;
164 }
165
166 if (isset($this->_lock_key))
Andrey Andreevc9eface2014-09-02 15:19:01 +0300167 {
168 $this->_memcached->replace($this->_lock_key, time(), 5);
169 if ($this->_fingerprint !== ($fingerprint = md5($session_data)))
170 {
Andrey Andreevdfb39be2014-10-06 01:50:14 +0300171 if ($this->_memcached->set($this->_key_prefix.$session_id, $session_data, $this->_config['expiration']))
Andrey Andreevc9eface2014-09-02 15:19:01 +0300172 {
173 $this->_fingerprint = $fingerprint;
174 return TRUE;
175 }
176
177 return FALSE;
178 }
179
Andrey Andreevdfb39be2014-10-06 01:50:14 +0300180 return $this->_memcached->touch($this->_key_prefix.$session_id, $this->_config['expiration']);
Andrey Andreevc9eface2014-09-02 15:19:01 +0300181 }
182
183 return FALSE;
184 }
185
186 // ------------------------------------------------------------------------
187
188 public function close()
189 {
190 if (isset($this->_memcached))
191 {
192 isset($this->_lock_key) && $this->_memcached->delete($this->_lock_key);
193 if ( ! $this->_memcached->quit())
194 {
195 return FALSE;
196 }
197
198 $this->_memcached = NULL;
199 return TRUE;
200 }
201
202 return FALSE;
203 }
204
205 // ------------------------------------------------------------------------
206
207 public function destroy($session_id)
208 {
209 if (isset($this->_memcached, $this->_lock_key))
210 {
211 $this->_memcached->delete($this->_key_prefix.$session_id);
Andrey Andreev7474a672014-10-31 23:35:32 +0200212 return $this->_cookie_destroy();
Andrey Andreevc9eface2014-09-02 15:19:01 +0300213 }
214
Andrey Andreev7474a672014-10-31 23:35:32 +0200215 return FALSE;
Andrey Andreevc9eface2014-09-02 15:19:01 +0300216 }
217
218 // ------------------------------------------------------------------------
219
220 public function gc($maxlifetime)
221 {
Andrey Andreev7474a672014-10-31 23:35:32 +0200222 // Not necessary, Memcached takes care of that.
Andrey Andreevc9eface2014-09-02 15:19:01 +0300223 return TRUE;
224 }
225
226 // ------------------------------------------------------------------------
227
228 protected function _get_lock($session_id)
229 {
230 if (isset($this->_lock_key))
231 {
232 return $this->_memcached->replace($this->_lock_key, time(), 5);
233 }
234
235 $lock_key = $this->_key_prefix.$session_id.':lock';
236 if ( ! ($ts = $this->_memcached->get($lock_key)))
237 {
238 if ( ! $this->_memcached->set($lock_key, TRUE, 5))
239 {
240 log_message('error', 'Session: Error while trying to obtain lock for '.$this->_key_prefix.$session_id);
241 return FALSE;
242 }
243
244 $this->_lock_key = $lock_key;
245 $this->_lock = TRUE;
246 return TRUE;
247 }
248
249 // Another process has the lock, we'll try to wait for it to free itself ...
250 $attempt = 0;
251 while ($attempt++ < 5)
252 {
253 usleep(((time() - $ts) * 1000000) - 20000);
254 if (($ts = $this->_memcached->get($lock_key)) < time())
255 {
256 continue;
257 }
258
259 if ( ! $this->_memcached->set($lock_key, time(), 5))
260 {
261 log_message('error', 'Session: Error while trying to obtain lock for '.$this->_key_prefix.$session_id);
262 return FALSE;
263 }
264
265 $this->_lock_key = $lock_key;
266 break;
267 }
268
269 if ($attempt === 5)
270 {
271 log_message('error', 'Session: Unable to obtain lock for '.$this->_key_prefix.$session_id.' after 5 attempts, aborting.');
272 return FALSE;
273 }
274
275 $this->_lock = TRUE;
276 return TRUE;
277 }
278
279 // ------------------------------------------------------------------------
280
281 protected function _release_lock()
282 {
283 if (isset($this->_memcached, $this->_lock_key) && $this->_lock)
284 {
285 if ( ! $this->_memcached->delete($this->_lock_key) && $this->_memcached->getResultCode() !== Memcached::RES_NOTFOUND)
286 {
287 log_message('error', 'Session: Error while trying to free lock for '.$this->_key_prefix.$session_id);
288 return FALSE;
289 }
290
291 $this->_lock_key = NULL;
292 $this->_lock = FALSE;
293 }
294
295 return TRUE;
296 }
297
298}
299
300/* End of file Session_memcached_driver.php */
301/* Location: ./system/libraries/Session/drivers/Session_memcached_driver.php */