3 +--------------------------------------------------------------------+
5 +--------------------------------------------------------------------+
6 | Copyright CiviCRM LLC (c) 2004-2019 |
7 +--------------------------------------------------------------------+
8 | This file is a part of CiviCRM. |
10 | CiviCRM is free software; you can copy, modify, and distribute it |
11 | under the terms of the GNU Affero General Public License |
12 | Version 3, 19 November 2007 and the CiviCRM Licensing Exception. |
14 | CiviCRM is distributed in the hope that it will be useful, but |
15 | WITHOUT ANY WARRANTY; without even the implied warranty of |
16 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. |
17 | See the GNU Affero General Public License for more details. |
19 | You should have received a copy of the GNU Affero General Public |
20 | License and the CiviCRM Licensing Exception along |
21 | with this program; if not, contact CiviCRM LLC |
22 | at info[AT]civicrm[DOT]org. If you have questions about the |
23 | GNU Affero General Public License or the licensing of CiviCRM, |
24 | see the CiviCRM license FAQ at http://civicrm.org/licensing |
25 +--------------------------------------------------------------------+
29 * Class CRM_Core_Session.
31 class CRM_Core_Session
{
34 * Cache of all the session names that we manage.
37 public static $_managedNames = NULL;
40 * Key is used to allow the application to have multiple top
41 * level scopes rather than a single scope. (avoids naming
42 * conflicts). We also extend this idea further and have local
43 * scopes within a global scope. Allows us to do cool things
44 * like resetting a specific area of the session code while
45 * keeping the rest intact
49 protected $_key = 'CiviCRM';
50 const USER_CONTEXT
= 'userContext';
53 * This is just a reference to the real session. Allows us to
54 * debug this class a wee bit easier
58 protected $_session = NULL;
61 * We only need one instance of this object. So we use the singleton
62 * pattern and cache the instance in this variable
66 static private $_singleton = NULL;
71 * The CMS takes care of initiating the php session handler session_start().
73 * All crm code should always use the session using
74 * CRM_Core_Session. we prefix stuff to avoid collisions with the CMS and also
75 * collisions with other crm modules!
77 * This constructor is invoked whenever any module requests an instance of
78 * the session and one is not available.
80 * @return CRM_Core_Session
82 public function __construct() {
83 $this->_session
= NULL;
87 * Singleton function used to manage this object.
89 * @return CRM_Core_Session
91 public static function &singleton() {
92 if (self
::$_singleton === NULL) {
93 self
::$_singleton = new CRM_Core_Session();
95 return self
::$_singleton;
99 * Creates an array in the session.
101 * All variables now will be stored under this array.
103 * @param bool $isRead
104 * Is this a read operation, in this case, the session will not be touched.
106 public function initialize($isRead = FALSE) {
107 // lets initialize the _session variable just before we need it
108 // hopefully any bootstrapping code will actually load the session from the CMS
109 if (!isset($this->_session
)) {
111 if (!isset($_SESSION) && PHP_SAPI
!== 'cli') {
115 // FIXME: This belongs in CRM_Utils_System_*
116 if (CRM_Core_Config
::singleton()->userSystem
->is_drupal
&& function_exists('drupal_session_start')) {
117 // https://issues.civicrm.org/jira/browse/CRM-14356
118 if (!(isset($GLOBALS['lazy_session']) && $GLOBALS['lazy_session'] == TRUE)) {
119 drupal_session_start();
127 $this->_session
=& $_SESSION;
134 if (!isset($this->_session
[$this->_key
]) ||
135 !is_array($this->_session
[$this->_key
])
137 $this->_session
[$this->_key
] = [];
142 * Resets the session store.
146 public function reset($all = 1) {
150 // to make certain we clear it, first initialize it to empty
151 $this->_session
[$this->_key
] = [];
152 unset($this->_session
[$this->_key
]);
155 $this->_session
= [];
161 * Creates a session local scope.
163 * @param string $prefix
165 * @param bool $isRead
166 * Is this a read operation, in this case, the session will not be touched.
168 public function createScope($prefix, $isRead = FALSE) {
169 $this->initialize($isRead);
171 if ($isRead ||
empty($prefix)) {
175 if (empty($this->_session
[$this->_key
][$prefix])) {
176 $this->_session
[$this->_key
][$prefix] = [];
181 * Resets the session local scope.
183 * @param string $prefix
186 public function resetScope($prefix) {
189 if (empty($prefix)) {
193 if (array_key_exists($prefix, $this->_session
[$this->_key
])) {
194 unset($this->_session
[$this->_key
][$prefix]);
199 * Store the variable with the value in the session scope.
201 * This function takes a name, value pair and stores this
202 * in the session scope. Not sure what happens if we try
203 * to store complex objects in the session. I suspect it
204 * is supported but we need to verify this
207 * @param string $name
208 * Name of the variable.
209 * @param mixed $value
210 * Value of the variable.
211 * @param string $prefix
212 * A string to prefix the keys in the session with.
214 public function set($name, $value = NULL, $prefix = NULL) {
215 // create session scope
216 $this->createScope($prefix);
218 if (empty($prefix)) {
219 $session = &$this->_session
[$this->_key
];
222 $session = &$this->_session
[$this->_key
][$prefix];
225 if (is_array($name)) {
226 foreach ($name as $n => $v) {
231 $session[$name] = $value;
236 * Gets the value of the named variable in the session scope.
238 * This function takes a name and retrieves the value of this
239 * variable from the session scope.
242 * @param string $name
243 * name of the variable.
244 * @param string $prefix
245 * adds another level of scope to the session.
249 public function get($name, $prefix = NULL) {
250 // create session scope
251 $this->createScope($prefix, TRUE);
253 if (empty($this->_session
) ||
empty($this->_session
[$this->_key
])) {
257 if (empty($prefix)) {
258 $session =& $this->_session
[$this->_key
];
261 if (empty($this->_session
[$this->_key
][$prefix])) {
264 $session =& $this->_session
[$this->_key
][$prefix];
267 return CRM_Utils_Array
::value($name, $session);
271 * Gets all the variables in the current session scope and stuffs them in an associate array.
274 * Associative array to store name/value pairs.
275 * @param string $prefix
276 * Will be stripped from the key before putting it in the return.
278 public function getVars(&$vars, $prefix = '') {
279 // create session scope
280 $this->createScope($prefix, TRUE);
282 if (empty($prefix)) {
283 $values = &$this->_session
[$this->_key
];
286 $values = Civi
::cache('session')->get("CiviCRM_{$prefix}");
290 foreach ($values as $name => $value) {
291 $vars[$name] = $value;
297 * Set and check a timer.
299 * If it's expired, it will be set again.
301 * Good for showing a message to the user every hour or day (so not bugging them on every page)
302 * Returns true-ish values if the timer is not set or expired, and false if the timer is still running
303 * If you want to get more nuanced, you can check the type of the return to see if it's 'not set' or actually expired at a certain time
306 * @param string $name
309 * expiry time (in seconds).
313 public function timer($name, $expire) {
314 $ts = $this->get($name, 'timer');
315 if (!$ts ||
$ts < time() - $expire) {
316 $this->set($name, time(), 'timer');
317 return $ts ?
$ts : 'not set';
323 * Adds a userContext to the stack.
325 * @param string $userContext
326 * The url to return to when done.
328 * Should we do a dupe checking with the top element.
330 public function pushUserContext($userContext, $check = TRUE) {
331 if (empty($userContext)) {
335 $this->createScope(self
::USER_CONTEXT
);
337 // hack, reset if too big
338 if (count($this->_session
[$this->_key
][self
::USER_CONTEXT
]) > 10) {
339 $this->resetScope(self
::USER_CONTEXT
);
340 $this->createScope(self
::USER_CONTEXT
);
343 $topUC = array_pop($this->_session
[$this->_key
][self
::USER_CONTEXT
]);
345 // see if there is a match between the new UC and the top one. the match needs to be
346 // fuzzy since we use the referer at times
347 // if close enough, lets just replace the top with the new one
348 if ($check && $topUC && CRM_Utils_String
::match($topUC, $userContext)) {
349 array_push($this->_session
[$this->_key
][self
::USER_CONTEXT
], $userContext);
353 array_push($this->_session
[$this->_key
][self
::USER_CONTEXT
], $topUC);
355 array_push($this->_session
[$this->_key
][self
::USER_CONTEXT
], $userContext);
360 * Replace the userContext of the stack with the passed one.
362 * @param string $userContext
363 * The url to return to when done.
365 public function replaceUserContext($userContext) {
366 if (empty($userContext)) {
370 $this->createScope(self
::USER_CONTEXT
);
372 array_pop($this->_session
[$this->_key
][self
::USER_CONTEXT
]);
373 array_push($this->_session
[$this->_key
][self
::USER_CONTEXT
], $userContext);
377 * Pops the top userContext stack.
380 * the top of the userContext stack (also pops the top element)
382 public function popUserContext() {
383 $this->createScope(self
::USER_CONTEXT
);
385 return array_pop($this->_session
[$this->_key
][self
::USER_CONTEXT
]);
389 * Reads the top userContext stack.
392 * the top of the userContext stack
394 public function readUserContext() {
395 $this->createScope(self
::USER_CONTEXT
);
397 $config = CRM_Core_Config
::singleton();
398 $lastElement = count($this->_session
[$this->_key
][self
::USER_CONTEXT
]) - 1;
399 return $lastElement >= 0 ?
$this->_session
[$this->_key
][self
::USER_CONTEXT
][$lastElement] : $config->userFrameworkBaseURL
;
403 * Dumps the session to the log.
407 public function debug($all = 1) {
410 CRM_Core_Error
::debug('CRM Session', $this->_session
);
413 CRM_Core_Error
::debug('CRM Session', $this->_session
[$this->_key
]);
418 * Fetches status messages.
421 * Should we reset the status variable?.
424 * the status message if any
426 public function getStatus($reset = FALSE) {
430 if (array_key_exists('status', $this->_session
[$this->_key
])) {
431 $status = $this->_session
[$this->_key
]['status'];
434 $this->_session
[$this->_key
]['status'] = NULL;
435 unset($this->_session
[$this->_key
]['status']);
441 * Stores an alert to be displayed to the user via crm-messages.
443 * @param string $text
446 * @param string $title
447 * The optional title of this message
449 * @param string $type
450 * The type of this message (printed as a css class). Possible options:
451 * - 'alert' (default)
454 * - 'error' (this message type by default will remain on the screen
455 * until the user dismisses it)
456 * - 'no-popup' (will display in the document like old-school)
458 * @param array $options
459 * Additional options. Possible values:
460 * - 'unique' (default: true) Check if this message was already set before adding
461 * - 'expires' how long to display this message before fadeout (in ms)
462 * set to 0 for no expiration
463 * defaults to 10 seconds for most messages, 5 if it has a title but no body,
464 * or 0 for errors or messages containing links
466 public static function setStatus($text, $title = '', $type = 'alert', $options = []) {
467 // make sure session is initialized, CRM-8120
468 $session = self
::singleton();
469 $session->initialize();
471 // Sanitize any HTML we're displaying. This helps prevent reflected XSS in error messages.
472 $text = CRM_Utils_String
::purifyHTML($text);
473 $title = CRM_Utils_String
::purifyHTML($title);
476 $options +
= ['unique' => TRUE];
478 if (!isset(self
::$_singleton->_session
[self
::$_singleton->_key
]['status'])) {
479 self
::$_singleton->_session
[self
::$_singleton->_key
]['status'] = [];
481 if ($text ||
$title) {
482 if ($options['unique']) {
483 foreach (self
::$_singleton->_session
[self
::$_singleton->_key
]['status'] as $msg) {
484 if ($msg['text'] == $text && $msg['title'] == $title) {
489 unset($options['unique']);
490 self
::$_singleton->_session
[self
::$_singleton->_key
]['status'][] = [
494 'options' => $options ?
$options : NULL,
500 * Register and retrieve session objects.
502 * @param string|array $names
504 public static function registerAndRetrieveSessionObjects($names) {
505 if (!is_array($names)) {
509 if (!self
::$_managedNames) {
510 self
::$_managedNames = $names;
513 self
::$_managedNames = array_merge(self
::$_managedNames, $names);
516 CRM_Core_BAO_Cache
::restoreSessionFromCache($names);
520 * Store session objects.
524 public static function storeSessionObjects($reset = TRUE) {
525 if (empty(self
::$_managedNames)) {
529 self
::$_managedNames = CRM_Utils_Array
::crmArrayUnique(self
::$_managedNames);
531 CRM_Core_BAO_Cache
::storeSessionToCache(self
::$_managedNames, $reset);
533 self
::$_managedNames = NULL;
537 * Retrieve contact id of the logged in user.
540 * contact ID of logged in user
542 public static function getLoggedInContactID() {
543 $session = CRM_Core_Session
::singleton();
544 if (!is_numeric($session->get('userID'))) {
547 return $session->get('userID');
551 * Get display name of the logged in user.
555 * @throws CiviCRM_API3_Exception
557 public function getLoggedInContactDisplayName() {
558 $userContactID = CRM_Core_Session
::getLoggedInContactID();
559 if (!$userContactID) {
562 return civicrm_api3('Contact', 'getvalue', ['id' => $userContactID, 'return' => 'display_name']);
566 * Check if session is empty.
568 * if so we don't cache stuff that we can get away with, helps proxies like varnish.
572 public function isEmpty() {
573 return empty($_SESSION);