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