1: <?php
2: /**
3: * The Horde_Session:: class provides a set of methods for handling the
4: * administration and contents of the Horde session variable.
5: *
6: * Copyright 2010-2012 Horde LLC (http://www.horde.org/)
7: *
8: * See the enclosed file COPYING for license information (LGPL). If you
9: * did not receive this file, see http://www.horde.org/licenses/lgpl21.
10: *
11: * @author Michael Slusarz <slusarz@horde.org>
12: * @category Horde
13: * @license http://www.horde.org/licenses/lgpl21 LGPL 2.1
14: * @package Core
15: */
16: class Horde_Session
17: {
18: /* Class constants. */
19: const DATA = '_d';
20: const MODIFIED = '_m';
21: const PRUNE = '_p';
22:
23: const TYPE_ARRAY = 1;
24: const TYPE_OBJECT = 2;
25:
26: const NOT_SERIALIZED = 0;
27: const IS_SERIALIZED = 1;
28:
29: /**
30: * Maximum size of the pruneable data store.
31: *
32: * @var integer
33: */
34: public $maxStore = 20;
35:
36: /**
37: * The session handler object.
38: *
39: * @var Horde_SessionHandler
40: */
41: public $sessionHandler = null;
42:
43: /**
44: * Indicates that the session is active (read/write).
45: *
46: * @var boolean
47: */
48: private $_active = false;
49:
50: /**
51: * Indicate that a new session ID has been generated for this page load.
52: *
53: * @var boolean
54: */
55: private $_cleansession = false;
56:
57: /**
58: * Use LZF compression?
59: * We use LZF compression on arrays and objects. Compressing numbers and
60: * most strings is not enought of an benefit for the overhead.
61: *
62: * @var boolean
63: */
64: private $_lzf = false;
65:
66: /**
67: * Indicates that session data is read-only.
68: *
69: * @var boolean
70: */
71: private $_readonly = false;
72:
73: /**
74: * On re-login, indicate whether we were previously authenticated.
75: *
76: * @var integer
77: */
78: private $_relogin = null;
79:
80: /**
81: * Constructor.
82: */
83: public function __construct()
84: {
85: $this->_lzf = Horde_Util::extensionExists('lzf');
86:
87: /* Make sure global session variable is always initialized. */
88: $_SESSION = array();
89: }
90:
91: /**
92: * Sets a custom session handler up, if there is one.
93: *
94: * @param boolean $start Initiate the session?
95: * @param string $cache_limiter Override for the session cache limiter
96: * value.
97: * @param string $session_id The session ID to use.
98: *
99: * @throws Horde_Exception
100: */
101: public function setup($start = true, $cache_limiter = null,
102: $session_id = null)
103: {
104: global $conf;
105:
106: ini_set('url_rewriter.tags', 0);
107: if (empty($conf['session']['use_only_cookies'])) {
108: ini_set('session.use_only_cookies', 0);
109: } else {
110: ini_set('session.use_only_cookies', 1);
111: if (!empty($conf['cookie']['domain']) &&
112: (strpos($conf['server']['name'], '.') === false)) {
113: throw new Horde_Exception('Session cookies will not work without a FQDN and with a non-empty cookie domain. Either use a fully qualified domain name like "http://www.example.com" instead of "http://example" only, or set the cookie domain in the Horde configuration to an empty value, or enable non-cookie (url-based) sessions in the Horde configuration.');
114: }
115: }
116:
117: if (!empty($conf['session']['timeout'])) {
118: ini_set('session.gc_maxlifetime', $conf['session']['timeout']);
119: }
120:
121: session_set_cookie_params(
122: 0,
123: $conf['cookie']['path'],
124: $conf['cookie']['domain'],
125: $conf['use_ssl'] == 1 ? 1 : 0,
126: true
127: );
128: session_cache_limiter(is_null($cache_limiter) ? $conf['session']['cache_limiter'] : $cache_limiter);
129: session_name(urlencode($conf['session']['name']));
130: if ($session_id) {
131: session_id($session_id);
132: }
133:
134: /* We want to create an instance here, not get, since we may be
135: * destroying the previous instances in the page. */
136: $this->sessionHandler = $GLOBALS['injector']->createInstance('Horde_SessionHandler');
137:
138: if ($start) {
139: $this->start();
140: $this->_start();
141: }
142: }
143:
144: /**
145: * Starts the session.
146: *
147: * @since 1.4.0
148: */
149: public function start()
150: {
151: session_start();
152: $this->_active = true;
153:
154: /* We have reopened a session. Check to make sure that authentication
155: * status has not changed in the meantime. */
156: if (!$this->_readonly &&
157: !is_null($this->_relogin) &&
158: (($GLOBALS['registry']->getAuth() !== false) !== $this->_relogin)) {
159: Horde::logMessage('Previous session attempted to be reopened after authentication status change. All session modifications will be ignored.', 'DEBUG');
160: $this->_readonly = true;
161: }
162: }
163:
164: /**
165: * Tasks to perform when starting a session.
166: */
167: private function _start()
168: {
169: /* Create internal data arrays. */
170: if (!isset($_SESSION[self::MODIFIED])) {
171: /* Last modification time of session.
172: * This will cause the check below to always return true
173: * (time() >= 0) and will set the initial value. */
174: $_SESSION[self::MODIFIED] = 0;
175: }
176:
177: /* Determine if we need to force write the session to avoid a
178: * session timeout, even though the session is unchanged.
179: * Theory: On initial login, set the current time plus half of the
180: * max lifetime in the session. Then check this timestamp before
181: * saving. If we exceed, force a write of the session and set a
182: * new timestamp. Why half the maxlifetime? It guarantees that if
183: * we are accessing the server via a periodic mechanism (think
184: * folder refreshing in IMP) that we will catch this refresh. */
185: $curr_time = time();
186: if ($curr_time >= $_SESSION[self::MODIFIED]) {
187: $_SESSION[self::MODIFIED] = intval($curr_time + (ini_get('session.gc_maxlifetime') / 2));
188: $this->sessionHandler->changed = true;
189: }
190: }
191:
192: /**
193: * Destroys any existing session on login and make sure to use a new
194: * session ID, to avoid session fixation issues. Should be called before
195: * checking a login.
196: *
197: * @return boolean True if the session was cleaned.
198: */
199: public function clean()
200: {
201: if ($this->_cleansession) {
202: return false;
203: }
204:
205: // Make sure to force a completely new session ID and clear all
206: // session data.
207: session_regenerate_id(true);
208: session_unset();
209: $_SESSION = array();
210: $this->_start();
211:
212: $this->_cleansession = true;
213:
214: return true;
215: }
216:
217: /**
218: * Close the current session.
219: */
220: public function close()
221: {
222: $this->_active = false;
223: $this->_relogin = ($GLOBALS['registry']->getAuth() !== false);
224: session_write_close();
225: }
226:
227: /**
228: * Destroy session data.
229: */
230: public function destroy()
231: {
232: session_destroy();
233: $this->_cleansession = true;
234: }
235:
236: /**
237: * Is the current session active (read/write)?
238: *
239: * @since 1.4.0
240: *
241: * @return boolean True if the current session is active.
242: */
243: public function isActive()
244: {
245: return $this->_active;
246: }
247:
248: /* Session variable access. */
249:
250: /**
251: * Does the session variable exist?
252: *
253: * @param string $app Application name.
254: * @param string $name Session variable name.
255: *
256: * @return boolean True if session variable exists.
257: */
258: public function exists($app, $name)
259: {
260: return isset($_SESSION[$app][self::NOT_SERIALIZED . $name]) ||
261: isset($_SESSION[$app][self::IS_SERIALIZED . $name]);
262: }
263:
264: /**
265: * Get the value of a session variable.
266: *
267: * @param string $app Application name.
268: * @param string $name Session variable name.
269: * @param integer $mask One of:
270: * - self::TYPE_ARRAY - Return an array value.
271: * - self::TYPE_OBJECT - Return an object value.
272: *
273: * @return mixed The value or null if the value doesn't exist.
274: */
275: public function get($app, $name, $mask = 0)
276: {
277: if (isset($_SESSION[$app][self::NOT_SERIALIZED . $name])) {
278: return $_SESSION[$app][self::NOT_SERIALIZED . $name];
279: } elseif (isset($_SESSION[$app][self::IS_SERIALIZED . $name])) {
280: $data = $_SESSION[$app][self::IS_SERIALIZED . $name];
281:
282: if ($this->_lzf &&
283: (($data = @lzf_decompress($data)) === false)) {
284: $this->remove($app, $name);
285: return $this->get($app, $name);
286: }
287:
288: return @unserialize($data);
289: }
290:
291: if ($subkeys = $this->_subkeys($app, $name)) {
292: $ret = array();
293: foreach ($subkeys as $k => $v) {
294: $ret[$k] = $this->get($app, $v, $mask);
295: }
296: return $ret;
297: }
298:
299: if (strpos($name, self::DATA) === 0) {
300: return $this->retrieve($name);
301: }
302:
303: switch ($mask) {
304: case self::TYPE_ARRAY:
305: return array();
306:
307: case self::TYPE_OBJECT:
308: return new stdClass;
309: }
310:
311: return null;
312: }
313:
314: /**
315: * Sets the value of a session variable.
316: *
317: * @param string $app Application name.
318: * @param string $name Session variable name.
319: * @param mixed $value Session variable value.
320: * @param integer $mask One of:
321: * - self::TYPE_ARRAY - Force save as an array value.
322: * - self::TYPE_OBJECT - Force save as an object value.
323: */
324: public function set($app, $name, $value, $mask = 0)
325: {
326: if ($this->_readonly) {
327: return;
328: }
329:
330: /* Each particular piece of session data is generally not used on any
331: * given page load. Thus, for arrays and objects, it is beneficial to
332: * always convert to string representations so that the object/array
333: * does not need to be rebuilt every time the session is reloaded. */
334: if (is_object($value) || ($mask & self::TYPE_OBJECT) ||
335: is_array($value) || ($mask & self::TYPE_ARRAY)) {
336: $value = serialize($value);
337: if ($this->_lzf) {
338: $value = lzf_compress($value);
339: }
340: $_SESSION[$app][self::IS_SERIALIZED . $name] = $value;
341: unset($_SESSION[$app][self::NOT_SERIALIZED . $name]);
342: } else {
343: $_SESSION[$app][self::NOT_SERIALIZED . $name] = $value;
344: unset($_SESSION[$app][self::IS_SERIALIZED . $name]);
345: }
346:
347: $this->sessionHandler->changed = true;
348: }
349:
350: /**
351: * Remove session key(s).
352: *
353: * @param string $app Application name.
354: * @param string $name Session variable name.
355: */
356: public function remove($app, $name = null)
357: {
358: if ($this->_readonly) {
359: return;
360: }
361:
362: if (!isset($_SESSION[$app])) {
363: return;
364: }
365:
366: if (is_null($name)) {
367: unset($_SESSION[$app]);
368: } elseif ($this->exists($app, $name)) {
369: unset(
370: $_SESSION[$app][self::NOT_SERIALIZED . $name],
371: $_SESSION[$app][self::IS_SERIALIZED . $name],
372: $_SESSION[self::PRUNE][$this->_getKey($app, $name)]
373: );
374: } else {
375: foreach ($this->_subkeys($app, $name) as $val) {
376: $this->remove($app, $val);
377: }
378: }
379: }
380:
381: /**
382: * Generates the unique storage key.
383: *
384: * @param string $app Application name.
385: * @param string $name Session variable name.
386: *
387: * @return string The unique storage key.
388: */
389: private function _getKey($app, $name)
390: {
391: return $app . ':' . $name;
392: }
393:
394: /**
395: * Return the list of subkeys for a master key.
396: *
397: * @param string $app Application name.
398: * @param string $name Session variable name.
399: *
400: * @return array Subkeyname (keys) and session variable name (values).
401: */
402: private function _subkeys($app, $name)
403: {
404: $ret = array();
405:
406: if ($name &&
407: isset($_SESSION[$app]) &&
408: ($name[strlen($name) - 1] == '/')) {
409: foreach (array_keys($_SESSION[$app]) as $k) {
410: if (strpos($k, $name) === 1) {
411: $ret[substr($k, strlen($name) + 1)] = substr($k, 1);
412: }
413: }
414: }
415:
416: return $ret;
417: }
418:
419: /* Session object storage. */
420:
421: /**
422: * Store an arbitrary piece of data in the session.
423: *
424: * @param mixed $data Data to save.
425: * @param boolean $prune Is data pruneable?
426: * @param string $id ID to use (otherwise, is autogenerated).
427: *
428: * @return string The session storage id (used to retrieve session data).
429: */
430: public function store($data, $prune = true, $id = null)
431: {
432: $id = is_null($id)
433: ? strval(new Horde_Support_Randomid())
434: : $this->_getStoreId($id);
435:
436: $this->set(self::DATA, $id, $data);
437:
438: if ($prune) {
439: $ptr = &$_SESSION[self::PRUNE];
440: unset($ptr[$id]);
441: $ptr[$id] = 1;
442: if (count($ptr) > $this->maxStore) {
443: array_shift($ptr);
444: }
445: }
446:
447: return $this->_getKey(self::DATA, $id);
448: }
449:
450: /**
451: * Retrieve data from the session data store (created via store()).
452: *
453: * @param string $id The session data ID.
454: *
455: * @return mixed The session data value.
456: */
457: public function retrieve($id)
458: {
459: return $this->get(self::DATA, $this->_getStoreId($id));
460: }
461:
462: /**
463: * Purge data from the session data store (created via store()).
464: *
465: * @param string $id The session data ID.
466: */
467: public function purge($id)
468: {
469: $this->remove(self::DATA, $this->_getStoreId($id));
470: }
471:
472: /**
473: * Returns the base storage ID.
474: *
475: * @param string $id The session data ID.
476: *
477: * @return string The base storage ID (without prefix).
478: */
479: private function _getStoreId($id)
480: {
481: $id = trim($id);
482:
483: if (strpos($id, self::DATA) === 0) {
484: $id = substr($id, strlen(self::DATA) + 1);
485: }
486:
487: return $id;
488: }
489:
490: }
491: