Merge pull request #23970 from MegaphoneJon/false-not-zero
[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
44 /**
45 * We only need one instance of this object. So we use the singleton
46 * pattern and cache the instance in this variable
47 *
65d27ad8 48 * @var \CRM_Core_Session
6a488035 49 */
65d27ad8 50 static private $_singleton;
6a488035
TO
51
52 /**
f9e31d7f 53 * Constructor.
6a488035 54 *
d4ad6ab3 55 * The CMS takes care of initiating the php session handler session_start().
6a488035 56 *
6a488035 57 * All crm code should always use the session using
d4ad6ab3
CW
58 * CRM_Core_Session. we prefix stuff to avoid collisions with the CMS and also
59 * collisions with other crm modules!
60 *
6a488035
TO
61 * This constructor is invoked whenever any module requests an instance of
62 * the session and one is not available.
63 *
d4ad6ab3 64 * @return CRM_Core_Session
6a488035 65 */
00be9182 66 public function __construct() {
2aa397bc 67 $this->_session = NULL;
6a488035
TO
68 }
69
70 /**
f9e31d7f 71 * Singleton function used to manage this object.
6a488035 72 *
d46561f5 73 * @return CRM_Core_Session
6a488035 74 */
00be9182 75 public static function &singleton() {
6a488035 76 if (self::$_singleton === NULL) {
317fceb4 77 self::$_singleton = new CRM_Core_Session();
6a488035
TO
78 }
79 return self::$_singleton;
80 }
81
bb1d1976
TO
82 /**
83 * Replace the session object with a fake session.
84 */
85 public static function useFakeSession() {
86 self::$_singleton = new class() extends CRM_Core_Session {
87
88 public function initialize($isRead = FALSE) {
89 if ($isRead) {
90 return;
91 }
92
93 if (!isset($this->_session)) {
94 $this->_session = [];
95 }
96
97 if (!isset($this->_session[$this->_key]) || !is_array($this->_session[$this->_key])) {
98 $this->_session[$this->_key] = [];
99 }
100 }
101
102 public function isEmpty() {
103 return empty($this->_session);
104 }
105
106 };
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);
111 }
112 return self::$_singleton;
113 }
114
6a488035 115 /**
8eedd10a 116 * Creates an array in the session.
117 *
118 * All variables now will be stored under this array.
6a488035 119 *
6a0b768e
TO
120 * @param bool $isRead
121 * Is this a read operation, in this case, the session will not be touched.
6a488035 122 */
00be9182 123 public function initialize($isRead = FALSE) {
1fa3e6ac 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);
9b418792 127 }
6a488035
TO
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)) {
131 // CRM-9483
132 if (!isset($_SESSION) && PHP_SAPI !== 'cli') {
133 if ($isRead) {
134 return;
135 }
671f655b 136 CRM_Core_Config::singleton()->userSystem->sessionStart();
6a488035
TO
137 }
138 $this->_session =& $_SESSION;
139 }
140
141 if ($isRead) {
142 return;
143 }
144
145 if (!isset($this->_session[$this->_key]) ||
146 !is_array($this->_session[$this->_key])
147 ) {
be2fb01f 148 $this->_session[$this->_key] = [];
6a488035 149 }
6a488035
TO
150 }
151
152 /**
f9e31d7f 153 * Resets the session store.
6a488035 154 *
dd244018 155 * @param int $all
6a488035 156 */
00be9182 157 public function reset($all = 1) {
6a488035
TO
158 if ($all != 1) {
159 $this->initialize();
160
161 // to make certain we clear it, first initialize it to empty
be2fb01f 162 $this->_session[$this->_key] = [];
6a488035
TO
163 unset($this->_session[$this->_key]);
164 }
165 else {
1fa3e6ac 166 $this->_session[$this->_key] = [];
167 unset($this->_session);
6a488035 168 }
6a488035
TO
169 }
170
171 /**
f9e31d7f 172 * Creates a session local scope.
6a488035 173 *
6a0b768e
TO
174 * @param string $prefix
175 * Local scope name.
176 * @param bool $isRead
177 * Is this a read operation, in this case, the session will not be touched.
6a488035 178 */
00be9182 179 public function createScope($prefix, $isRead = FALSE) {
6a488035
TO
180 $this->initialize($isRead);
181
182 if ($isRead || empty($prefix)) {
183 return;
184 }
185
a7488080 186 if (empty($this->_session[$this->_key][$prefix])) {
be2fb01f 187 $this->_session[$this->_key][$prefix] = [];
6a488035
TO
188 }
189 }
190
191 /**
f9e31d7f 192 * Resets the session local scope.
6a488035 193 *
6a0b768e
TO
194 * @param string $prefix
195 * Local scope name.
6a488035 196 */
00be9182 197 public function resetScope($prefix) {
6a488035
TO
198 $this->initialize();
199
200 if (empty($prefix)) {
201 return;
202 }
203
204 if (array_key_exists($prefix, $this->_session[$this->_key])) {
205 unset($this->_session[$this->_key][$prefix]);
206 }
207 }
208
209 /**
f9e31d7f 210 * Store the variable with the value in the session scope.
6a488035
TO
211 *
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
216 *
6a488035 217 *
6a0b768e
TO
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.
6a488035 224 */
00be9182 225 public function set($name, $value = NULL, $prefix = NULL) {
6a488035
TO
226 // create session scope
227 $this->createScope($prefix);
228
229 if (empty($prefix)) {
230 $session = &$this->_session[$this->_key];
231 }
232 else {
233 $session = &$this->_session[$this->_key][$prefix];
234 }
235
236 if (is_array($name)) {
237 foreach ($name as $n => $v) {
238 $session[$n] = $v;
239 }
240 }
241 else {
242 $session[$name] = $value;
243 }
244 }
245
246 /**
f9e31d7f 247 * Gets the value of the named variable in the session scope.
6a488035
TO
248 *
249 * This function takes a name and retrieves the value of this
250 * variable from the session scope.
251 *
6a488035 252 *
6a0b768e 253 * @param string $name
16b10e64 254 * name of the variable.
6a0b768e 255 * @param string $prefix
16b10e64 256 * adds another level of scope to the session.
6a488035
TO
257 *
258 * @return mixed
6a488035 259 */
00be9182 260 public function get($name, $prefix = NULL) {
6a488035
TO
261 // create session scope
262 $this->createScope($prefix, TRUE);
263
264 if (empty($this->_session) || empty($this->_session[$this->_key])) {
2aa397bc 265 return NULL;
6a488035
TO
266 }
267
268 if (empty($prefix)) {
269 $session =& $this->_session[$this->_key];
270 }
271 else {
272 if (empty($this->_session[$this->_key][$prefix])) {
2aa397bc 273 return NULL;
6a488035
TO
274 }
275 $session =& $this->_session[$this->_key][$prefix];
276 }
277
914d3734 278 return $session[$name] ?? NULL;
6a488035
TO
279 }
280
281 /**
8eedd10a 282 * Gets all the variables in the current session scope and stuffs them in an associate array.
6a488035 283 *
6a0b768e
TO
284 * @param array $vars
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.
6a488035 288 */
00be9182 289 public function getVars(&$vars, $prefix = '') {
6a488035
TO
290 // create session scope
291 $this->createScope($prefix, TRUE);
292
293 if (empty($prefix)) {
294 $values = &$this->_session[$this->_key];
295 }
296 else {
19707a63 297 $values = Civi::cache('session')->get("CiviCRM_{$prefix}");
6a488035
TO
298 }
299
300 if ($values) {
301 foreach ($values as $name => $value) {
302 $vars[$name] = $value;
303 }
304 }
305 }
306
307 /**
8eedd10a 308 * Set and check a timer.
309 *
310 * If it's expired, it will be set again.
311 *
6a488035
TO
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
315 *
6a488035 316 *
6a0b768e 317 * @param string $name
16b10e64 318 * name of the timer.
6a0b768e 319 * @param int $expire
16b10e64 320 * expiry time (in seconds).
6a488035
TO
321 *
322 * @return mixed
6a488035 323 */
00be9182 324 public function timer($name, $expire) {
6a488035
TO
325 $ts = $this->get($name, 'timer');
326 if (!$ts || $ts < time() - $expire) {
327 $this->set($name, time(), 'timer');
328 return $ts ? $ts : 'not set';
329 }
2aa397bc 330 return FALSE;
6a488035
TO
331 }
332
333 /**
f9e31d7f 334 * Adds a userContext to the stack.
6a488035 335 *
6a0b768e
TO
336 * @param string $userContext
337 * The url to return to when done.
338 * @param bool $check
339 * Should we do a dupe checking with the top element.
6a488035 340 */
00be9182 341 public function pushUserContext($userContext, $check = TRUE) {
6a488035
TO
342 if (empty($userContext)) {
343 return;
344 }
345
346 $this->createScope(self::USER_CONTEXT);
347
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);
352 }
353
354 $topUC = array_pop($this->_session[$this->_key][self::USER_CONTEXT]);
355
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);
361 }
362 else {
363 if ($topUC) {
364 array_push($this->_session[$this->_key][self::USER_CONTEXT], $topUC);
365 }
366 array_push($this->_session[$this->_key][self::USER_CONTEXT], $userContext);
367 }
368 }
369
370 /**
f9e31d7f 371 * Replace the userContext of the stack with the passed one.
6a488035 372 *
6a0b768e
TO
373 * @param string $userContext
374 * The url to return to when done.
6a488035 375 */
00be9182 376 public function replaceUserContext($userContext) {
6a488035
TO
377 if (empty($userContext)) {
378 return;
379 }
380
381 $this->createScope(self::USER_CONTEXT);
382
383 array_pop($this->_session[$this->_key][self::USER_CONTEXT]);
384 array_push($this->_session[$this->_key][self::USER_CONTEXT], $userContext);
385 }
386
387 /**
f9e31d7f 388 * Pops the top userContext stack.
6a488035 389 *
a6c01b45
CW
390 * @return string
391 * the top of the userContext stack (also pops the top element)
6a488035 392 */
00be9182 393 public function popUserContext() {
6a488035
TO
394 $this->createScope(self::USER_CONTEXT);
395
396 return array_pop($this->_session[$this->_key][self::USER_CONTEXT]);
397 }
398
399 /**
f9e31d7f 400 * Reads the top userContext stack.
6a488035 401 *
a6c01b45
CW
402 * @return string
403 * the top of the userContext stack
6a488035 404 */
00be9182 405 public function readUserContext() {
6a488035
TO
406 $this->createScope(self::USER_CONTEXT);
407
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;
411 }
412
413 /**
f9e31d7f 414 * Dumps the session to the log.
8eedd10a 415 *
d4ad6ab3 416 * @param int $all
6a488035 417 */
00be9182 418 public function debug($all = 1) {
6a488035
TO
419 $this->initialize();
420 if ($all != 1) {
421 CRM_Core_Error::debug('CRM Session', $this->_session);
422 }
423 else {
424 CRM_Core_Error::debug('CRM Session', $this->_session[$this->_key]);
425 }
426 }
427
428 /**
f9e31d7f 429 * Fetches status messages.
6a488035 430 *
6a0b768e
TO
431 * @param bool $reset
432 * Should we reset the status variable?.
6a488035 433 *
a6c01b45
CW
434 * @return string
435 * the status message if any
6a488035 436 */
00be9182 437 public function getStatus($reset = FALSE) {
6a488035
TO
438 $this->initialize();
439
440 $status = NULL;
441 if (array_key_exists('status', $this->_session[$this->_key])) {
442 $status = $this->_session[$this->_key]['status'];
443 }
444 if ($reset) {
445 $this->_session[$this->_key]['status'] = NULL;
446 unset($this->_session[$this->_key]['status']);
447 }
448 return $status;
449 }
450
451 /**
8eedd10a 452 * Stores an alert to be displayed to the user via crm-messages.
6a488035 453 *
5a4f6742 454 * @param string $text
6a488035
TO
455 * The status message
456 *
5a4f6742 457 * @param string $title
6a488035
TO
458 * The optional title of this message
459 *
5a4f6742 460 * @param string $type
6a488035
TO
461 * The type of this message (printed as a css class). Possible options:
462 * - 'alert' (default)
463 * - 'info'
464 * - 'success'
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)
468 *
5a4f6742 469 * @param array $options
6a488035
TO
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
6a488035 476 */
be2fb01f 477 public static function setStatus($text, $title = '', $type = 'alert', $options = []) {
6a488035
TO
478 // make sure session is initialized, CRM-8120
479 $session = self::singleton();
480 $session->initialize();
481
98b2a189
SM
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);
485
6a488035 486 // default options
be2fb01f 487 $options += ['unique' => TRUE];
6a488035
TO
488
489 if (!isset(self::$_singleton->_session[self::$_singleton->_key]['status'])) {
be2fb01f 490 self::$_singleton->_session[self::$_singleton->_key]['status'] = [];
6a488035 491 }
bc86ffa9 492 if ($text || $title) {
6a488035
TO
493 if ($options['unique']) {
494 foreach (self::$_singleton->_session[self::$_singleton->_key]['status'] as $msg) {
495 if ($msg['text'] == $text && $msg['title'] == $title) {
496 return;
497 }
498 }
499 }
500 unset($options['unique']);
be2fb01f 501 self::$_singleton->_session[self::$_singleton->_key]['status'][] = [
6a488035
TO
502 'text' => $text,
503 'title' => $title,
504 'type' => $type,
03a7ec8f 505 'options' => $options ? $options : NULL,
be2fb01f 506 ];
6a488035
TO
507 }
508 }
509
a0ee3941 510 /**
8eedd10a 511 * Register and retrieve session objects.
512 *
d4ad6ab3 513 * @param string|array $names
a0ee3941 514 */
00be9182 515 public static function registerAndRetrieveSessionObjects($names) {
6a488035 516 if (!is_array($names)) {
be2fb01f 517 $names = [$names];
6a488035
TO
518 }
519
520 if (!self::$_managedNames) {
521 self::$_managedNames = $names;
522 }
523 else {
524 self::$_managedNames = array_merge(self::$_managedNames, $names);
525 }
526
527 CRM_Core_BAO_Cache::restoreSessionFromCache($names);
528 }
529
a0ee3941 530 /**
8eedd10a 531 * Store session objects.
532 *
a0ee3941
EM
533 * @param bool $reset
534 */
00be9182 535 public static function storeSessionObjects($reset = TRUE) {
6a488035
TO
536 if (empty(self::$_managedNames)) {
537 return;
538 }
539
540 self::$_managedNames = CRM_Utils_Array::crmArrayUnique(self::$_managedNames);
541
542 CRM_Core_BAO_Cache::storeSessionToCache(self::$_managedNames, $reset);
543
544 self::$_managedNames = NULL;
545 }
546
bb341097 547 /**
f9e31d7f 548 * Retrieve contact id of the logged in user.
8eedd10a 549 *
e97c66ff 550 * @return int|null
72b3a70c 551 * contact ID of logged in user
bb341097 552 */
00be9182 553 public static function getLoggedInContactID() {
bb341097
EM
554 $session = CRM_Core_Session::singleton();
555 if (!is_numeric($session->get('userID'))) {
556 return NULL;
557 }
2d852c61 558 return (int) $session->get('userID');
bb341097
EM
559 }
560
8f864776 561 /**
562 * Get display name of the logged in user.
563 *
564 * @return string
565 *
566 * @throws CiviCRM_API3_Exception
567 */
568 public function getLoggedInContactDisplayName() {
3bdcd4ec 569 $userContactID = CRM_Core_Session::getLoggedInContactID();
8f864776 570 if (!$userContactID) {
571 return '';
572 }
be2fb01f 573 return civicrm_api3('Contact', 'getvalue', ['id' => $userContactID, 'return' => 'display_name']);
8f864776 574 }
575
a0ee3941 576 /**
8eedd10a 577 * Check if session is empty.
578 *
579 * if so we don't cache stuff that we can get away with, helps proxies like varnish.
580 *
a0ee3941
EM
581 * @return bool
582 */
00be9182 583 public function isEmpty() {
d4ad6ab3 584 return empty($_SESSION);
6a488035 585 }
96025800 586
6a488035 587}