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