cake/libs/cake_session.php

1 <?php
2 /**
3 * Session class for Cake.
4 *
5 * Cake abstracts the handling of sessions.
6 * There are several convenient methods to access session information.
7 * This class is the implementation of those methods.
8 * They are mostly used by the Session Component.
9 *
10 * PHP versions 4 and 5
11 *
12 * CakePHP(tm) : Rapid Development Framework (http://cakephp.org)
13 * Copyright 2005-2010, Cake Software Foundation, Inc. (http://cakefoundation.org)
14 *
15 * Licensed under The MIT License
16 * Redistributions of files must retain the above copyright notice.
17 *
18 * @copyright Copyright 2005-2010, Cake Software Foundation, Inc. (http://cakefoundation.org)
19 * @link http://cakephp.org CakePHP(tm) Project
20 * @package cake
21 * @subpackage cake.cake.libs
22 * @since CakePHP(tm) v .0.10.0.1222
23 * @license MIT License (http://www.opensource.org/licenses/mit-license.php)
24 */
25 if (!class_exists('Security')) {
26 App::import('Core', 'Security');
27 }
28  
29 /**
30 * Session class for Cake.
31 *
32 * Cake abstracts the handling of sessions. There are several convenient methods to access session information.
33 * This class is the implementation of those methods. They are mostly used by the Session Component.
34 *
35 * @package cake
36 * @subpackage cake.cake.libs
37 */
38 class CakeSession extends Object {
39  
40 /**
41 * True if the Session is still valid
42 *
43 * @var boolean
44 * @access public
45 */
46 var $valid = false;
47  
48 /**
49 * Error messages for this session
50 *
51 * @var array
52 * @access public
53 */
54 var $error = false;
55  
56 /**
57 * User agent string
58 *
59 * @var string
60 * @access protected
61 */
62 var $_userAgent = '';
63  
64 /**
65 * Path to where the session is active.
66 *
67 * @var string
68 * @access public
69 */
70 var $path = '/';
71  
72 /**
73 * Error number of last occurred error
74 *
75 * @var integer
76 * @access public
77 */
78 var $lastError = null;
79  
80 /**
81 * 'Security.level' setting, "high", "medium", or "low".
82 *
83 * @var string
84 * @access public
85 */
86 var $security = null;
87  
88 /**
89 * Start time for this session.
90 *
91 * @var integer
92 * @access public
93 */
94 var $time = false;
95  
96 /**
97 * Time when this session becomes invalid.
98 *
99 * @var integer
100 * @access public
101 */
102 var $sessionTime = false;
103  
104 /**
105 * The number of seconds to set for session.cookie_lifetime. 0 means
106 * at browser close.
107 *
108 * @var integer
109 */
110 var $cookieLifeTime = false;
111  
112 /**
113 * Keeps track of keys to watch for writes on
114 *
115 * @var array
116 * @access public
117 */
118 var $watchKeys = array();
119  
120 /**
121 * Current Session id
122 *
123 * @var string
124 * @access public
125 */
126 var $id = null;
127  
128 /**
129 * Hostname
130 *
131 * @var string
132 * @access public
133 */
134 var $host = null;
135  
136 /**
137 * Session timeout multiplier factor
138 *
139 * @var integer
140 * @access public
141 */
142 var $timeout = null;
143  
144 /**
145 * Constructor.
146 *
147 * @param string $base The base path for the Session
148 * @param boolean $start Should session be started right now
149 * @access public
150 */
151 function __construct($base = null, $start = true) {
152 App::import('Core', array('Set', 'Security'));
153 $this->time = time();
154  
155 if (Configure::read('Session.checkAgent') === true || Configure::read('Session.checkAgent') === null) {
156 if (env('HTTP_USER_AGENT') != null) {
157 $this->_userAgent = md5(env('HTTP_USER_AGENT') . Configure::read('Security.salt'));
158 }
159 }
160 if (Configure::read('Session.save') === 'database') {
161 $modelName = Configure::read('Session.model');
162 $database = Configure::read('Session.database');
163 $table = Configure::read('Session.table');
164  
165 if (empty($database)) {
166 $database = 'default';
167 }
168 $settings = array(
169 'class' => 'Session',
170 'alias' => 'Session',
171 'table' => 'cake_sessions',
172 'ds' => $database
173 );
174 if (!empty($modelName)) {
175 $settings['class'] = $modelName;
176 }
177 if (!empty($table)) {
178 $settings['table'] = $table;
179 }
180 ClassRegistry::init($settings);
181 }
182 if ($start === true) {
183 if (!empty($base)) {
184 $this->path = $base;
185 if (strpos($base, 'index.php') !== false) {
186 $this->path = str_replace('index.php', '', $base);
187 }
188 if (strpos($base, '?') !== false) {
189 $this->path = str_replace('?', '', $base);
190 }
191 }
192 $this->host = env('HTTP_HOST');
193  
194 if (strpos($this->host, ':') !== false) {
195 $this->host = substr($this->host, 0, strpos($this->host, ':'));
196 }
197 }
198 if (isset($_SESSION) || $start === true) {
199 $this->sessionTime = $this->time + (Security::inactiveMins() * Configure::read('Session.timeout'));
200 $this->security = Configure::read('Security.level');
201 }
202 parent::__construct();
203 }
204  
205 /**
206 * Starts the Session.
207 *
208 * @return boolean True if session was started
209 * @access public
210 */
211 function start() {
212 if ($this->started()) {
213 return true;
214 }
215 if (function_exists('session_write_close')) {
216 session_write_close();
217 }
218 $this->__initSession();
219 $this->__startSession();
220 return $this->started();
221 }
222  
223 /**
224 * Determine if Session has been started.
225 *
226 * @access public
227 * @return boolean True if session has been started.
228 */
229 function started() {
230 if (isset($_SESSION) && session_id()) {
231 return true;
232 }
233 return false;
234 }
235  
236 /**
237 * Returns true if given variable is set in session.
238 *
239 * @param string $name Variable name to check for
240 * @return boolean True if variable is there
241 * @access public
242 */
243 function check($name) {
244 if (empty($name)) {
245 return false;
246 }
247 $result = Set::classicExtract($_SESSION, $name);
248 return isset($result);
249 }
250  
251 /**
252 * Returns the Session id
253 *
254 * @param id $name string
255 * @return string Session id
256 * @access public
257 */
258 function id($id = null) {
259 if ($id) {
260 $this->id = $id;
261 session_id($this->id);
262 }
263 if ($this->started()) {
264 return session_id();
265 } else {
266 return $this->id;
267 }
268 }
269  
270 /**
271 * Removes a variable from session.
272 *
273 * @param string $name Session variable to remove
274 * @return boolean Success
275 * @access public
276 */
277 function delete($name) {
278 if ($this->check($name)) {
279 if (in_array($name, $this->watchKeys)) {
280 trigger_error(sprintf(__('Deleting session key {%s}', true), $name), E_USER_NOTICE);
281 }
282 $this->__overwrite($_SESSION, Set::remove($_SESSION, $name));
283 return ($this->check($name) == false);
284 }
285 $this->__setError(2, sprintf(__("%s doesn't exist", true), $name));
286 return false;
287 }
288  
289 /**
290 * Used to write new data to _SESSION, since PHP doesn't like us setting the _SESSION var itself
291 *
292 * @param array $old Set of old variables => values
293 * @param array $new New set of variable => value
294 * @access private
295 */
296 function __overwrite(&$old, $new) {
297 if (!empty($old)) {
298 foreach ($old as $key => $var) {
299 if (!isset($new[$key])) {
300 unset($old[$key]);
301 }
302 }
303 }
304 foreach ($new as $key => $var) {
305 $old[$key] = $var;
306 }
307 }
308  
309 /**
310 * Return error description for given error number.
311 *
312 * @param integer $errorNumber Error to set
313 * @return string Error as string
314 * @access private
315 */
316 function __error($errorNumber) {
317 if (!is_array($this->error) || !array_key_exists($errorNumber, $this->error)) {
318 return false;
319 } else {
320 return $this->error[$errorNumber];
321 }
322 }
323  
324 /**
325 * Returns last occurred error as a string, if any.
326 *
327 * @return mixed Error description as a string, or false.
328 * @access public
329 */
330 function error() {
331 if ($this->lastError) {
332 return $this->__error($this->lastError);
333 } else {
334 return false;
335 }
336 }
337  
338 /**
339 * Returns true if session is valid.
340 *
341 * @return boolean Success
342 * @access public
343 */
344 function valid() {
345 if ($this->read('Config')) {
346 if ((Configure::read('Session.checkAgent') === false || $this->_userAgent == $this->read('Config.userAgent')) && $this->time <= $this->read('Config.time')) {
347 if ($this->error === false) {
348 $this->valid = true;
349 }
350 } else {
351 $this->valid = false;
352 $this->__setError(1, 'Session Highjacking Attempted !!!');
353 }
354 }
355 return $this->valid;
356 }
357  
358 /**
359 * Returns given session variable, or all of them, if no parameters given.
360 *
361 * @param mixed $name The name of the session variable (or a path as sent to Set.extract)
362 * @return mixed The value of the session variable
363 * @access public
364 */
365 function read($name = null) {
366 if (is_null($name)) {
367 return $this->__returnSessionVars();
368 }
369 if (empty($name)) {
370 return false;
371 }
372 $result = Set::classicExtract($_SESSION, $name);
373  
374 if (!is_null($result)) {
375 return $result;
376 }
377 $this->__setError(2, "$name doesn't exist");
378 return null;
379 }
380  
381 /**
382 * Returns all session variables.
383 *
384 * @return mixed Full $_SESSION array, or false on error.
385 * @access private
386 */
387 function __returnSessionVars() {
388 if (!empty($_SESSION)) {
389 return $_SESSION;
390 }
391 $this->__setError(2, "No Session vars set");
392 return false;
393 }
394  
395 /**
396 * Tells Session to write a notification when a certain session path or subpath is written to
397 *
398 * @param mixed $var The variable path to watch
399 * @return void
400 * @access public
401 */
402 function watch($var) {
403 if (empty($var)) {
404 return false;
405 }
406 if (!in_array($var, $this->watchKeys, true)) {
407 $this->watchKeys[] = $var;
408 }
409 }
410  
411 /**
412 * Tells Session to stop watching a given key path
413 *
414 * @param mixed $var The variable path to watch
415 * @return void
416 * @access public
417 */
418 function ignore($var) {
419 if (!in_array($var, $this->watchKeys)) {
420 return;
421 }
422 foreach ($this->watchKeys as $i => $key) {
423 if ($key == $var) {
424 unset($this->watchKeys[$i]);
425 $this->watchKeys = array_values($this->watchKeys);
426 return;
427 }
428 }
429 }
430  
431 /**
432 * Writes value to given session variable name.
433 *
434 * @param mixed $name Name of variable
435 * @param string $value Value to write
436 * @return boolean True if the write was successful, false if the write failed
437 * @access public
438 */
439 function write($name, $value) {
440 if (empty($name)) {
441 return false;
442 }
443 if (in_array($name, $this->watchKeys)) {
444 trigger_error(sprintf(__('Writing session key {%s}: %s', true), $name, Debugger::exportVar($value)), E_USER_NOTICE);
445 }
446 $this->__overwrite($_SESSION, Set::insert($_SESSION, $name, $value));
447 return (Set::classicExtract($_SESSION, $name) === $value);
448 }
449  
450 /**
451 * Helper method to destroy invalid sessions.
452 *
453 * @return void
454 * @access public
455 */
456 function destroy() {
457 if ($this->started()) {
458 session_destroy();
459 }
460 $_SESSION = null;
461 $this->__construct($this->path);
462 $this->start();
463 $this->renew();
464 $this->_checkValid();
465 }
466  
467 /**
468 * Helper method to initialize a session, based on Cake core settings.
469 *
470 * @access private
471 */
472 function __initSession() {
473 $iniSet = function_exists('ini_set');
474 if ($iniSet && env('HTTPS')) {
475 ini_set('session.cookie_secure', 1);
476 }
477 if ($iniSet && ($this->security === 'high' || $this->security === 'medium')) {
478 ini_set('session.referer_check', $this->host);
479 }
480  
481 if ($this->security == 'high') {
482 $this->cookieLifeTime = 0;
483 } else {
484 $this->cookieLifeTime = Configure::read('Session.timeout') * (Security::inactiveMins() * 60);
485 }
486  
487 switch (Configure::read('Session.save')) {
488 case 'cake':
489 if (empty($_SESSION)) {
490 if ($iniSet) {
491 ini_set('session.use_trans_sid', 0);
492 ini_set('url_rewriter.tags', '');
493 ini_set('session.serialize_handler', 'php');
494 ini_set('session.use_cookies', 1);
495 ini_set('session.name', Configure::read('Session.cookie'));
496 ini_set('session.cookie_lifetime', $this->cookieLifeTime);
497 ini_set('session.cookie_path', $this->path);
498 ini_set('session.auto_start', 0);
499 ini_set('session.save_path', TMP . 'sessions');
500 }
501 }
502 break;
503 case 'database':
504 if (empty($_SESSION)) {
505 if (Configure::read('Session.model') === null) {
506 trigger_error(__("You must set the all Configure::write('Session.*') in core.php to use database storage"), E_USER_WARNING);
507 $this->_stop();
508 }
509 if ($iniSet) {
510 ini_set('session.use_trans_sid', 0);
511 ini_set('url_rewriter.tags', '');
512 ini_set('session.save_handler', 'user');
513 ini_set('session.serialize_handler', 'php');
514 ini_set('session.use_cookies', 1);
515 ini_set('session.name', Configure::read('Session.cookie'));
516 ini_set('session.cookie_lifetime', $this->cookieLifeTime);
517 ini_set('session.cookie_path', $this->path);
518 ini_set('session.auto_start', 0);
519 }
520 }
521 session_set_save_handler(
522 array('CakeSession','__open'),
523 array('CakeSession', '__close'),
524 array('CakeSession', '__read'),
525 array('CakeSession', '__write'),
526 array('CakeSession', '__destroy'),
527 array('CakeSession', '__gc')
528 );
529 break;
530 case 'php':
531 if (empty($_SESSION)) {
532 if ($iniSet) {
533 ini_set('session.use_trans_sid', 0);
534 ini_set('session.name', Configure::read('Session.cookie'));
535 ini_set('session.cookie_lifetime', $this->cookieLifeTime);
536 ini_set('session.cookie_path', $this->path);
537 }
538 }
539 break;
540 case 'cache':
541 if (empty($_SESSION)) {
542 if (!class_exists('Cache')) {
543 require LIBS . 'cache.php';
544 }
545 if ($iniSet) {
546 ini_set('session.use_trans_sid', 0);
547 ini_set('url_rewriter.tags', '');
548 ini_set('session.save_handler', 'user');
549 ini_set('session.use_cookies', 1);
550 ini_set('session.name', Configure::read('Session.cookie'));
551 ini_set('session.cookie_lifetime', $this->cookieLifeTime);
552 ini_set('session.cookie_path', $this->path);
553 }
554 }
555 session_set_save_handler(
556 array('CakeSession','__open'),
557 array('CakeSession', '__close'),
558 array('Cache', 'read'),
559 array('Cache', 'write'),
560 array('Cache', 'delete'),
561 array('Cache', 'gc')
562 );
563 break;
564 default:
565 $config = CONFIGS . Configure::read('Session.save') . '.php';
566  
567 if (is_file($config)) {
568 require($config);
569 }
570 break;
571 }
572 }
573  
574 /**
575 * Helper method to start a session
576 *
577 * @access private
578 */
579 function __startSession() {
580 if (headers_sent()) {
581 if (empty($_SESSION)) {
582 $_SESSION = array();
583 }
584 return true;
585 } elseif (!isset($_SESSION)) {
586 session_cache_limiter ("must-revalidate");
587 session_start();
588 header ('P3P: CP="NOI ADM DEV PSAi COM NAV OUR OTRo STP IND DEM"');
589 return true;
590 } else {
591 session_start();
592 return true;
593 }
594 }
595  
596 /**
597 * Helper method to create a new session.
598 *
599 * @return void
600 * @access protected
601 */
602 function _checkValid() {
603 if ($this->read('Config')) {
604 if ((Configure::read('Session.checkAgent') === false || $this->_userAgent == $this->read('Config.userAgent')) && $this->time <= $this->read('Config.time')) {
605 $time = $this->read('Config.time');
606 $this->write('Config.time', $this->sessionTime);
607 if (Configure::read('Security.level') === 'high') {
608 $check = $this->read('Config.timeout');
609 $check -= 1;
610 $this->write('Config.timeout', $check);
611  
612 if (time() > ($time - (Security::inactiveMins() * Configure::read('Session.timeout')) + 2) || $check < 1) {
613 $this->renew();
614 $this->write('Config.timeout', 10);
615 }
616 }
617 $this->valid = true;
618 } else {
619 $this->destroy();
620 $this->valid = false;
621 $this->__setError(1, 'Session Highjacking Attempted !!!');
622 }
623 } else {
624 $this->write('Config.userAgent', $this->_userAgent);
625 $this->write('Config.time', $this->sessionTime);
626 $this->write('Config.timeout', 10);
627 $this->valid = true;
628 $this->__setError(1, 'Session is valid');
629 }
630 }
631  
632 /**
633 * Helper method to restart a session.
634 *
635 * @return void
636 * @access private
637 */
638 function __regenerateId() {
639 $oldSessionId = session_id();
640 if ($oldSessionId) {
641 if (session_id() != ''|| isset($_COOKIE[session_name()])) {
642 setcookie(Configure::read('Session.cookie'), '', time() - 42000, $this->path);
643 }
644 session_regenerate_id(true);
645 if (PHP_VERSION < 5.1) {
646 $sessionPath = session_save_path();
647 if (empty($sessionPath)) {
648 $sessionPath = '/tmp';
649 }
650 $newSessid = session_id();
651  
652 if (function_exists('session_write_close')) {
653 session_write_close();
654 }
655 $this->__initSession();
656 session_id($oldSessionId);
657 session_start();
658 session_destroy();
659 $file = $sessionPath . DS . 'sess_' . $oldSessionId;
660 @unlink($file);
661 $this->__initSession();
662 session_id($newSessid);
663 session_start();
664 }
665 }
666 }
667  
668 /**
669 * Restarts this session.
670 *
671 * @access public
672 */
673 function renew() {
674 $this->__regenerateId();
675 }
676  
677 /**
678 * Helper method to set an internal error message.
679 *
680 * @param integer $errorNumber Number of the error
681 * @param string $errorMessage Description of the error
682 * @return void
683 * @access private
684 */
685 function __setError($errorNumber, $errorMessage) {
686 if ($this->error === false) {
687 $this->error = array();
688 }
689 $this->error[$errorNumber] = $errorMessage;
690 $this->lastError = $errorNumber;
691 }
692  
693 /**
694 * Method called on open of a database session.
695 *
696 * @return boolean Success
697 * @access private
698 */
699 function __open() {
700 return true;
701 }
702  
703 /**
704 * Method called on close of a database session.
705 *
706 * @return boolean Success
707 * @access private
708 */
709 function __close() {
710 $probability = mt_rand(1, 150);
711 if ($probability <= 3) {
712 switch (Configure::read('Session.save')) {
713 case 'cache':
714 Cache::gc();
715 break;
716 default:
717 CakeSession::__gc();
718 break;
719 }
720 }
721 return true;
722 }
723  
724 /**
725 * Method used to read from a database session.
726 *
727 * @param mixed $id The key of the value to read
728 * @return mixed The value of the key or false if it does not exist
729 * @access private
730 */
731 function __read($id) {
732 $model =& ClassRegistry::getObject('Session');
733  
734 $row = $model->find('first', array(
735 'conditions' => array($model->primaryKey => $id)
736 ));
737  
738 if (empty($row[$model->alias]['data'])) {
739 return false;
740 }
741  
742 return $row[$model->alias]['data'];
743 }
744  
745 /**
746 * Helper function called on write for database sessions.
747 *
748 * @param integer $id ID that uniquely identifies session in database
749 * @param mixed $data The value of the data to be saved.
750 * @return boolean True for successful write, false otherwise.
751 * @access private
752 */
753 function __write($id, $data) {
754 if (!$id) {
755 return false;
756 }
757 $expires = time() + Configure::read('Session.timeout') * Security::inactiveMins();
758 $model =& ClassRegistry::getObject('Session');
759 $return = $model->save(array($model->primaryKey => $id) + compact('data', 'expires'));
760 return $return;
761 }
762  
763 /**
764 * Method called on the destruction of a database session.
765 *
766 * @param integer $id ID that uniquely identifies session in database
767 * @return boolean True for successful delete, false otherwise.
768 * @access private
769 */
770 function __destroy($id) {
771 $model =& ClassRegistry::getObject('Session');
772 $return = $model->delete($id);
773  
774 return $return;
775 }
776  
777 /**
778 * Helper function called on gc for database sessions.
779 *
780 * @param integer $expires Timestamp (defaults to current time)
781 * @return boolean Success
782 * @access private
783 */
784 function __gc($expires = null) {
785 $model =& ClassRegistry::getObject('Session');
786  
787 if (!$expires) {
788 $expires = time();
789 }
790  
791 $return = $model->deleteAll(array($model->alias . ".expires <" => $expires), false, false);
792 return $return;
793 }
794 }
795  
796