1 <?php
2 /**
3 * Part of the Joomla Framework Session Package
4 *
5 * @copyright Copyright (C) 2005 - 2016 Open Source Matters, Inc. All rights reserved.
6 * @license GNU General Public License version 2 or later; see LICENSE
7 */
8
9 namespace Joomla\Session;
10
11 use Joomla\Event\DispatcherInterface;
12 use Joomla\Input\Input;
13
14 /**
15 * Class for managing HTTP sessions
16 *
17 * Provides access to session-state values as well as session-level
18 * settings and lifetime management methods.
19 * Based on the standard PHP session handling mechanism it provides
20 * more advanced features such as expire timeouts.
21 *
22 * @since 1.0
23 */
24 class Session implements \IteratorAggregate
25 {
26 /**
27 * Internal state.
28 * One of 'inactive'|'active'|'expired'|'destroyed'|'error'
29 *
30 * @var string
31 * @see getState()
32 * @since 1.0
33 */
34 protected $state = 'inactive';
35
36 /**
37 * Maximum age of unused session in minutes
38 *
39 * @var string
40 * @since 1.0
41 */
42 protected $expire = 15;
43
44 /**
45 * The session store object.
46 *
47 * @var Storage
48 * @since 1.0
49 */
50 protected $store = null;
51
52 /**
53 * Security policy.
54 * List of checks that will be done.
55 *
56 * Default values:
57 * - fix_browser
58 * - fix_adress
59 *
60 * @var array
61 * @since 1.0
62 */
63 protected $security = array('fix_browser');
64
65 /**
66 * Force cookies to be SSL only
67 * Default false
68 *
69 * @var boolean
70 * @since 1.0
71 */
72 protected $force_ssl = false;
73
74 /**
75 * The domain to use when setting cookies.
76 *
77 * @var mixed
78 * @since 1.0
79 * @deprecated 2.0
80 */
81 protected $cookie_domain;
82
83 /**
84 * The path to use when setting cookies.
85 *
86 * @var mixed
87 * @since 1.0
88 * @deprecated 2.0
89 */
90 protected $cookie_path;
91
92 /**
93 * Session instances container.
94 *
95 * @var Session
96 * @since 1.0
97 * @deprecated 2.0
98 */
99 protected static $instance;
100
101 /**
102 * The type of storage for the session.
103 *
104 * @var string
105 * @since 1.0
106 * @deprecated 2.0
107 */
108 protected $storeName;
109
110 /**
111 * Holds the Input object
112 *
113 * @var Input
114 * @since 1.0
115 */
116 private $input = null;
117
118 /**
119 * Holds the Dispatcher object
120 *
121 * @var DispatcherInterface
122 * @since 1.0
123 */
124 private $dispatcher = null;
125
126 /**
127 * Constructor
128 *
129 * @param string $store The type of storage for the session.
130 * @param array $options Optional parameters
131 *
132 * @since 1.0
133 */
134 public function __construct($store = 'none', array $options = array())
135 {
136 // Need to destroy any existing sessions started with session.auto_start
137 if (session_id())
138 {
139 session_unset();
140 session_destroy();
141 }
142
143 // Disable transparent sid support
144 ini_set('session.use_trans_sid', '0');
145
146 // Only allow the session ID to come from cookies and nothing else.
147 ini_set('session.use_only_cookies', '1');
148
149 // Create handler
150 $this->store = Storage::getInstance($store, $options);
151
152 $this->storeName = $store;
153
154 // Set options
155 $this->_setOptions($options);
156
157 $this->_setCookieParams();
158
159 $this->setState('inactive');
160 }
161
162 /**
163 * Magic method to get read-only access to properties.
164 *
165 * @param string $name Name of property to retrieve
166 *
167 * @return mixed The value of the property
168 *
169 * @since 1.0
170 * @deprecated 2.0 Use get methods for non-deprecated properties
171 */
172 public function __get($name)
173 {
174 if ($name === 'storeName' || $name === 'state' || $name === 'expire')
175 {
176 return $this->$name;
177 }
178 }
179
180 /**
181 * Returns the global Session object, only creating it
182 * if it doesn't already exist.
183 *
184 * @param string $handler The type of session handler.
185 * @param array $options An array of configuration options (for new sessions only).
186 *
187 * @return Session The Session object.
188 *
189 * @since 1.0
190 * @deprecated 2.0 A singleton object store will no longer be supported
191 */
192 public static function getInstance($handler, array $options = array ())
193 {
194 if (!is_object(self::$instance))
195 {
196 self::$instance = new self($handler, $options);
197 }
198
199 return self::$instance;
200 }
201
202 /**
203 * Get current state of session
204 *
205 * @return string The session state
206 *
207 * @since 1.0
208 */
209 public function getState()
210 {
211 return $this->state;
212 }
213
214 /**
215 * Get expiration time in minutes
216 *
217 * @return integer The session expiration time in minutes
218 *
219 * @since 1.0
220 */
221 public function getExpire()
222 {
223 return $this->expire;
224 }
225
226 /**
227 * Get a session token, if a token isn't set yet one will be generated.
228 *
229 * Tokens are used to secure forms from spamming attacks. Once a token
230 * has been generated the system will check the post request to see if
231 * it is present, if not it will invalidate the session.
232 *
233 * @param boolean $forceNew If true, force a new token to be created
234 *
235 * @return string The session token
236 *
237 * @since 1.0
238 */
239 public function getToken($forceNew = false)
240 {
241 $token = $this->get('session.token');
242
243 // Create a token
244 if ($token === null || $forceNew)
245 {
246 $token = $this->_createToken();
247 $this->set('session.token', $token);
248 }
249
250 return $token;
251 }
252
253 /**
254 * Method to determine if a token exists in the session. If not the
255 * session will be set to expired
256 *
257 * @param string $tCheck Hashed token to be verified
258 * @param boolean $forceExpire If true, expires the session
259 *
260 * @return boolean
261 *
262 * @since 1.0
263 */
264 public function hasToken($tCheck, $forceExpire = true)
265 {
266 // Check if a token exists in the session
267 $tStored = $this->get('session.token');
268
269 // Check token
270 if (($tStored !== $tCheck))
271 {
272 if ($forceExpire)
273 {
274 $this->setState('expired');
275 }
276
277 return false;
278 }
279
280 return true;
281 }
282
283 /**
284 * Retrieve an external iterator.
285 *
286 * @return \ArrayIterator Return an ArrayIterator of $_SESSION.
287 *
288 * @since 1.0
289 */
290 public function getIterator()
291 {
292 return new \ArrayIterator($_SESSION);
293 }
294
295 /**
296 * Get session name
297 *
298 * @return string The session name
299 *
300 * @since 1.0
301 */
302 public function getName()
303 {
304 if ($this->getState() === 'destroyed')
305 {
306 // @TODO : raise error
307 return null;
308 }
309
310 return session_name();
311 }
312
313 /**
314 * Get session id
315 *
316 * @return string The session name
317 *
318 * @since 1.0
319 */
320 public function getId()
321 {
322 if ($this->getState() === 'destroyed')
323 {
324 return null;
325 }
326
327 return session_id();
328 }
329
330 /**
331 * Get the session handlers
332 *
333 * @return array An array of available session handlers
334 *
335 * @since 1.0
336 * @deprecated 2.0 The Storage class chain will be removed
337 */
338 public static function getStores()
339 {
340 $connectors = array();
341
342 // Get an iterator and loop trough the driver classes.
343 $iterator = new \DirectoryIterator(__DIR__ . '/Storage');
344
345 foreach ($iterator as $file)
346 {
347 $fileName = $file->getFilename();
348
349 // Only load for php files.
350 if (!$file->isFile() || $file->getExtension() != 'php')
351 {
352 continue;
353 }
354
355 // Derive the class name from the type.
356 $class = str_ireplace('.php', '', '\\Joomla\\Session\\Storage\\' . ucfirst(trim($fileName)));
357
358 // If the class doesn't exist we have nothing left to do but look at the next type. We did our best.
359 if (!class_exists($class))
360 {
361 continue;
362 }
363
364 // Sweet! Our class exists, so now we just need to know if it passes its test method.
365 if ($class::isSupported())
366 {
367 // Connector names should not have file extensions.
368 $connectors[] = str_ireplace('.php', '', $fileName);
369 }
370 }
371
372 return $connectors;
373 }
374
375 /**
376 * Shorthand to check if the session is active
377 *
378 * @return boolean
379 *
380 * @since 1.0
381 */
382 public function isActive()
383 {
384 return (bool) ($this->getState() == 'active');
385 }
386
387 /**
388 * Check whether this session is currently created
389 *
390 * @return boolean True on success.
391 *
392 * @since 1.0
393 */
394 public function isNew()
395 {
396 $counter = $this->get('session.counter');
397
398 return (bool) ($counter === 1);
399 }
400
401 /**
402 * Check whether this session is currently created
403 *
404 * @param Input $input Input object for the session to use.
405 * @param DispatcherInterface $dispatcher Dispatcher object for the session to use.
406 *
407 * @return void
408 *
409 * @since 1.0
410 * @deprecated 2.0 In 2.0 the DispatcherInterface should be injected via the object constructor
411 */
412 public function initialise(Input $input, DispatcherInterface $dispatcher = null)
413 {
414 $this->input = $input;
415 $this->dispatcher = $dispatcher;
416 }
417
418 /**
419 * Get data from the session store
420 *
421 * @param string $name Name of a variable
422 * @param mixed $default Default value of a variable if not set
423 * @param string $namespace Namespace to use, default to 'default' {@deprecated 2.0 Namespace support will be removed.}
424 *
425 * @return mixed Value of a variable
426 *
427 * @since 1.0
428 */
429 public function get($name, $default = null, $namespace = 'default')
430 {
431 // Add prefix to namespace to avoid collisions
432 $namespace = '__' . $namespace;
433
434 if ($this->getState() !== 'active' && $this->getState() !== 'expired')
435 {
436 // @TODO :: generated error here
437 $error = null;
438
439 return $error;
440 }
441
442 if (isset($_SESSION[$namespace][$name]))
443 {
444 return $_SESSION[$namespace][$name];
445 }
446
447 return $default;
448 }
449
450 /**
451 * Set data into the session store.
452 *
453 * @param string $name Name of a variable.
454 * @param mixed $value Value of a variable.
455 * @param string $namespace Namespace to use, default to 'default' {@deprecated 2.0 Namespace support will be removed.}
456 *
457 * @return mixed Old value of a variable.
458 *
459 * @since 1.0
460 */
461 public function set($name, $value = null, $namespace = 'default')
462 {
463 // Add prefix to namespace to avoid collisions
464 $namespace = '__' . $namespace;
465
466 if ($this->getState() !== 'active')
467 {
468 // @TODO :: generated error here
469 return null;
470 }
471
472 $old = isset($_SESSION[$namespace][$name]) ? $_SESSION[$namespace][$name] : null;
473
474 if (null === $value)
475 {
476 unset($_SESSION[$namespace][$name]);
477 }
478 else
479 {
480 $_SESSION[$namespace][$name] = $value;
481 }
482
483 return $old;
484 }
485
486 /**
487 * Check whether data exists in the session store
488 *
489 * @param string $name Name of variable
490 * @param string $namespace Namespace to use, default to 'default' {@deprecated 2.0 Namespace support will be removed.}
491 *
492 * @return boolean True if the variable exists
493 *
494 * @since 1.0
495 */
496 public function has($name, $namespace = 'default')
497 {
498 // Add prefix to namespace to avoid collisions.
499 $namespace = '__' . $namespace;
500
501 if ($this->getState() !== 'active')
502 {
503 // @TODO :: generated error here
504 return null;
505 }
506
507 return isset($_SESSION[$namespace][$name]);
508 }
509
510 /**
511 * Unset data from the session store
512 *
513 * @param string $name Name of variable
514 * @param string $namespace Namespace to use, default to 'default' {@deprecated 2.0 Namespace support will be removed.}
515 *
516 * @return mixed The value from session or NULL if not set
517 *
518 * @since 1.0
519 */
520 public function clear($name, $namespace = 'default')
521 {
522 // Add prefix to namespace to avoid collisions
523 $namespace = '__' . $namespace;
524
525 if ($this->getState() !== 'active')
526 {
527 // @TODO :: generated error here
528 return null;
529 }
530
531 $value = null;
532
533 if (isset($_SESSION[$namespace][$name]))
534 {
535 $value = $_SESSION[$namespace][$name];
536 unset($_SESSION[$namespace][$name]);
537 }
538
539 return $value;
540 }
541
542 /**
543 * Start a session.
544 *
545 * @return void
546 *
547 * @since 1.0
548 */
549 public function start()
550 {
551 if ($this->getState() === 'active')
552 {
553 return;
554 }
555
556 $this->_start();
557
558 $this->setState('active');
559
560 // Initialise the session
561 $this->_setCounter();
562 $this->_setTimers();
563
564 // Perform security checks
565 $this->_validate();
566
567 if ($this->dispatcher instanceof DispatcherInterface)
568 {
569 $this->dispatcher->triggerEvent('onAfterSessionStart');
570 }
571 }
572
573 /**
574 * Start a session.
575 *
576 * Creates a session (or resumes the current one based on the state of the session)
577 *
578 * @return boolean true on success
579 *
580 * @since 1.0
581 * @deprecated 2.0
582 */
583 protected function _start()
584 {
585 // Start session if not started
586 if ($this->getState() === 'restart')
587 {
588 session_regenerate_id(true);
589 }
590 else
591 {
592 $session_name = session_name();
593
594 // Get the Joomla\Input\Cookie object
595 $cookie = $this->input->cookie;
596
597 if (is_null($cookie->get($session_name)))
598 {
599 $session_clean = $this->input->get($session_name, false, 'string');
600
601 if ($session_clean)
602 {
603 session_id($session_clean);
604 $cookie->set($session_name, '', 1);
605 }
606 }
607 }
608
609 /**
610 * Write and Close handlers are called after destructing objects since PHP 5.0.5.
611 * Thus destructors can use sessions but session handler can't use objects.
612 * So we are moving session closure before destructing objects.
613 *
614 * Replace with session_register_shutdown() when dropping compatibility with PHP 5.3
615 */
616 register_shutdown_function('session_write_close');
617
618 session_cache_limiter('none');
619 session_start();
620
621 return true;
622 }
623
624 /**
625 * Frees all session variables and destroys all data registered to a session
626 *
627 * This method resets the $_SESSION variable and destroys all of the data associated
628 * with the current session in its storage (file or DB). It forces new session to be
629 * started after this method is called. It does not unset the session cookie.
630 *
631 * @return boolean True on success
632 *
633 * @see session_destroy()
634 * @see session_unset()
635 * @since 1.0
636 */
637 public function destroy()
638 {
639 // Session was already destroyed
640 if ($this->getState() === 'destroyed')
641 {
642 return true;
643 }
644
645 /*
646 * In order to kill the session altogether, such as to log the user out, the session id
647 * must also be unset. If a cookie is used to propagate the session id (default behavior),
648 * then the session cookie must be deleted.
649 */
650 if (isset($_COOKIE[session_name()]))
651 {
652 $this->input->cookie->set(session_name(), '', 1);
653 }
654
655 session_unset();
656 session_destroy();
657
658 $this->setState('destroyed');
659
660 return true;
661 }
662
663 /**
664 * Restart an expired or locked session.
665 *
666 * @return boolean True on success
667 *
668 * @see destroy
669 * @since 1.0
670 */
671 public function restart()
672 {
673 $this->destroy();
674
675 if ($this->getState() !== 'destroyed')
676 {
677 // @TODO :: generated error here
678 return false;
679 }
680
681 // Re-register the session handler after a session has been destroyed, to avoid PHP bug
682 $this->store->register();
683
684 $this->setState('restart');
685
686 // Regenerate session id
687 session_regenerate_id(true);
688 $this->_start();
689 $this->setState('active');
690
691 $this->_validate();
692 $this->_setCounter();
693
694 return true;
695 }
696
697 /**
698 * Create a new session and copy variables from the old one
699 *
700 * @return boolean $result true on success
701 *
702 * @since 1.0
703 */
704 public function fork()
705 {
706 if ($this->getState() !== 'active')
707 {
708 // @TODO :: generated error here
709 return false;
710 }
711
712 // Keep session config
713 $cookie = session_get_cookie_params();
714
715 // Kill session
716 session_destroy();
717
718 // Re-register the session store after a session has been destroyed, to avoid PHP bug
719 $this->store->register();
720
721 // Restore config
722 session_set_cookie_params($cookie['lifetime'], $cookie['path'], $cookie['domain'], $cookie['secure'], true);
723
724 // Restart session with new id
725 session_regenerate_id(true);
726 session_start();
727
728 return true;
729 }
730
731 /**
732 * Writes session data and ends session
733 *
734 * Session data is usually stored after your script terminated without the need
735 * to call JSession::close(), but as session data is locked to prevent concurrent
736 * writes only one script may operate on a session at any time. When using
737 * framesets together with sessions you will experience the frames loading one
738 * by one due to this locking. You can reduce the time needed to load all the
739 * frames by ending the session as soon as all changes to session variables are
740 * done.
741 *
742 * @return void
743 *
744 * @see session_write_close()
745 * @since 1.0
746 */
747 public function close()
748 {
749 session_write_close();
750 }
751
752 /**
753 * Set the session expiration
754 *
755 * @param integer $expire Maximum age of unused session in minutes
756 *
757 * @return $this
758 *
759 * @since 1.3.0
760 */
761 protected function setExpire($expire)
762 {
763 $this->expire = $expire;
764
765 return $this;
766 }
767
768 /**
769 * Set the session state
770 *
771 * @param string $state Internal state
772 *
773 * @return $this
774 *
775 * @since 1.3.0
776 */
777 protected function setState($state)
778 {
779 $this->state = $state;
780
781 return $this;
782 }
783
784 /**
785 * Set session cookie parameters
786 *
787 * @return void
788 *
789 * @since 1.0
790 * @deprecated 2.0
791 */
792 protected function _setCookieParams()
793 {
794 $cookie = session_get_cookie_params();
795
796 if ($this->force_ssl)
797 {
798 $cookie['secure'] = true;
799 }
800
801 if ($this->cookie_domain)
802 {
803 $cookie['domain'] = $this->cookie_domain;
804 }
805
806 if ($this->cookie_path)
807 {
808 $cookie['path'] = $this->cookie_path;
809 }
810
811 session_set_cookie_params($cookie['lifetime'], $cookie['path'], $cookie['domain'], $cookie['secure'], true);
812 }
813
814 /**
815 * Create a token-string
816 *
817 * @param integer $length Length of string
818 *
819 * @return string Generated token
820 *
821 * @since 1.0
822 * @deprecated 2.0 Use createToken instead
823 */
824 protected function _createToken($length = 32)
825 {
826 return $this->createToken($length);
827 }
828
829 /**
830 * Create a token-string
831 *
832 * @param integer $length Length of string
833 *
834 * @return string Generated token
835 *
836 * @since 1.3.1
837 */
838 protected function createToken($length = 32)
839 {
840 return bin2hex(random_bytes($length));
841 }
842
843 /**
844 * Set counter of session usage
845 *
846 * @return boolean True on success
847 *
848 * @since 1.0
849 * @deprecated 2.0 Use setCounter instead
850 */
851 protected function _setCounter()
852 {
853 return $this->setCounter();
854 }
855
856 /**
857 * Set counter of session usage
858 *
859 * @return boolean True on success
860 *
861 * @since 1.3.0
862 */
863 protected function setCounter()
864 {
865 $counter = $this->get('session.counter', 0);
866 ++$counter;
867
868 $this->set('session.counter', $counter);
869
870 return true;
871 }
872
873 /**
874 * Set the session timers
875 *
876 * @return boolean True on success
877 *
878 * @since 1.0
879 * @deprecated 2.0 Use setTimers instead
880 */
881 protected function _setTimers()
882 {
883 return $this->setTimers();
884 }
885
886 /**
887 * Set the session timers
888 *
889 * @return boolean True on success
890 *
891 * @since 1.3.0
892 */
893 protected function setTimers()
894 {
895 if (!$this->has('session.timer.start'))
896 {
897 $start = time();
898
899 $this->set('session.timer.start', $start);
900 $this->set('session.timer.last', $start);
901 $this->set('session.timer.now', $start);
902 }
903
904 $this->set('session.timer.last', $this->get('session.timer.now'));
905 $this->set('session.timer.now', time());
906
907 return true;
908 }
909
910 /**
911 * Set additional session options
912 *
913 * @param array $options List of parameter
914 *
915 * @return boolean True on success
916 *
917 * @since 1.0
918 * @deprecated 2.0 Use setOptions instead
919 */
920 protected function _setOptions(array $options)
921 {
922 return $this->setOptions($options);
923 }
924
925 /**
926 * Set additional session options
927 *
928 * @param array $options List of parameter
929 *
930 * @return boolean True on success
931 *
932 * @since 1.3.0
933 */
934 protected function setOptions(array $options)
935 {
936 // Set name
937 if (isset($options['name']))
938 {
939 session_name(md5($options['name']));
940 }
941
942 // Set id
943 if (isset($options['id']))
944 {
945 session_id($options['id']);
946 }
947
948 // Set expire time
949 if (isset($options['expire']))
950 {
951 $this->setExpire($options['expire']);
952 }
953
954 // Get security options
955 if (isset($options['security']))
956 {
957 $this->security = explode(',', $options['security']);
958 }
959
960 if (isset($options['force_ssl']))
961 {
962 $this->force_ssl = (bool) $options['force_ssl'];
963 }
964
965 if (isset($options['cookie_domain']))
966 {
967 $this->cookie_domain = $options['cookie_domain'];
968 }
969
970 if (isset($options['cookie_path']))
971 {
972 $this->cookie_path = $options['cookie_path'];
973 }
974
975 // Sync the session maxlifetime
976 ini_set('session.gc_maxlifetime', $this->getExpire());
977
978 return true;
979 }
980
981 /**
982 * Do some checks for security reason
983 *
984 * - timeout check (expire)
985 * - ip-fixiation
986 * - browser-fixiation
987 *
988 * If one check failed, session data has to be cleaned.
989 *
990 * @param boolean $restart Reactivate session
991 *
992 * @return boolean True on success
993 *
994 * @see http://shiflett.org/articles/the-truth-about-sessions
995 * @since 1.0
996 * @deprecated 2.0 Use validate instead
997 */
998 protected function _validate($restart = false)
999 {
1000 return $this->validate($restart);
1001 }
1002
1003 /**
1004 * Do some checks for security reason
1005 *
1006 * - timeout check (expire)
1007 * - ip-fixiation
1008 * - browser-fixiation
1009 *
1010 * If one check failed, session data has to be cleaned.
1011 *
1012 * @param boolean $restart Reactivate session
1013 *
1014 * @return boolean True on success
1015 *
1016 * @see http://shiflett.org/articles/the-truth-about-sessions
1017 * @since 1.3.0
1018 */
1019 protected function validate($restart = false)
1020 {
1021 // Allow to restart a session
1022 if ($restart)
1023 {
1024 $this->setState('active');
1025
1026 $this->set('session.client.address', null);
1027 $this->set('session.client.forwarded', null);
1028 $this->set('session.token', null);
1029 }
1030
1031 // Check if session has expired
1032 if ($this->getExpire())
1033 {
1034 $curTime = $this->get('session.timer.now', 0);
1035 $maxTime = $this->get('session.timer.last', 0) + $this->getExpire();
1036
1037 // Empty session variables
1038 if ($maxTime < $curTime)
1039 {
1040 $this->setState('expired');
1041
1042 return false;
1043 }
1044 }
1045
1046 $remoteAddr = $this->input->server->getString('REMOTE_ADDR', '');
1047
1048 // Check for client address
1049 if (in_array('fix_adress', $this->security) && !empty($remoteAddr) && filter_var($remoteAddr, FILTER_VALIDATE_IP) !== false)
1050 {
1051 $ip = $this->get('session.client.address');
1052
1053 if ($ip === null)
1054 {
1055 $this->set('session.client.address', $remoteAddr);
1056 }
1057 elseif ($remoteAddr !== $ip)
1058 {
1059 $this->setState('error');
1060
1061 return false;
1062 }
1063 }
1064
1065 $xForwardedFor = $this->input->server->getString('HTTP_X_FORWARDED_FOR', '');
1066
1067 // Record proxy forwarded for in the session in case we need it later
1068 if (!empty($xForwardedFor) && filter_var($xForwardedFor, FILTER_VALIDATE_IP) !== false)
1069 {
1070 $this->set('session.client.forwarded', $xForwardedFor);
1071 }
1072
1073 return true;
1074 }
1075 }
1076