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