3 +--------------------------------------------------------------------+
4 | Copyright CiviCRM LLC. All rights reserved. |
6 | This work is published under the GNU AGPLv3 license with some |
7 | permitted exceptions and without any warranty. For full license |
8 | and copyright information, see https://civicrm.org/licensing |
9 +--------------------------------------------------------------------+
13 * Class CRM_Core_Session.
15 class CRM_Core_Session
{
18 * Cache of all the session names that we manage.
21 public static $_managedNames = NULL;
24 * Key is used to allow the application to have multiple top
25 * level scopes rather than a single scope. (avoids naming
26 * conflicts). We also extend this idea further and have local
27 * scopes within a global scope. Allows us to do cool things
28 * like resetting a specific area of the session code while
29 * keeping the rest intact
33 protected $_key = 'CiviCRM';
34 const USER_CONTEXT
= 'userContext';
37 * This is just a reference to the real session. Allows us to
38 * debug this class a wee bit easier
42 protected $_session = NULL;
45 * We only need one instance of this object. So we use the singleton
46 * pattern and cache the instance in this variable
48 * @var \CRM_Core_Session
50 static private $_singleton;
55 * The CMS takes care of initiating the php session handler session_start().
57 * All crm code should always use the session using
58 * CRM_Core_Session. we prefix stuff to avoid collisions with the CMS and also
59 * collisions with other crm modules!
61 * This constructor is invoked whenever any module requests an instance of
62 * the session and one is not available.
64 * @return CRM_Core_Session
66 public function __construct() {
67 $this->_session
= NULL;
71 * Singleton function used to manage this object.
73 * @return CRM_Core_Session
75 public static function &singleton() {
76 if (self
::$_singleton === NULL) {
77 self
::$_singleton = new CRM_Core_Session();
79 return self
::$_singleton;
83 * Replace the session object with a fake session.
85 public static function useFakeSession() {
86 self
::$_singleton = new class() extends CRM_Core_Session
{
88 public function initialize($isRead = FALSE) {
93 if (!isset($this->_session
)) {
97 if (!isset($this->_session
[$this->_key
]) ||
!is_array($this->_session
[$this->_key
])) {
98 $this->_session
[$this->_key
] = [];
102 public function isEmpty() {
103 return empty($this->_session
);
107 self
::$_singleton->_session
= NULL;
108 // This is not a revocable proposition. Should survive, even with things 'System.flush'.
109 if (!defined('_CIVICRM_FAKE_SESSION')) {
110 define('_CIVICRM_FAKE_SESSION', TRUE);
112 return self
::$_singleton;
116 * Creates an array in the session.
118 * All variables now will be stored under this array.
120 * @param bool $isRead
121 * Is this a read operation, in this case, the session will not be touched.
123 public function initialize($isRead = FALSE) {
124 // reset $this->_session in case if it is no longer a reference to $_SESSION;
125 if (isset($_SESSION) && isset($this->_session
) && $_SESSION !== $this->_session
) {
126 unset($this->_session
);
128 // lets initialize the _session variable just before we need it
129 // hopefully any bootstrapping code will actually load the session from the CMS
130 if (!isset($this->_session
)) {
132 if (!isset($_SESSION) && PHP_SAPI
!== 'cli') {
136 CRM_Core_Config
::singleton()->userSystem
->sessionStart();
138 $this->_session
=& $_SESSION;
145 if (!isset($this->_session
[$this->_key
]) ||
146 !is_array($this->_session
[$this->_key
])
148 $this->_session
[$this->_key
] = [];
153 * Resets the session store.
157 public function reset($all = 1) {
161 // to make certain we clear it, first initialize it to empty
162 $this->_session
[$this->_key
] = [];
163 unset($this->_session
[$this->_key
]);
166 $this->_session
[$this->_key
] = [];
167 unset($this->_session
);
172 * Creates a session local scope.
174 * @param string $prefix
176 * @param bool $isRead
177 * Is this a read operation, in this case, the session will not be touched.
179 public function createScope($prefix, $isRead = FALSE) {
180 $this->initialize($isRead);
182 if ($isRead ||
empty($prefix)) {
186 if (empty($this->_session
[$this->_key
][$prefix])) {
187 $this->_session
[$this->_key
][$prefix] = [];
192 * Resets the session local scope.
194 * @param string $prefix
197 public function resetScope($prefix) {
200 if (empty($prefix)) {
204 if (array_key_exists($prefix, $this->_session
[$this->_key
])) {
205 unset($this->_session
[$this->_key
][$prefix]);
210 * Store the variable with the value in the session scope.
212 * This function takes a name, value pair and stores this
213 * in the session scope. Not sure what happens if we try
214 * to store complex objects in the session. I suspect it
215 * is supported but we need to verify this
218 * @param string $name
219 * Name of the variable.
220 * @param mixed $value
221 * Value of the variable.
222 * @param string $prefix
223 * A string to prefix the keys in the session with.
225 public function set($name, $value = NULL, $prefix = NULL) {
226 // create session scope
227 $this->createScope($prefix);
229 if (empty($prefix)) {
230 $session = &$this->_session
[$this->_key
];
233 $session = &$this->_session
[$this->_key
][$prefix];
236 if (is_array($name)) {
237 foreach ($name as $n => $v) {
242 $session[$name] = $value;
247 * Gets the value of the named variable in the session scope.
249 * This function takes a name and retrieves the value of this
250 * variable from the session scope.
253 * @param string $name
254 * name of the variable.
255 * @param string $prefix
256 * adds another level of scope to the session.
260 public function get($name, $prefix = NULL) {
261 // create session scope
262 $this->createScope($prefix, TRUE);
264 if (empty($this->_session
) ||
empty($this->_session
[$this->_key
])) {
268 if (empty($prefix)) {
269 $session =& $this->_session
[$this->_key
];
272 if (empty($this->_session
[$this->_key
][$prefix])) {
275 $session =& $this->_session
[$this->_key
][$prefix];
278 return $session[$name] ??
NULL;
282 * Gets all the variables in the current session scope and stuffs them in an associate array.
285 * Associative array to store name/value pairs.
286 * @param string $prefix
287 * Will be stripped from the key before putting it in the return.
289 public function getVars(&$vars, $prefix = '') {
290 // create session scope
291 $this->createScope($prefix, TRUE);
293 if (empty($prefix)) {
294 $values = &$this->_session
[$this->_key
];
297 $values = Civi
::cache('session')->get("CiviCRM_{$prefix}");
301 foreach ($values as $name => $value) {
302 $vars[$name] = $value;
308 * Set and check a timer.
310 * If it's expired, it will be set again.
312 * Good for showing a message to the user every hour or day (so not bugging them on every page)
313 * Returns true-ish values if the timer is not set or expired, and false if the timer is still running
314 * 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
317 * @param string $name
320 * expiry time (in seconds).
324 public function timer($name, $expire) {
325 $ts = $this->get($name, 'timer');
326 if (!$ts ||
$ts < time() - $expire) {
327 $this->set($name, time(), 'timer');
328 return $ts ?
$ts : 'not set';
334 * Adds a userContext to the stack.
336 * @param string $userContext
337 * The url to return to when done.
339 * Should we do a dupe checking with the top element.
341 public function pushUserContext($userContext, $check = TRUE) {
342 if (empty($userContext)) {
346 $this->createScope(self
::USER_CONTEXT
);
348 // hack, reset if too big
349 if (count($this->_session
[$this->_key
][self
::USER_CONTEXT
]) > 10) {
350 $this->resetScope(self
::USER_CONTEXT
);
351 $this->createScope(self
::USER_CONTEXT
);
354 $topUC = array_pop($this->_session
[$this->_key
][self
::USER_CONTEXT
]);
356 // see if there is a match between the new UC and the top one. the match needs to be
357 // fuzzy since we use the referer at times
358 // if close enough, lets just replace the top with the new one
359 if ($check && $topUC && CRM_Utils_String
::match($topUC, $userContext)) {
360 array_push($this->_session
[$this->_key
][self
::USER_CONTEXT
], $userContext);
364 array_push($this->_session
[$this->_key
][self
::USER_CONTEXT
], $topUC);
366 array_push($this->_session
[$this->_key
][self
::USER_CONTEXT
], $userContext);
371 * Replace the userContext of the stack with the passed one.
373 * @param string $userContext
374 * The url to return to when done.
376 public function replaceUserContext($userContext) {
377 if (empty($userContext)) {
381 $this->createScope(self
::USER_CONTEXT
);
383 array_pop($this->_session
[$this->_key
][self
::USER_CONTEXT
]);
384 array_push($this->_session
[$this->_key
][self
::USER_CONTEXT
], $userContext);
388 * Pops the top userContext stack.
391 * the top of the userContext stack (also pops the top element)
393 public function popUserContext() {
394 $this->createScope(self
::USER_CONTEXT
);
396 return array_pop($this->_session
[$this->_key
][self
::USER_CONTEXT
]);
400 * Reads the top userContext stack.
403 * the top of the userContext stack
405 public function readUserContext() {
406 $this->createScope(self
::USER_CONTEXT
);
408 $config = CRM_Core_Config
::singleton();
409 $lastElement = count($this->_session
[$this->_key
][self
::USER_CONTEXT
]) - 1;
410 return $lastElement >= 0 ?
$this->_session
[$this->_key
][self
::USER_CONTEXT
][$lastElement] : $config->userFrameworkBaseURL
;
414 * Dumps the session to the log.
418 public function debug($all = 1) {
421 CRM_Core_Error
::debug('CRM Session', $this->_session
);
424 CRM_Core_Error
::debug('CRM Session', $this->_session
[$this->_key
]);
429 * Fetches status messages.
432 * Should we reset the status variable?.
435 * the status message if any
437 public function getStatus($reset = FALSE) {
441 if (array_key_exists('status', $this->_session
[$this->_key
])) {
442 $status = $this->_session
[$this->_key
]['status'];
445 $this->_session
[$this->_key
]['status'] = NULL;
446 unset($this->_session
[$this->_key
]['status']);
452 * Stores an alert to be displayed to the user via crm-messages.
454 * @param string $text
457 * @param string $title
458 * The optional title of this message
460 * @param string $type
461 * The type of this message (printed as a css class). Possible options:
462 * - 'alert' (default)
465 * - 'error' (this message type by default will remain on the screen
466 * until the user dismisses it)
467 * - 'no-popup' (will display in the document like old-school)
469 * @param array $options
470 * Additional options. Possible values:
471 * - 'unique' (default: true) Check if this message was already set before adding
472 * - 'expires' how long to display this message before fadeout (in ms)
473 * set to 0 for no expiration
474 * defaults to 10 seconds for most messages, 5 if it has a title but no body,
475 * or 0 for errors or messages containing links
477 public static function setStatus($text, $title = '', $type = 'alert', $options = []) {
478 // make sure session is initialized, CRM-8120
479 $session = self
::singleton();
480 $session->initialize();
482 // Sanitize any HTML we're displaying. This helps prevent reflected XSS in error messages.
483 $text = CRM_Utils_String
::purifyHTML($text);
484 $title = CRM_Utils_String
::purifyHTML($title);
487 $options +
= ['unique' => TRUE];
489 if (!isset(self
::$_singleton->_session
[self
::$_singleton->_key
]['status'])) {
490 self
::$_singleton->_session
[self
::$_singleton->_key
]['status'] = [];
492 if ($text ||
$title) {
493 if ($options['unique']) {
494 foreach (self
::$_singleton->_session
[self
::$_singleton->_key
]['status'] as $msg) {
495 if ($msg['text'] == $text && $msg['title'] == $title) {
500 unset($options['unique']);
501 self
::$_singleton->_session
[self
::$_singleton->_key
]['status'][] = [
505 'options' => $options ?
$options : NULL,
511 * Register and retrieve session objects.
513 * @param string|array $names
515 public static function registerAndRetrieveSessionObjects($names) {
516 if (!is_array($names)) {
520 if (!self
::$_managedNames) {
521 self
::$_managedNames = $names;
524 self
::$_managedNames = array_merge(self
::$_managedNames, $names);
527 CRM_Core_BAO_Cache
::restoreSessionFromCache($names);
531 * Store session objects.
535 public static function storeSessionObjects($reset = TRUE) {
536 if (empty(self
::$_managedNames)) {
540 self
::$_managedNames = CRM_Utils_Array
::crmArrayUnique(self
::$_managedNames);
542 CRM_Core_BAO_Cache
::storeSessionToCache(self
::$_managedNames, $reset);
544 self
::$_managedNames = NULL;
548 * Retrieve contact id of the logged in user.
551 * contact ID of logged in user
553 public static function getLoggedInContactID() {
554 $session = CRM_Core_Session
::singleton();
555 if (!is_numeric($session->get('userID'))) {
558 return (int) $session->get('userID');
562 * Get display name of the logged in user.
566 * @throws CiviCRM_API3_Exception
568 public function getLoggedInContactDisplayName() {
569 $userContactID = CRM_Core_Session
::getLoggedInContactID();
570 if (!$userContactID) {
573 return civicrm_api3('Contact', 'getvalue', ['id' => $userContactID, 'return' => 'display_name']);
577 * Check if session is empty.
579 * if so we don't cache stuff that we can get away with, helps proxies like varnish.
583 public function isEmpty() {
584 return empty($_SESSION);