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