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