Subversion-Projekte lars-tiefland.ci

Revision

Revision 1257 | Zur aktuellen Revision | Details | Letzte Änderung | Log anzeigen | RSS feed

Revision Autor Zeilennr. Zeile
68 lars 1
<?php
2
/**
3
 * CodeIgniter
4
 *
5
 * An open source application development framework for PHP
6
 *
7
 * This content is released under the MIT License (MIT)
8
 *
9
 * Copyright (c) 2014 - 2016, British Columbia Institute of Technology
10
 *
11
 * Permission is hereby granted, free of charge, to any person obtaining a copy
12
 * of this software and associated documentation files (the "Software"), to deal
13
 * in the Software without restriction, including without limitation the rights
14
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
15
 * copies of the Software, and to permit persons to whom the Software is
16
 * furnished to do so, subject to the following conditions:
17
 *
18
 * The above copyright notice and this permission notice shall be included in
19
 * all copies or substantial portions of the Software.
20
 *
21
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
22
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
23
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
24
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
25
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
26
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
27
 * THE SOFTWARE.
28
 *
29
 * @package	CodeIgniter
30
 * @author	EllisLab Dev Team
31
 * @copyright	Copyright (c) 2008 - 2014, EllisLab, Inc. (https://ellislab.com/)
32
 * @copyright	Copyright (c) 2014 - 2016, British Columbia Institute of Technology (http://bcit.ca/)
33
 * @license	http://opensource.org/licenses/MIT	MIT License
34
 * @link	https://codeigniter.com
35
 * @since	Version 3.0.0
36
 * @filesource
37
 */
38
defined('BASEPATH') OR exit('No direct script access allowed');
39
 
40
/**
41
 * CodeIgniter Session Memcached Driver
42
 *
43
 * @package	CodeIgniter
44
 * @subpackage	Libraries
45
 * @category	Sessions
46
 * @author	Andrey Andreev
47
 * @link	https://codeigniter.com/user_guide/libraries/sessions.html
48
 */
49
class CI_Session_memcached_driver extends CI_Session_driver implements SessionHandlerInterface {
50
 
51
	/**
52
	 * Memcached instance
53
	 *
54
	 * @var	Memcached
55
	 */
56
	protected $_memcached;
57
 
58
	/**
59
	 * Key prefix
60
	 *
61
	 * @var	string
62
	 */
63
	protected $_key_prefix = 'ci_session:';
64
 
65
	/**
66
	 * Lock key
67
	 *
68
	 * @var	string
69
	 */
70
	protected $_lock_key;
71
 
72
	// ------------------------------------------------------------------------
73
 
74
	/**
75
	 * Class constructor
76
	 *
77
	 * @param	array	$params	Configuration parameters
78
	 * @return	void
79
	 */
80
	public function __construct(&$params)
81
	{
82
		parent::__construct($params);
83
 
84
		if (empty($this->_config['save_path']))
85
		{
86
			log_message('error', 'Session: No Memcached save path configured.');
87
		}
88
 
89
		if ($this->_config['match_ip'] === TRUE)
90
		{
91
			$this->_key_prefix .= $_SERVER['REMOTE_ADDR'].':';
92
		}
93
	}
94
 
95
	// ------------------------------------------------------------------------
96
 
97
	/**
98
	 * Open
99
	 *
100
	 * Sanitizes save_path and initializes connections.
101
	 *
102
	 * @param	string	$save_path	Server path(s)
103
	 * @param	string	$name		Session cookie name, unused
104
	 * @return	bool
105
	 */
106
	public function open($save_path, $name)
107
	{
108
		$this->_memcached = new Memcached();
109
		$this->_memcached->setOption(Memcached::OPT_BINARY_PROTOCOL, TRUE); // required for touch() usage
110
		$server_list = array();
111
		foreach ($this->_memcached->getServerList() as $server)
112
		{
113
			$server_list[] = $server['host'].':'.$server['port'];
114
		}
115
 
116
		if ( ! preg_match_all('#,?([^,:]+)\:(\d{1,5})(?:\:(\d+))?#', $this->_config['save_path'], $matches, PREG_SET_ORDER))
117
		{
118
			$this->_memcached = NULL;
119
			log_message('error', 'Session: Invalid Memcached save path format: '.$this->_config['save_path']);
120
			return $this->_fail();
121
		}
122
 
123
		foreach ($matches as $match)
124
		{
125
			// If Memcached already has this server (or if the port is invalid), skip it
126
			if (in_array($match[1].':'.$match[2], $server_list, TRUE))
127
			{
128
				log_message('debug', 'Session: Memcached server pool already has '.$match[1].':'.$match[2]);
129
				continue;
130
			}
131
 
132
			if ( ! $this->_memcached->addServer($match[1], $match[2], isset($match[3]) ? $match[3] : 0))
133
			{
134
				log_message('error', 'Could not add '.$match[1].':'.$match[2].' to Memcached server pool.');
135
			}
136
			else
137
			{
138
				$server_list[] = $match[1].':'.$match[2];
139
			}
140
		}
141
 
142
		if (empty($server_list))
143
		{
144
			log_message('error', 'Session: Memcached server pool is empty.');
145
			return $this->_fail();
146
		}
147
 
148
		return $this->_success;
149
	}
150
 
151
	// ------------------------------------------------------------------------
152
 
153
	/**
154
	 * Read
155
	 *
156
	 * Reads session data and acquires a lock
157
	 *
158
	 * @param	string	$session_id	Session ID
159
	 * @return	string	Serialized session data
160
	 */
161
	public function read($session_id)
162
	{
163
		if (isset($this->_memcached) && $this->_get_lock($session_id))
164
		{
165
			// Needed by write() to detect session_regenerate_id() calls
166
			$this->_session_id = $session_id;
167
 
168
			$session_data = (string) $this->_memcached->get($this->_key_prefix.$session_id);
169
			$this->_fingerprint = md5($session_data);
170
			return $session_data;
171
		}
172
 
173
		return $this->_fail();
174
	}
175
 
176
	// ------------------------------------------------------------------------
177
 
178
	/**
179
	 * Write
180
	 *
181
	 * Writes (create / update) session data
182
	 *
183
	 * @param	string	$session_id	Session ID
184
	 * @param	string	$session_data	Serialized session data
185
	 * @return	bool
186
	 */
187
	public function write($session_id, $session_data)
188
	{
189
		if ( ! isset($this->_memcached))
190
		{
191
			return $this->_fail();
192
		}
193
		// Was the ID regenerated?
194
		elseif ($session_id !== $this->_session_id)
195
		{
196
			if ( ! $this->_release_lock() OR ! $this->_get_lock($session_id))
197
			{
198
				return $this->_fail();
199
			}
200
 
201
			$this->_fingerprint = md5('');
202
			$this->_session_id = $session_id;
203
		}
204
 
205
		if (isset($this->_lock_key))
206
		{
207
			$key = $this->_key_prefix.$session_id;
208
 
209
			$this->_memcached->replace($this->_lock_key, time(), 300);
210
			if ($this->_fingerprint !== ($fingerprint = md5($session_data)))
211
			{
212
				if (
213
					$this->_memcached->replace($key, $session_data, $this->_config['expiration'])
214
					OR ($this->_memcached->getResultCode() === Memcached::RES_NOTFOUND && $this->_memcached->set($key, $session_data, $this->_config['expiration']))
215
				)
216
				{
217
					$this->_fingerprint = $fingerprint;
218
					return $this->_success;
219
				}
220
 
221
				return $this->_fail();
222
			}
223
 
224
			if (
225
				$this->_memcached->touch($key, $this->_config['expiration'])
226
				OR ($this->_memcached->getResultCode() === Memcached::RES_NOTFOUND && $this->_memcached->set($key, $session_data, $this->_config['expiration']))
227
			)
228
			{
229
				return $this->_success;
230
			}
231
		}
232
 
233
		return $this->_fail();
234
	}
235
 
236
	// ------------------------------------------------------------------------
237
 
238
	/**
239
	 * Close
240
	 *
241
	 * Releases locks and closes connection.
242
	 *
243
	 * @return	bool
244
	 */
245
	public function close()
246
	{
247
		if (isset($this->_memcached))
248
		{
249
			$this->_release_lock();
250
			if ( ! $this->_memcached->quit())
251
			{
252
				return $this->_fail();
253
			}
254
 
255
			$this->_memcached = NULL;
256
			return $this->_success;
257
		}
258
 
259
		return $this->_fail();
260
	}
261
 
262
	// ------------------------------------------------------------------------
263
 
264
	/**
265
	 * Destroy
266
	 *
267
	 * Destroys the current session.
268
	 *
269
	 * @param	string	$session_id	Session ID
270
	 * @return	bool
271
	 */
272
	public function destroy($session_id)
273
	{
274
		if (isset($this->_memcached, $this->_lock_key))
275
		{
276
			$this->_memcached->delete($this->_key_prefix.$session_id);
277
			$this->_cookie_destroy();
278
			return $this->_success;
279
		}
280
 
281
		return $this->_fail();
282
	}
283
 
284
	// ------------------------------------------------------------------------
285
 
286
	/**
287
	 * Garbage Collector
288
	 *
289
	 * Deletes expired sessions
290
	 *
291
	 * @param	int 	$maxlifetime	Maximum lifetime of sessions
292
	 * @return	bool
293
	 */
294
	public function gc($maxlifetime)
295
	{
296
		// Not necessary, Memcached takes care of that.
297
		return $this->_success;
298
	}
299
 
300
	// ------------------------------------------------------------------------
301
 
302
	/**
303
	 * Get lock
304
	 *
305
	 * Acquires an (emulated) lock.
306
	 *
307
	 * @param	string	$session_id	Session ID
308
	 * @return	bool
309
	 */
310
	protected function _get_lock($session_id)
311
	{
312
		// PHP 7 reuses the SessionHandler object on regeneration,
313
		// so we need to check here if the lock key is for the
314
		// correct session ID.
315
		if ($this->_lock_key === $this->_key_prefix.$session_id.':lock')
316
		{
317
			if ( ! $this->_memcached->replace($this->_lock_key, time(), 300))
318
			{
319
				return ($this->_memcached->getResultCode() === Memcached::RES_NOTFOUND)
320
					? $this->_memcached->set($this->_lock_key, time(), 300)
321
					: FALSE;
322
			}
323
		}
324
 
325
		// 30 attempts to obtain a lock, in case another request already has it
326
		$lock_key = $this->_key_prefix.$session_id.':lock';
327
		$attempt = 0;
328
		do
329
		{
330
			if ($this->_memcached->get($lock_key))
331
			{
332
				sleep(1);
333
				continue;
334
			}
335
 
336
			if ( ! $this->_memcached->set($lock_key, time(), 300))
337
			{
338
				log_message('error', 'Session: Error while trying to obtain lock for '.$this->_key_prefix.$session_id);
339
				return FALSE;
340
			}
341
 
342
			$this->_lock_key = $lock_key;
343
			break;
344
		}
345
		while (++$attempt < 30);
346
 
347
		if ($attempt === 30)
348
		{
349
			log_message('error', 'Session: Unable to obtain lock for '.$this->_key_prefix.$session_id.' after 30 attempts, aborting.');
350
			return FALSE;
351
		}
352
 
353
		$this->_lock = TRUE;
354
		return TRUE;
355
	}
356
 
357
	// ------------------------------------------------------------------------
358
 
359
	/**
360
	 * Release lock
361
	 *
362
	 * Releases a previously acquired lock
363
	 *
364
	 * @return	bool
365
	 */
366
	protected function _release_lock()
367
	{
368
		if (isset($this->_memcached, $this->_lock_key) && $this->_lock)
369
		{
370
			if ( ! $this->_memcached->delete($this->_lock_key) && $this->_memcached->getResultCode() !== Memcached::RES_NOTFOUND)
371
			{
372
				log_message('error', 'Session: Error while trying to free lock for '.$this->_lock_key);
373
				return FALSE;
374
			}
375
 
376
			$this->_lock_key = NULL;
377
			$this->_lock = FALSE;
378
		}
379
 
380
		return TRUE;
381
	}
382
}