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