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