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