1<?php
2/*
3 * e107 website system
4 *
5 * Copyright (C) 2008-2012 e107 Inc (e107.org)
6 * Released under the terms and conditions of the
7 * GNU General Public License (http://www.gnu.org/licenses/gpl.txt)
8 *
9 * Session handler
10 *
11 * $URL$
12 * $Id$
13 */
14
15if (!defined('e107_INIT'))
16{
17	exit;
18}
19
20/**
21 * @package e107
22 * @subpackage	e107_handlers
23 * @version $Id$
24 * @author SecretR
25 *
26 * Dependencies:
27 * - direct: language handler
28 * - indirect: system preferences (required by language handler)
29 *
30 * What could break it?
31 * If session is started before the first system session call (see class2.php
32 * 'Start: Set User Language' phase), session config will not be applied!
33 * This could happen if included $CLASS2_INCLUDE script (see class2.php)
34 * calls session_start(). However, sessions will not be broken, just not secured
35 * as per e_SECURITY_LEVEL setting.
36 *
37 * Security levels:
38 * - SECURITY_LEVEL_NONE [0]: security disabled - no token checks, all session validation settings dsiabled
39 * - SECURITY_LEVEL_BALANCED [5]: ValidateRemoteAddr, ValidateHttpXForwardedFor are on,
40 * session token is created/checked, but not regenerated on every page load
41 * - SECURITY_LEVEL_HIGH [7]: Same as above but ValidateHttpVia, ValidateHttpUserAgent are on.
42 * - SECURITY_LEVEL_PARANOID [9]: Same as SECURITY_LEVEL_HIGH except session token is regenerated on
43 * every page load. 'httponly' is on, which means JS is unable to retrieve session cookie, this may cause
44 * troubles with some browsers.
45 * - SECURITY_LEVEL_INSANE [10]: Same as SECURITY_LEVEL_HIGH plus session id is regenerated at the end
46 * of every page request.
47 *
48 * Session objects are created by namespace:
49 * $_SESSION['e107'] is default namesapce auto created with
50 * <code><?php e107::getSession();</code>
51 * Session handler is validating corresponding session COOKIE
52 * (named as current session name, keeping the session id)
53 * on regular basis (session lifetime/4). If validation
54 * fails, corresponding cookie is destroyed (not the session itself).
55 *
56 * Initial system Session is started after language detection (see class2.php) to
57 * ensure proper session handling for sites using language sub-domains (e.g. fr.site.com)
58 *
59 * Some important system session data will be kept outside of the object for now (e.g. user validation data)
60 *
61 */
62
63
64class e_session
65{
66	/**
67	 * No protection, label 'Looking for trouble'
68	 * @var integer
69	 */
70	const SECURITY_LEVEL_NONE = 0;
71
72	/**
73	 * Default system protection, balanced for best user experience,
74	 * label 'Safe mode - Balanced'
75	 * @var integer
76	 */
77	const SECURITY_LEVEL_BALANCED = 5;
78
79	/**
80	 * Adds more system security, but there is a chance (minimal) to break stuff,
81	 * label 'High Security'
82	 * @var integer
83	 */
84	const SECURITY_LEVEL_HIGH = 7;
85
86	/**
87	 * High system protection, session id is regenerated on every page request,
88	 * label 'Paranoid'
89	 * @var integer
90	 */
91	const SECURITY_LEVEL_PARANOID = 9;
92
93	/**
94	 * Highest system protection, session id and token values are regenerated on every page request,
95	 * label 'Insane'
96	 * @var int unknown_type
97	 */
98	const SECURITY_LEVEL_INSANE = 10;
99
100	/**
101	 * Session save path
102	 * @var string
103	 */
104	protected $_sessionSavePath = false;
105
106	/**
107	 * Session save method
108	 * @var string files|db
109	 */
110	protected $_sessionSaveMethod = 'files';//'files';
111
112	/**
113	 * Session cache limiter, ignored if empty
114	 * php.net/manual/en/function.session-cache-limiter.php
115	 * @var string public|private_no_expire|private|nocache
116	 */
117	protected $_sessionCacheLimiter = '';
118
119	protected $_namespace;
120	protected $_name;
121	protected $_sessionStarted = false; // Fixes lost $_SESSION value problem.
122
123	/**
124	 * Validation options
125	 * @var boolean
126	 */
127	protected $_sessionValidateRemoteAddr = true;
128	protected $_sessionValidateHttpVia = true;
129	protected $_sessionValidateHttpXForwardedFor = true;
130	protected $_sessionValidateHttpUserAgent = true;
131
132	/**
133	 * Skip validation
134	 * @var array
135	 */
136	protected $_sessionValidateRemoteAddrSkip = array();
137	protected $_sessionValidateHttpViaSkip = array();
138	protected $_sessionValidateHttpXForwardedForSkip = array();
139	protected $_sessionValidateHttpUserAgentSkip = array();
140
141	/**
142	 * Default session options
143	 * @var array
144	 */
145	protected $_options = array(
146		'lifetime'	 => 3600 , // 1 hour
147		'path'		 => '',
148		'domain'	 => '',
149		'secure'	 => false,
150		'httponly'	 => true,
151	);
152
153	/**
154	 * Session data
155	 * @var array
156	 */
157	protected $_data = array();
158
159	/**
160	 * Set session options
161	 * @param string $key
162	 * @param mixed $value
163	 * @return e_session
164	 */
165	public function setOption($key, $value)
166	{
167		$this->setOptions(array($key => $value));
168		return $this;
169	}
170
171	public function getOptions()
172	{
173		return $this->_options;
174	}
175
176
177
178	/**
179	 * Get session option
180	 * @param string $key
181	 * @param mixed $default
182	 * @return mixed value
183	 */
184	public function getOption($key, $default = null)
185	{
186		return (isset($this->_options[$key]) ? $this->_options[$key] : $default);
187	}
188
189	/**
190	 * Set default settings/options based on the current security level
191	 * NOTE: new prefs 'session_save_path', 'session_save_method', 'session_lifetime' introduced,
192	 * still not added to preference administration
193	 * @return e_session
194	 */
195	public function setDefaultSystemConfig()
196	{
197        if ($this->getSessionId()) return $this;
198
199        $config = array(
200            'ValidateRemoteAddr' => (e_SECURITY_LEVEL >= self::SECURITY_LEVEL_BALANCED),
201            'ValidateHttpVia' => (e_SECURITY_LEVEL >= self::SECURITY_LEVEL_HIGH),
202            'ValidateHttpXForwardedFor' => (e_SECURITY_LEVEL >= self::SECURITY_LEVEL_BALANCED),
203            'ValidateHttpUserAgent' => (e_SECURITY_LEVEL >= self::SECURITY_LEVEL_HIGH),
204        );
205
206        $options = array(
207            //		'httponly' => (e_SECURITY_LEVEL >= self::SECURITY_LEVEL_PARANOID),
208            'httponly' => true,
209        );
210
211        if (!defined('E107_INSTALL'))
212        {
213            $systemSaveMethod = ini_get('session.save_handler');
214
215            $saveMethod = (!empty($systemSaveMethod)) ? $systemSaveMethod : 'files';
216
217            $config['SavePath']     = e107::getPref('session_save_path', false); // FIXME - new pref
218            $config['SaveMethod']   = e107::getPref('session_save_method', $saveMethod);
219            $options['lifetime']    = (integer)e107::getPref('session_lifetime', 86400);
220            $options['path']        = e107::getPref('session_cookie_path', ''); // FIXME - new pref
221            $options['secure']      = e107::getPref('ssl_enabled', false); //
222
223            e107::getDebug()->log("Session Save Method: ".$config['SaveMethod']);
224
225            if (!empty($options['secure']))
226            {
227                ini_set('session.cookie_secure', 1);
228            }
229        }
230
231        if (defined('SESSION_SAVE_PATH')) // safer than a pref.
232        {
233            $config['SavePath'] = e_BASE . SESSION_SAVE_PATH;
234        }
235
236        $hashes = hash_algos();
237
238        if ((e_SECURITY_LEVEL >= self::SECURITY_LEVEL_BALANCED) && in_array('sha512', $hashes))
239        {
240            ini_set('session.hash_function', 'sha512');
241            ini_set('session.hash_bits_per_character', 5);
242        }
243
244        $this->fixSessionFileGarbageCollection();
245
246        $this->setConfig($config)
247            ->setOptions($options);
248
249        return $this;
250	}
251
252    /**
253     * Modify PHP ini at runtime to enable session file garbage collection
254     *
255     * Takes no action if the garbage collector is already enabled.
256     *
257     * @see https://github.com/e107inc/e107/issues/4113
258     * @return void
259     */
260	private function fixSessionFileGarbageCollection()
261    {
262        $gc_probability = ini_get('session.gc_probability');
263        if ($gc_probability > 0) return;
264
265        ini_set('session.gc_probability', 1);
266        ini_set('session.gc_divisor', 100);
267    }
268
269	/**
270	 * Retrieve value from current session namespace
271	 * Equals to $_SESSION[NAMESPACE][$key]
272	 * @param string $key
273	 * @param boolean $clear unset key
274	 * @return mixed
275	 */
276	public function get($key, $clear = false)
277	{
278		$ret = isset($this->_data[$key]) ? $this->_data[$key] : null;
279		if($clear) $this->clear($key);
280		return $ret;
281	}
282
283	/**
284	 * Retrieve value from current session namespace
285	 * If key is null, returns all current session namespace data
286	 *
287	 * @param string|null $key
288	 * @param boolean $clear
289	 * @return mixed
290	 */
291	public function getData($key = null, $clear = false)
292	{
293		if(null === $key)
294		{
295			$ret = $this->_data;
296			if($clear) $this->clearData();
297			return $ret;
298		}
299		return $this->get($key, $clear);
300	}
301
302	/**
303	 * Set value in current session namespace
304	 * Equals to $_SESSION[NAMESPACE][$key] = $value
305	 * @param string $key
306	 * @param mixed $value
307	 * @return e_session
308	 */
309	public function set($key, $value)
310	{
311		$this->_data[$key] = $value;
312		return $this;
313	}
314
315	/**
316	 * Set value in current session namespace
317	 * If $key is array, the whole namespace array will be replaced with it,
318	 * $value will be ignored
319	 * @param string|null $key
320	 * @param mixed $value
321	 * @return e_session
322	 */
323	public function setData($key, $value = null)
324	{
325		if(is_array($key))
326		{
327			$this->_data = $key;
328			return $this;
329		}
330		return $this->set($key, $value);
331	}
332
333	/**
334	 * Check if given key is set in current session namespace
335	 * Equals to isset($_SESSION[NAMESPACE][$key])
336	 * @param string $key
337	 * @return boolean
338	 */
339	public function is($key)
340	{
341		return isset($this->_data[$key]);
342	}
343
344	/**
345	 * Check if given key is set and not empty in current session namespace
346	 * Equals to !empty($_SESSION[NAMESPACE][$key]) check
347	 * @param string $key
348	 * @return boolean
349	 */
350	public function has($key)
351	{
352		return (isset($this->_data[$key]) && $this->_data[$key]);
353	}
354
355	/**
356	 * Checks if current session namespace contains any data
357	 * Equals to !empty($_SESSION[NAMESPACE]) check
358	 * @return boolean
359	 */
360	public function hasData()
361	{
362		return !empty($this->_data);
363	}
364
365	/**
366	 * Unset member of current session namespace array
367	 * Equals to unset($_SESSION[NAMESPACE][$key])
368	 * @param string $key
369	 * @return e_session
370	 */
371	public function clear($key=null)
372	{
373		if($key == null) // clear all under this namespace.
374		{
375			$this->_data = array(); // must be set to array() not unset.
376		}
377
378		unset($this->_data[$key]);
379		return $this;
380	}
381
382	/**
383	 * Reset current session namespace to empty array
384	 * @return e_session
385	 */
386	public function clearData()
387	{
388		$this->_data = array();
389		return $this;
390	}
391
392	/**
393	 * Set protected class vars, prefixed with _session
394	 * @param array $config
395	 * @return e_session
396	 */
397	public function setConfig($config)
398	{
399		foreach ($config as $k => $v)
400		{
401			$key = '_session'.$k;
402			if (isset($this->$key)) $this->$key = $v;
403		}
404		return $this;
405	}
406
407	/**
408	 * Get registered namespace key
409	 * @return string
410	 */
411	public function getNamespaceKey()
412	{
413		return $this->_namespace;
414	}
415
416	/**
417	 * Reset session options
418	 * @param array $options
419	 * @return e_session
420	 */
421	public function setOptions($options)
422	{
423		if (empty($options) || !is_array($options)) return $this;
424		foreach ($options as $k => $v)
425		{
426			switch ($k)
427			{
428				case 'lifetime':
429					$v = intval($v);
430				break;
431
432				case 'path':
433				case 'domain':
434					$v = (string) $v;
435				break;
436
437				case 'secure':
438				case 'httponly':
439					$v = $v ? true : false;
440				break;
441
442				default:
443					$v = null;
444				break;
445			}
446
447			if($v !== null)
448			{
449				$this->_options[$k] = $v;
450			}
451		}
452		return $this;
453	}
454
455	public function init($namespace, $sessionName = null)
456	{
457		$this->start($sessionName);
458
459		if (!isset($_SESSION[$namespace]))
460		{
461			$_SESSION[$namespace] = array();
462		}
463		$this->_data =& $_SESSION[$namespace];
464		$this->_namespace = $namespace;
465
466		$this->validate();
467		$this->validateSessionCookie();
468	}
469
470	/**
471	 * Conigure and start session
472	 *
473	 * @param string $sessionName optional session name
474	 * @return e_session
475	 */
476	public function start($sessionName = null)
477	{
478
479		if (isset($_SESSION) && ($this->_sessionStarted == true))
480		{
481			return $this;
482		}
483
484		if (false !== $this->_sessionSavePath && is_writable($this->_sessionSavePath))
485		{
486			session_save_path($this->_sessionSavePath);
487		}
488
489		switch ($this->_sessionSaveMethod)
490		{
491			case 'db':
492				ini_set('session.save_handler', 'user');
493				$session = new e_session_db;
494				$session->setSaveHandler();
495			break;
496
497			default:
498				if(!isset($_SESSION))
499				{
500					session_module_name($this->_sessionSaveMethod);
501				}
502			break;
503		}
504
505		if (empty($this->_options['domain']))
506		{
507			// MULTILANG_SUBDOMAIN set during initial language detection in language handler
508			$doma = ((deftrue('e_SUBDOMAIN') || deftrue('MULTILANG_SUBDOMAIN')) && e_DOMAIN != FALSE) ? ".".e_DOMAIN : FALSE; // from v1.x
509			$this->_options['domain'] = $doma;
510		}
511
512		if (empty($this->_options['path']))
513		{
514			if(defined('e_MULTISITE_MATCH')) // multisite support.
515			{
516				$this->_options['path'] = '/';
517			}
518			else
519			{
520				$this->_options['path'] = defined('e_HTTP') ? e_HTTP : '/';
521			}
522		}
523
524		// session name before options - problems reported on php.net
525		if (!empty($sessionName))
526		{
527			$this->setSessionName($sessionName);
528		}
529
530		// set session cookie params
531		session_set_cookie_params($this->_options['lifetime'],
532			$this->_options['path'],
533			$this->_options['domain'],
534			$this->_options['secure'],
535			$this->_options['httponly']);
536
537		if ($this->_sessionCacheLimiter)
538		{
539			session_cache_limiter((string) $this->_sessionCacheLimiter); //XXX Remove and have e_headers class handle it?
540		}
541
542
543		session_start();
544		$this->_sessionStarted = true;
545		return $this;
546	}
547
548	/**
549	 * Set session ID
550	 * @param string $sid
551	 * @return e_session
552	 */
553	public function setSessionId($sid = null)
554	{
555		// comma and minus allowed since 5.0
556		if (!empty($sid) && preg_match('#^[0-9a-zA-Z,-]+$#', $sid))
557		{
558			session_id($sid);
559		}
560		return $this;
561	}
562
563	/**
564	 * Retrieve current session id
565	 * @return string
566	 */
567	public function getSessionId()
568	{
569		return session_id();
570	}
571
572	/**
573	 * Retrieve current session save method.
574	 * @return string
575	 */
576	public function getSaveMethod()
577	{
578		return $this->_sessionSaveMethod;
579	}
580
581	/**
582	 * Set new session name
583	 * @param string $name alphanumeric characters only
584	 * @return string old session name or false on error
585	 */
586	public function setSessionName($name)
587	{
588		if (!empty($name) && preg_match('#^[0-9a-z_]+$#i', $name))
589		{
590			$this->_name = $name;
591			return session_name($name);
592		}
593		return false;
594	}
595
596	/**
597	 * Retrieve current session name
598	 * @return string
599	 */
600	public function getSessionName()
601	{
602		return session_name();
603	}
604
605	/**
606	 * Reset session cookie lifetime
607	 * We reset session cookie on every (session_lifetime / 4) seconds
608	 * It's done by all session handler instances, they all share
609	 * one and the same '_cookie_session_validate' variable (global session namespace)
610	 * @return e_session
611	 */
612	public function validateSessionCookie()
613	{
614		if (!$this->_options['lifetime'])
615		{
616			return $this;
617		}
618
619		if (empty($_SESSION['_cookie_session_validate']))
620		{
621			$time = time() + round($this->_options['lifetime'] / 4);
622			$_SESSION['_cookie_session_validate'] = $time;
623		}
624		elseif ($_SESSION['_cookie_session_validate'] < time())
625		{
626			if (!headers_sent())
627			{
628				cookie(session_name(), session_id(), time() + $this->_options['lifetime'], $this->_options['path'], $this->_options['domain'], $this->_options['secure']);
629				$time = time() + round($this->_options['lifetime'] / 4);
630				$_SESSION['_cookie_session_validate'] = $time;
631			}
632		}
633
634		return $this;
635	}
636
637	/**
638	 * Delete session cookie
639	 * @return e_session
640	 */
641	public function cookieDelete()
642	{
643		cookie(session_name(), null, null, $this->_options['path'], $this->_options['domain'], $this->_options['secure']);
644		return $this;
645	}
646
647	/**
648	 * Validate current session
649	 * @return e_session
650	 */
651	public function validate()
652	{
653		if (!isset($this->_data['_session_validate_data']))
654		{
655			$this->_data['_session_validate_data'] = $this->getValidateData();
656		}
657		elseif (!$this->_validate())
658		{
659			$sessionData = $this->_data['_session_validate_data'];
660			$validateData = $this->getValidateData();
661
662			$details = 'USER INFORMATION: '.(isset($_COOKIE[e_COOKIE]) ? $_COOKIE[e_COOKIE] : (isset($_SESSION[e_COOKIE]) ? $_SESSION[e_COOKIE] : 'n/a'))."\n";
663			$details .= "HOST: ".$_SERVER['HTTP_HOST']."\n";
664			$details .= "REQUEST_URI: ".$_SERVER['REQUEST_URI']."\n";
665			$details .= "SESSION OPTIONS: ".print_r($this->_options, true)."\n";
666			$details .= "SESSION NAMESPACE: ".$this->_namespace."\n";
667			$details .= "SESSION VALIDATION DATA SAVED: ".print_r($sessionData, true)."\n";
668			$details .= "SESSION VALIDATION DATA CURRENT: ".print_r($validateData, true)."\n";
669			$details .= "CURRENT NAMESPACE SESSION DATA:\n";
670			$this->clear('_session_validate_data'); // already logged
671			$details .= print_r($this->_data, true);
672			$this->close(false);
673			$details .= "SESSION GLOBAL DATA:\n";
674			$details .= print_r($_SESSION, true);
675
676			// delete cookie, destroy session
677			$this->cookieDelete()->destroy();
678
679			// TODO event trigger
680
681			// e107::getAdminLog()->log_event('Session validation failed!', $details, E_LOG_FATAL);
682			// TODO session exception, handle it proper on live site
683			// throw new Exception('');
684
685			// just for now
686			$msg = 'Session validation failed! <a href="'.strip_tags($_SERVER['REQUEST_URI']).'">Go Back</a>';
687		//	die($msg); //FIXME not functioning as intended.
688		}
689
690		return $this;
691	}
692
693	/**
694	 * Validate current session based on config options
695	 *
696	 * @return bool
697	 */
698	protected function _validate()
699	{
700		$sessionData = $this->_data['_session_validate_data'];
701		$validateData = $this->getValidateData();
702		$keyvar = '_sessionValidate';
703
704		foreach ($validateData as $vkey => $value)
705		{
706			$var = $keyvar.$vkey;
707			$varskip = $var.'Skip';
708			if ($this->$var && $sessionData[$vkey] != $value && !in_array($value, $this->$varskip))
709			{
710				return false;
711			}
712		}
713
714		return true;
715	}
716
717	/**
718	 * Retrieve data for validator
719	 * @return array
720	 */
721	public function getValidateData()
722	{
723		$data = array(
724			'RemoteAddr' => '',
725			'HttpVia' => '',
726			'HttpXForwardedFor' => '',
727			'HttpUserAgent' => ''
728		);
729
730		// collect ip data
731		if (isset($_SERVER['REMOTE_ADDR']))
732		{
733			$data['RemoteAddr'] = (string) $_SERVER['REMOTE_ADDR'];
734		}
735		if (isset($_ENV['HTTP_VIA']))
736		{
737			$data['HttpVia'] = (string) $_ENV['HTTP_VIA'];
738		}
739		if (isset($_ENV['HTTP_X_FORWARDED_FOR']))
740		{
741			$data['HttpXForwardedFor'] = (string) $_ENV['HTTP_X_FORWARDED_FOR'];
742		}
743
744		// collect user agent data
745		if (isset($_SERVER['HTTP_USER_AGENT']))
746		{
747			$data['HttpUserAgent'] = (string) $_SERVER['HTTP_USER_AGENT'];
748		}
749
750		return $data;
751	}
752
753	/**
754	 * Retrieve (create if doesn't exist) XSF protection token
755	 * @param boolean $in_form if true (default) - value for forms, else raw session value
756	 * @return string
757	 */
758	public function getFormToken($in_form = true)
759	{
760		if(!$this->has('__form_token') && !defined('e_TOKEN_DISABLE'))  // TODO FIXME: SEF URL of Error page causes e-token refresh.
761		{
762			$this->set('__form_token', uniqid(md5(rand()), true));
763			if(deftrue('e_DEBUG_SESSION')) // XXX enable to troubleshoot "Unauthorized Access!" issues.
764			{
765				$message = date('r')."\t\t".e_REQUEST_URI."\n";
766				file_put_contents(__DIR__.'/session.log', $message, FILE_APPEND);
767			}
768		}
769		return ($in_form ? md5($this->get('__form_token')) : $this->get('__form_token'));
770	}
771
772	/**
773	 * Regenerate form token value
774	 * TODO - save old token
775	 * @return e_session
776	 */
777	protected function _regenerateFormToken()
778	{
779		$this->set('__form_token', uniqid(md5(rand()), true));
780		return $this;
781	}
782
783	/**
784	 * Do a check against passed token
785	 * @param string $token
786	 * @return boolean
787	 */
788	public function checkFormToken($token)
789	{
790		$utoken = $this->getFormToken(false);
791		return ($token === md5($utoken));
792	}
793
794	/**
795	 * Clear and Unset current namespace, unregister session singleton
796	 * e107::getSession('namespace') if needed.
797	 * @param boolean $unregister if true (default) - unregister Singleton, destroy namespace,
798	 * 								else alias of self::clearData()
799	 * @return void
800	 */
801	public function close($unregister = true)
802	{
803		$this->clearData();
804		if($unregister)
805		{
806			unset($_SESSION[$this->_namespace]);
807			e107::setRegistry('core/e107/session/'.$this->_namespace, null);
808		}
809	}
810
811	/**
812	 * Save session data to disk, end session.
813	 * Sessions can't be used after this point.
814	 * Method should be called before every header redirect.
815	 * @return void
816	 */
817	public function end()
818	{
819		session_write_close();
820	}
821
822	/**
823	 * Destroy all session data
824	 * @return e_session
825	 */
826	public function destroy()
827	{
828		$this->cookieDelete()->close();
829		//unset($_SESSION);
830
831		// cleanup
832		cookie(e_COOKIE, null, null); // remove user auth cookie
833		// unset($_SESSION['_cookie_session_validate']);
834
835		session_destroy();
836		return $this;
837	}
838
839	public function replaceRegistry()
840	{
841		e107::setRegistry('core/e107/session/'.$this->_namespace, $this, true);
842	}
843}
844
845class e_core_session extends e_session
846{
847	/**
848	 * Constructor
849	 * 3rd party code and/or other system areas are
850	 * able to extend the base e_session class and
851	 * add more or override the implemented functionality, has their own
852	 * namespace, add more session security etc.
853	 * @param array $data session config data
854	 */
855	public function __construct($data = array())
856	{
857		// default system configuration
858		$this->setDefaultSystemConfig();
859
860		$namespace = 'e107sess'; // Quick Fix for Fatal Error "Cannot use object of type e107 as array" on line 550
861		$name = (isset($data['name']) && !empty($data['name']) ? $data['name'] : deftrue('e_COOKIE', 'e107')).'SID';
862		if(isset($data['namespace']) && !empty($data['namespace'])) $namespace = $data['namespace'];
863
864		// create $_SESSION['e107'] namespace by default
865		$this->init($namespace, $name);
866	}
867
868	/**
869	 * Session shutdown - called at the top of footer_default.php by default
870	 * @return void
871	 */
872	public function shutdown()
873	{
874		if(!session_id()) // someone closed the session?
875		{
876			$this->init($this->_namespace, $this->_name); // restart
877		}
878
879		// give 3rd party code a way to prevent token re-generation
880		if(e_SECURITY_LEVEL >= e_session::SECURITY_LEVEL_PARANOID && !deftrue('e_TOKEN_FREEZE'))
881		{
882			if(e_SECURITY_LEVEL == e_session::SECURITY_LEVEL_INSANE)
883			{
884				// regenerate SID
885				$oldSID = session_id(); // old SID
886				$oldSData = $_SESSION; // old session data
887				session_regenerate_id(false); // true don't work on php4 - so time to move on people!
888				$newSID = session_id(); // new SID
889
890				// Clean
891				session_id($oldSID); // switch to the old session
892				session_destroy(); // destroy it
893
894				// set new ID, reopen the session, set saved data
895				session_id($newSID);
896				session_start();
897				$_SESSION = $oldSData;
898			}
899			$this->set('__form_token_regenerate', time()); // check() needs it to re-create token on the next request
900		}
901		// write session data
902		$this->end();
903	}
904
905	private function log($status, $type=E_LOG_FATAL)
906	{
907
908		if(!deftrue('e_DEBUG_SESSION'))
909		{
910			return null;
911		}
912
913
914	//	$details = "USER: ".USERNAME."\n";
915		$details = "HOST: ".$_SERVER['HTTP_HOST']."\n";
916		$details .= "REQUEST_URI: ".$_SERVER['REQUEST_URI']."\n";
917
918		$details .= ($_POST['e-token']) ? "e-token (POST): ".$_POST['e-token']."\n" : "";
919		$details .= ($_GET['e-token']) ? "e-token (GET): ".$_GET['e-token']."\n" : "";
920		$details .= ($_POST['e_token']) ? "AJAX e_token (POST): ".$_POST['e_token']."\n" : "";
921/*
922		$utoken = $this->getFormToken(false);
923		$details .= "raw token: ".$utoken."\n";
924		$details .= "checkFormToken (e-token should match this): ".md5($utoken)."\n";
925		$details .= "md5(e-token): ".md5($_POST['e-token'])."\n";*/
926/*
927		$regenerate = $this->get('__form_token_regenerate');
928		$details .= "Regenerate after: ".date('r', $regenerate)." (".$regenerate.")\n";
929*/
930
931		$details .= "has __form_token: ";
932		$hasToken = $this->has('__form_token');
933		$details .= empty($hasToken) ? 'false' : 'true';
934		$details .= "\n";
935
936		$details .= "_SESSION:\n";
937		$details .= print_r($_SESSION,true);
938
939		/*	if($pref['plug_installed'])
940			{
941				$details .= "\nPlugins:\n";
942				$details .= print_r($pref['plug_installed'],true);
943			}*/
944
945		$details .= $status."\n\n---------------------------------\n\n";
946
947		$log = e107::getAdminLog();
948		$log->addDebug($details);
949
950		if(deftrue('e_DEBUG_SESSION'))
951		{
952			$log->toFile('Unauthorized_access','Unauthorized access Log', true);
953		}
954
955		$log->add($status, $details, $type);
956
957
958	}
959	/**
960	 * Core CSF protection, see class2.php
961	 * Could be adopted by plugins for their own (different) protection logic
962	 * @param boolean $die
963	 * @return boolean
964	 */
965	public function check($die = true)
966	{
967		// define('e_TOKEN_NAME', 'e107_token_'.md5($_SERVER['HTTP_HOST'].e_HTTP));
968		// TODO e-token required for all system forms?
969
970		// only if not disabled and not in 'cli' mod
971		if(e_SECURITY_LEVEL < e_session::SECURITY_LEVEL_BALANCED || e107::getE107('cli')) return true;
972
973		if($this->getSessionId())
974		{
975
976			if((isset($_POST['e-token']) && !$this->checkFormToken($_POST['e-token']))
977			|| (isset($_GET['e-token']) && !$this->checkFormToken($_GET['e-token']))
978			|| (isset($_POST['e_token']) && !$this->checkFormToken($_POST['e_token']))) // '-' is not allowed in jquery. b
979			{
980				$this->log('Unauthorized access!');
981				// do not redirect, prevent dead loop, save server resources
982				if($die == true)
983				{
984					 die('Unauthorized access!');
985				}
986
987				return false;
988			}
989
990				$this->log('Session Token Okay!', E_LOG_NOTICE);
991
992		}
993
994		if(!defined('e_TOKEN'))
995		{
996			// FREEZE token regeneration if minimal, ajax or iframe (ajax and iframe not implemented yet) request
997			$_toFreeze = (e107::getE107('minimal') || e107::getE107('ajax') || e107::getE107('iframe'));
998			if(!defined('e_TOKEN_FREEZE') && $_toFreeze)
999			{
1000				define('e_TOKEN_FREEZE', true);
1001			}
1002			// __form_token_regenerate set in footer, so if footer is not called, token will be never regenerated!
1003			if(e_SECURITY_LEVEL == e_session::SECURITY_LEVEL_INSANE && !deftrue('e_TOKEN_FREEZE') && $this->has('__form_token_regenerate'))
1004			{
1005				$this->_regenerateFormToken()
1006					->clear('__form_token_regenerate');
1007			}
1008			define('e_TOKEN', $this->getFormToken());
1009		}
1010
1011		return true;
1012	}
1013
1014
1015
1016	/**
1017	 * Manually Reset the Token.
1018	 * @see e107forum::ajaxQuickReply();
1019	 */
1020	public function reset()
1021	{
1022		$this->_regenerateFormToken()->clear('__form_token_regenerate');
1023	}
1024
1025
1026	/**
1027	 * Make sure there is unique challenge string for CHAP login
1028	 * @see class2.php
1029	 * @return e_core_session
1030
1031	 @TODO: Remove debug code
1032	 */
1033	public function challenge()
1034	{
1035		if (!$this->is('challenge'))		// TODO: Eliminate need for this
1036		{
1037			$this->set('challenge', sha1(time().rand().$this->getSessionId()));		// New challenge for next time
1038		}
1039		if ($this->is('challenge'))
1040		{
1041			$this->set('prevprevchallenge', $this->get('prevchallenge'));		// Purely for debug
1042			$this->set('prevchallenge', $this->get('challenge'));				// Need to check user login against this
1043		}
1044		else
1045		{
1046			$this->set('prevchallenge', '');									// Dummy value
1047			$this->set('prevprevchallenge', '');								// Dummy value
1048		}
1049		//$this->set('challenge', sha1(time().rand().$this->getSessionId()));		// Temporarily disabled
1050		// FIXME - session id will be regenerated if e_SECURITY_LEVEL is 'paranoid|insane' - generate  (might be OK as long as values retained)
1051
1052		//$extra_text = 'C: '.$this->get('challenge').' PC: '.$this->get('prevchallenge').' PPC: '.$this->get('prevprevchallenge');
1053		//$logfp = fopen(e_LOG.'authlog.txt', 'a+'); fwrite($logfp, strftime('%H:%M:%S').' CHAP start: '.$extra_text."\n"); fclose($logfp);
1054
1055		// could go, see _validate()
1056		$user_agent = isset($_SERVER['HTTP_USER_AGENT']) ? $_SERVER['HTTP_USER_AGENT'] : '';
1057		$ubrowser = md5('E107'.$user_agent);
1058		if (!$this->is('ubrowser'))
1059		{
1060			$this->set('ubrowser', $ubrowser);
1061		}
1062		return $this;
1063	}
1064}
1065
1066
1067class e_session_db
1068{
1069	/**
1070	 * @var e_db
1071	 */
1072	protected $_db = null;
1073
1074	/**
1075	 * Table name
1076	 * @var string
1077	 */
1078	protected $_table = 'session';
1079
1080	/**
1081	 * @var integer
1082	 */
1083	protected $_lifetime = null;
1084
1085	public function __construct()
1086	{
1087		$this->_db = e107::getDb('session');
1088	}
1089
1090	public function __destruct()
1091	{
1092		session_write_close();
1093	}
1094
1095	/**
1096	 * @return string
1097	 */
1098	public function getTable()
1099	{
1100		return $this->_table;
1101	}
1102
1103	/**
1104	 * @param string $table
1105	 * @return e_session_db
1106	 */
1107	public function setTable($table)
1108	{
1109		$this->_table = $table;
1110		return $this;
1111	}
1112
1113	/**
1114	 * @return integer
1115	 */
1116	public function getLifetime()
1117	{
1118		if(null === $this->_lifetime)
1119		{
1120			$this->_lifetime = ini_get('session.gc_maxlifetime');
1121			if(!$this->_lifetime)
1122			{
1123				$this->_lifetime = 3600;
1124			}
1125		}
1126		return (integer) $this->_lifetime;
1127	}
1128
1129	/**
1130	 * @param integer $seconds
1131	 * @return e_session_db
1132	 */
1133	public function setLifetime($seconds = null)
1134	{
1135		$this->_lifetime = $seconds;
1136		return $this;
1137	}
1138
1139	/**
1140	 * Set session save handler
1141	 * @return e_session_db
1142	 */
1143	public function setSaveHandler()
1144	{
1145		session_set_save_handler(
1146			array($this, 'open'),
1147			array($this, 'close'),
1148			array($this, 'read'),
1149			array($this, 'write'),
1150			array($this, 'destroy'),
1151			array($this, 'gc')
1152		);
1153		return $this;
1154	}
1155
1156	/**
1157	 * Open session, parameters are ignored (see e_session handler)
1158	 * @param string $save_path
1159	 * @param string $sess_name
1160	 * @return boolean
1161	 */
1162    public function open($save_path, $sess_name)
1163    {
1164        return true;
1165    }
1166
1167	/**
1168	 * Close session
1169	 * @return boolean
1170	 */
1171    public function close()
1172    {
1173    	$this->gc($this->getLifetime());
1174        return true;
1175    }
1176
1177    /**
1178     * Get session data
1179     * @param string $session_id
1180     * @return string
1181     */
1182    public function read($session_id)
1183    {
1184    	$data = false;
1185    	$check = $this->_db->select($this->getTable(), 'session_data', "session_id='".$this->_sanitize($session_id)."' AND session_expires>".time());
1186    	if($check)
1187    	{
1188    		$tmp = $this->_db->fetch();
1189    		$data = base64_decode($tmp['session_data']);
1190    	}
1191    	elseif(false !== $check)
1192    	{
1193    		$data = '';
1194    	}
1195    	return $data;
1196    }
1197
1198    /**
1199     * Write session data
1200     * @param string $session_id
1201     * @param string $session_data
1202     * @return boolean
1203     */
1204    public function write($session_id, $session_data)
1205    {
1206    	$data = array(
1207    		'data' => array(
1208	    		'session_expires' => time() + $this->getLifetime(),
1209	    		'session_data' 	  => base64_encode($session_data),
1210    		),
1211    		'_FIELD_TYPES' => array(
1212    			'session_id'		=> 'str',
1213    			'session_expires'	=> 'int',
1214    			'session_data'		=> 'str'
1215    		),
1216    		'_DEFAULT' => 'str'
1217    	);
1218    	if(!($session_id = $this->_sanitize($session_id)))
1219    	{
1220    		return false;
1221    	}
1222
1223    	$check = $this->_db->select($this->getTable(), 'session_id', "`session_id`='{$session_id}'");
1224
1225    	if($check)
1226    	{
1227    		$data['WHERE'] = "`session_id`='{$session_id}'";
1228    		if(false !== $this->_db->update($this->getTable(), $data))
1229    		{
1230    			return true;
1231    		}
1232    	}
1233    	else
1234    	{
1235    		$data['data']['session_id'] = $session_id;
1236    		if($this->_db->insert($this->getTable(), $data))
1237    		{
1238    			return true;
1239    		}
1240    	}
1241    	return false;
1242    }
1243
1244    /**
1245     * Destroy session
1246     * @param string $session_id
1247     * @return boolean
1248     */
1249    public function destroy($session_id)
1250    {
1251    	$session_id = $this->_sanitize($session_id);
1252    	$this->_db->delete($this->getTable(), "`session_id`='{$session_id}'");
1253    	return true;
1254    }
1255
1256    /**
1257     * Garbage collection
1258     * @param integer $session_maxlf ignored - see write()
1259     * @return boolean
1260     */
1261    public function gc($session_maxlf)
1262    {
1263    	$this->_db->delete($this->getTable(), '`session_expires`<'.time());
1264    	return true;
1265    }
1266
1267    /**
1268     * Allow only well formed session id string
1269     * @param string $session_id
1270     * @return string
1271     */
1272    protected function _sanitize($session_id)
1273    {
1274    	return preg_replace('#[^0-9a-zA-Z,-]#', '', $session_id);
1275    }
1276}
1277