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