Merge pull request #20152 from colemanw/resetLocationProviderHashPrefix
[civicrm-core.git] / CRM / Core / Session.php
CommitLineData
6a488035
TO
1<?php
2/*
3 +--------------------------------------------------------------------+
bc77d7c0 4 | Copyright CiviCRM LLC. All rights reserved. |
6a488035 5 | |
bc77d7c0
TO
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 |
6a488035 9 +--------------------------------------------------------------------+
d25dd0ee 10 */
6a488035 11
4c6ce474 12/**
8eedd10a 13 * Class CRM_Core_Session.
4c6ce474 14 */
6a488035
TO
15class CRM_Core_Session {
16
17 /**
f9e31d7f 18 * Cache of all the session names that we manage.
518fa0ee 19 * @var array
6a488035 20 */
518fa0ee 21 public static $_managedNames = NULL;
6a488035
TO
22
23 /**
100fef9d 24 * Key is used to allow the application to have multiple top
6a488035
TO
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
30 *
31 * @var string
32 */
33 protected $_key = 'CiviCRM';
7da04cde 34 const USER_CONTEXT = 'userContext';
6a488035
TO
35
36 /**
37 * This is just a reference to the real session. Allows us to
38 * debug this class a wee bit easier
39 *
40 * @var object
41 */
42 protected $_session = NULL;
43
9b418792 44 /**
45 * Current php Session ID : needed to detect if the session is changed
46 *
47 * @var string
48 */
49 protected $sessionID;
50
6a488035
TO
51 /**
52 * We only need one instance of this object. So we use the singleton
53 * pattern and cache the instance in this variable
54 *
65d27ad8 55 * @var \CRM_Core_Session
6a488035 56 */
65d27ad8 57 static private $_singleton;
6a488035
TO
58
59 /**
f9e31d7f 60 * Constructor.
6a488035 61 *
d4ad6ab3 62 * The CMS takes care of initiating the php session handler session_start().
6a488035 63 *
6a488035 64 * All crm code should always use the session using
d4ad6ab3
CW
65 * CRM_Core_Session. we prefix stuff to avoid collisions with the CMS and also
66 * collisions with other crm modules!
67 *
6a488035
TO
68 * This constructor is invoked whenever any module requests an instance of
69 * the session and one is not available.
70 *
d4ad6ab3 71 * @return CRM_Core_Session
6a488035 72 */
00be9182 73 public function __construct() {
2aa397bc 74 $this->_session = NULL;
6a488035
TO
75 }
76
77 /**
f9e31d7f 78 * Singleton function used to manage this object.
6a488035 79 *
d46561f5 80 * @return CRM_Core_Session
6a488035 81 */
00be9182 82 public static function &singleton() {
6a488035 83 if (self::$_singleton === NULL) {
317fceb4 84 self::$_singleton = new CRM_Core_Session();
6a488035
TO
85 }
86 return self::$_singleton;
87 }
88
bb1d1976
TO
89 /**
90 * Replace the session object with a fake session.
91 */
92 public static function useFakeSession() {
93 self::$_singleton = new class() extends CRM_Core_Session {
94
95 public function initialize($isRead = FALSE) {
96 if ($isRead) {
97 return;
98 }
99
100 if (!isset($this->_session)) {
101 $this->_session = [];
102 }
103
104 if (!isset($this->_session[$this->_key]) || !is_array($this->_session[$this->_key])) {
105 $this->_session[$this->_key] = [];
106 }
107 }
108
109 public function isEmpty() {
110 return empty($this->_session);
111 }
112
113 };
114 self::$_singleton->_session = NULL;
115 // This is not a revocable proposition. Should survive, even with things 'System.flush'.
116 if (!defined('_CIVICRM_FAKE_SESSION')) {
117 define('_CIVICRM_FAKE_SESSION', TRUE);
118 }
119 return self::$_singleton;
120 }
121
6a488035 122 /**
8eedd10a 123 * Creates an array in the session.
124 *
125 * All variables now will be stored under this array.
6a488035 126 *
6a0b768e
TO
127 * @param bool $isRead
128 * Is this a read operation, in this case, the session will not be touched.
6a488035 129 */
00be9182 130 public function initialize($isRead = FALSE) {
9b418792 131 // remove $_SESSION reference if session is changed
132 if (($sid = session_id()) !== $this->sessionID) {
133 $this->_session = NULL;
134 $this->sessionID = $sid;
135 }
6a488035
TO
136 // lets initialize the _session variable just before we need it
137 // hopefully any bootstrapping code will actually load the session from the CMS
138 if (!isset($this->_session)) {
139 // CRM-9483
140 if (!isset($_SESSION) && PHP_SAPI !== 'cli') {
141 if ($isRead) {
142 return;
143 }
671f655b 144 CRM_Core_Config::singleton()->userSystem->sessionStart();
6a488035
TO
145 }
146 $this->_session =& $_SESSION;
147 }
148
149 if ($isRead) {
150 return;
151 }
152
153 if (!isset($this->_session[$this->_key]) ||
154 !is_array($this->_session[$this->_key])
155 ) {
be2fb01f 156 $this->_session[$this->_key] = [];
6a488035 157 }
6a488035
TO
158 }
159
160 /**
f9e31d7f 161 * Resets the session store.
6a488035 162 *
dd244018 163 * @param int $all
6a488035 164 */
00be9182 165 public function reset($all = 1) {
6a488035
TO
166 if ($all != 1) {
167 $this->initialize();
168
169 // to make certain we clear it, first initialize it to empty
be2fb01f 170 $this->_session[$this->_key] = [];
6a488035
TO
171 unset($this->_session[$this->_key]);
172 }
173 else {
be2fb01f 174 $this->_session = [];
6a488035
TO
175 }
176
6a488035
TO
177 }
178
179 /**
f9e31d7f 180 * Creates a session local scope.
6a488035 181 *
6a0b768e
TO
182 * @param string $prefix
183 * Local scope name.
184 * @param bool $isRead
185 * Is this a read operation, in this case, the session will not be touched.
6a488035 186 */
00be9182 187 public function createScope($prefix, $isRead = FALSE) {
6a488035
TO
188 $this->initialize($isRead);
189
190 if ($isRead || empty($prefix)) {
191 return;
192 }
193
a7488080 194 if (empty($this->_session[$this->_key][$prefix])) {
be2fb01f 195 $this->_session[$this->_key][$prefix] = [];
6a488035
TO
196 }
197 }
198
199 /**
f9e31d7f 200 * Resets the session local scope.
6a488035 201 *
6a0b768e
TO
202 * @param string $prefix
203 * Local scope name.
6a488035 204 */
00be9182 205 public function resetScope($prefix) {
6a488035
TO
206 $this->initialize();
207
208 if (empty($prefix)) {
209 return;
210 }
211
212 if (array_key_exists($prefix, $this->_session[$this->_key])) {
213 unset($this->_session[$this->_key][$prefix]);
214 }
215 }
216
217 /**
f9e31d7f 218 * Store the variable with the value in the session scope.
6a488035
TO
219 *
220 * This function takes a name, value pair and stores this
221 * in the session scope. Not sure what happens if we try
222 * to store complex objects in the session. I suspect it
223 * is supported but we need to verify this
224 *
6a488035 225 *
6a0b768e
TO
226 * @param string $name
227 * Name of the variable.
228 * @param mixed $value
229 * Value of the variable.
230 * @param string $prefix
231 * A string to prefix the keys in the session with.
6a488035 232 */
00be9182 233 public function set($name, $value = NULL, $prefix = NULL) {
6a488035
TO
234 // create session scope
235 $this->createScope($prefix);
236
237 if (empty($prefix)) {
238 $session = &$this->_session[$this->_key];
239 }
240 else {
241 $session = &$this->_session[$this->_key][$prefix];
242 }
243
244 if (is_array($name)) {
245 foreach ($name as $n => $v) {
246 $session[$n] = $v;
247 }
248 }
249 else {
250 $session[$name] = $value;
251 }
252 }
253
254 /**
f9e31d7f 255 * Gets the value of the named variable in the session scope.
6a488035
TO
256 *
257 * This function takes a name and retrieves the value of this
258 * variable from the session scope.
259 *
6a488035 260 *
6a0b768e 261 * @param string $name
16b10e64 262 * name of the variable.
6a0b768e 263 * @param string $prefix
16b10e64 264 * adds another level of scope to the session.
6a488035
TO
265 *
266 * @return mixed
6a488035 267 */
00be9182 268 public function get($name, $prefix = NULL) {
6a488035
TO
269 // create session scope
270 $this->createScope($prefix, TRUE);
271
272 if (empty($this->_session) || empty($this->_session[$this->_key])) {
2aa397bc 273 return NULL;
6a488035
TO
274 }
275
276 if (empty($prefix)) {
277 $session =& $this->_session[$this->_key];
278 }
279 else {
280 if (empty($this->_session[$this->_key][$prefix])) {
2aa397bc 281 return NULL;
6a488035
TO
282 }
283 $session =& $this->_session[$this->_key][$prefix];
284 }
285
914d3734 286 return $session[$name] ?? NULL;
6a488035
TO
287 }
288
289 /**
8eedd10a 290 * Gets all the variables in the current session scope and stuffs them in an associate array.
6a488035 291 *
6a0b768e
TO
292 * @param array $vars
293 * Associative array to store name/value pairs.
294 * @param string $prefix
295 * Will be stripped from the key before putting it in the return.
6a488035 296 */
00be9182 297 public function getVars(&$vars, $prefix = '') {
6a488035
TO
298 // create session scope
299 $this->createScope($prefix, TRUE);
300
301 if (empty($prefix)) {
302 $values = &$this->_session[$this->_key];
303 }
304 else {
19707a63 305 $values = Civi::cache('session')->get("CiviCRM_{$prefix}");
6a488035
TO
306 }
307
308 if ($values) {
309 foreach ($values as $name => $value) {
310 $vars[$name] = $value;
311 }
312 }
313 }
314
315 /**
8eedd10a 316 * Set and check a timer.
317 *
318 * If it's expired, it will be set again.
319 *
6a488035
TO
320 * Good for showing a message to the user every hour or day (so not bugging them on every page)
321 * Returns true-ish values if the timer is not set or expired, and false if the timer is still running
322 * 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
323 *
6a488035 324 *
6a0b768e 325 * @param string $name
16b10e64 326 * name of the timer.
6a0b768e 327 * @param int $expire
16b10e64 328 * expiry time (in seconds).
6a488035
TO
329 *
330 * @return mixed
6a488035 331 */
00be9182 332 public function timer($name, $expire) {
6a488035
TO
333 $ts = $this->get($name, 'timer');
334 if (!$ts || $ts < time() - $expire) {
335 $this->set($name, time(), 'timer');
336 return $ts ? $ts : 'not set';
337 }
2aa397bc 338 return FALSE;
6a488035
TO
339 }
340
341 /**
f9e31d7f 342 * Adds a userContext to the stack.
6a488035 343 *
6a0b768e
TO
344 * @param string $userContext
345 * The url to return to when done.
346 * @param bool $check
347 * Should we do a dupe checking with the top element.
6a488035 348 */
00be9182 349 public function pushUserContext($userContext, $check = TRUE) {
6a488035
TO
350 if (empty($userContext)) {
351 return;
352 }
353
354 $this->createScope(self::USER_CONTEXT);
355
356 // hack, reset if too big
357 if (count($this->_session[$this->_key][self::USER_CONTEXT]) > 10) {
358 $this->resetScope(self::USER_CONTEXT);
359 $this->createScope(self::USER_CONTEXT);
360 }
361
362 $topUC = array_pop($this->_session[$this->_key][self::USER_CONTEXT]);
363
364 // see if there is a match between the new UC and the top one. the match needs to be
365 // fuzzy since we use the referer at times
366 // if close enough, lets just replace the top with the new one
367 if ($check && $topUC && CRM_Utils_String::match($topUC, $userContext)) {
368 array_push($this->_session[$this->_key][self::USER_CONTEXT], $userContext);
369 }
370 else {
371 if ($topUC) {
372 array_push($this->_session[$this->_key][self::USER_CONTEXT], $topUC);
373 }
374 array_push($this->_session[$this->_key][self::USER_CONTEXT], $userContext);
375 }
376 }
377
378 /**
f9e31d7f 379 * Replace the userContext of the stack with the passed one.
6a488035 380 *
6a0b768e
TO
381 * @param string $userContext
382 * The url to return to when done.
6a488035 383 */
00be9182 384 public function replaceUserContext($userContext) {
6a488035
TO
385 if (empty($userContext)) {
386 return;
387 }
388
389 $this->createScope(self::USER_CONTEXT);
390
391 array_pop($this->_session[$this->_key][self::USER_CONTEXT]);
392 array_push($this->_session[$this->_key][self::USER_CONTEXT], $userContext);
393 }
394
395 /**
f9e31d7f 396 * Pops the top userContext stack.
6a488035 397 *
a6c01b45
CW
398 * @return string
399 * the top of the userContext stack (also pops the top element)
6a488035 400 */
00be9182 401 public function popUserContext() {
6a488035
TO
402 $this->createScope(self::USER_CONTEXT);
403
404 return array_pop($this->_session[$this->_key][self::USER_CONTEXT]);
405 }
406
407 /**
f9e31d7f 408 * Reads the top userContext stack.
6a488035 409 *
a6c01b45
CW
410 * @return string
411 * the top of the userContext stack
6a488035 412 */
00be9182 413 public function readUserContext() {
6a488035
TO
414 $this->createScope(self::USER_CONTEXT);
415
416 $config = CRM_Core_Config::singleton();
417 $lastElement = count($this->_session[$this->_key][self::USER_CONTEXT]) - 1;
418 return $lastElement >= 0 ? $this->_session[$this->_key][self::USER_CONTEXT][$lastElement] : $config->userFrameworkBaseURL;
419 }
420
421 /**
f9e31d7f 422 * Dumps the session to the log.
8eedd10a 423 *
d4ad6ab3 424 * @param int $all
6a488035 425 */
00be9182 426 public function debug($all = 1) {
6a488035
TO
427 $this->initialize();
428 if ($all != 1) {
429 CRM_Core_Error::debug('CRM Session', $this->_session);
430 }
431 else {
432 CRM_Core_Error::debug('CRM Session', $this->_session[$this->_key]);
433 }
434 }
435
436 /**
f9e31d7f 437 * Fetches status messages.
6a488035 438 *
6a0b768e
TO
439 * @param bool $reset
440 * Should we reset the status variable?.
6a488035 441 *
a6c01b45
CW
442 * @return string
443 * the status message if any
6a488035 444 */
00be9182 445 public function getStatus($reset = FALSE) {
6a488035
TO
446 $this->initialize();
447
448 $status = NULL;
449 if (array_key_exists('status', $this->_session[$this->_key])) {
450 $status = $this->_session[$this->_key]['status'];
451 }
452 if ($reset) {
453 $this->_session[$this->_key]['status'] = NULL;
454 unset($this->_session[$this->_key]['status']);
455 }
456 return $status;
457 }
458
459 /**
8eedd10a 460 * Stores an alert to be displayed to the user via crm-messages.
6a488035 461 *
5a4f6742 462 * @param string $text
6a488035
TO
463 * The status message
464 *
5a4f6742 465 * @param string $title
6a488035
TO
466 * The optional title of this message
467 *
5a4f6742 468 * @param string $type
6a488035
TO
469 * The type of this message (printed as a css class). Possible options:
470 * - 'alert' (default)
471 * - 'info'
472 * - 'success'
473 * - 'error' (this message type by default will remain on the screen
474 * until the user dismisses it)
475 * - 'no-popup' (will display in the document like old-school)
476 *
5a4f6742 477 * @param array $options
6a488035
TO
478 * Additional options. Possible values:
479 * - 'unique' (default: true) Check if this message was already set before adding
480 * - 'expires' how long to display this message before fadeout (in ms)
481 * set to 0 for no expiration
482 * defaults to 10 seconds for most messages, 5 if it has a title but no body,
483 * or 0 for errors or messages containing links
6a488035 484 */
be2fb01f 485 public static function setStatus($text, $title = '', $type = 'alert', $options = []) {
6a488035
TO
486 // make sure session is initialized, CRM-8120
487 $session = self::singleton();
488 $session->initialize();
489
98b2a189
SM
490 // Sanitize any HTML we're displaying. This helps prevent reflected XSS in error messages.
491 $text = CRM_Utils_String::purifyHTML($text);
492 $title = CRM_Utils_String::purifyHTML($title);
493
6a488035 494 // default options
be2fb01f 495 $options += ['unique' => TRUE];
6a488035
TO
496
497 if (!isset(self::$_singleton->_session[self::$_singleton->_key]['status'])) {
be2fb01f 498 self::$_singleton->_session[self::$_singleton->_key]['status'] = [];
6a488035 499 }
bc86ffa9 500 if ($text || $title) {
6a488035
TO
501 if ($options['unique']) {
502 foreach (self::$_singleton->_session[self::$_singleton->_key]['status'] as $msg) {
503 if ($msg['text'] == $text && $msg['title'] == $title) {
504 return;
505 }
506 }
507 }
508 unset($options['unique']);
be2fb01f 509 self::$_singleton->_session[self::$_singleton->_key]['status'][] = [
6a488035
TO
510 'text' => $text,
511 'title' => $title,
512 'type' => $type,
03a7ec8f 513 'options' => $options ? $options : NULL,
be2fb01f 514 ];
6a488035
TO
515 }
516 }
517
a0ee3941 518 /**
8eedd10a 519 * Register and retrieve session objects.
520 *
d4ad6ab3 521 * @param string|array $names
a0ee3941 522 */
00be9182 523 public static function registerAndRetrieveSessionObjects($names) {
6a488035 524 if (!is_array($names)) {
be2fb01f 525 $names = [$names];
6a488035
TO
526 }
527
528 if (!self::$_managedNames) {
529 self::$_managedNames = $names;
530 }
531 else {
532 self::$_managedNames = array_merge(self::$_managedNames, $names);
533 }
534
535 CRM_Core_BAO_Cache::restoreSessionFromCache($names);
536 }
537
a0ee3941 538 /**
8eedd10a 539 * Store session objects.
540 *
a0ee3941
EM
541 * @param bool $reset
542 */
00be9182 543 public static function storeSessionObjects($reset = TRUE) {
6a488035
TO
544 if (empty(self::$_managedNames)) {
545 return;
546 }
547
548 self::$_managedNames = CRM_Utils_Array::crmArrayUnique(self::$_managedNames);
549
550 CRM_Core_BAO_Cache::storeSessionToCache(self::$_managedNames, $reset);
551
552 self::$_managedNames = NULL;
553 }
554
bb341097 555 /**
f9e31d7f 556 * Retrieve contact id of the logged in user.
8eedd10a 557 *
e97c66ff 558 * @return int|null
72b3a70c 559 * contact ID of logged in user
bb341097 560 */
00be9182 561 public static function getLoggedInContactID() {
bb341097
EM
562 $session = CRM_Core_Session::singleton();
563 if (!is_numeric($session->get('userID'))) {
564 return NULL;
565 }
566 return $session->get('userID');
567 }
568
8f864776 569 /**
570 * Get display name of the logged in user.
571 *
572 * @return string
573 *
574 * @throws CiviCRM_API3_Exception
575 */
576 public function getLoggedInContactDisplayName() {
3bdcd4ec 577 $userContactID = CRM_Core_Session::getLoggedInContactID();
8f864776 578 if (!$userContactID) {
579 return '';
580 }
be2fb01f 581 return civicrm_api3('Contact', 'getvalue', ['id' => $userContactID, 'return' => 'display_name']);
8f864776 582 }
583
a0ee3941 584 /**
8eedd10a 585 * Check if session is empty.
586 *
587 * if so we don't cache stuff that we can get away with, helps proxies like varnish.
588 *
a0ee3941
EM
589 * @return bool
590 */
00be9182 591 public function isEmpty() {
d4ad6ab3 592 return empty($_SESSION);
6a488035 593 }
96025800 594
6a488035 595}