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