1 <?php
2 /**
3 * Handles all manipulation of the session.
4 *
5 * The static methods are used to manipulate the currently active controller's session.
6 * The instance methods are used to manipulate a particular session. There can be more than one of these created.
7 *
8 * In order to support things like testing, the session is associated with a particular Controller. In normal usage, this is loaded from
9 * and saved to the regular PHP session, but for things like static-page-generation and unit-testing, you can create multiple Controllers,
10 * each with their own session.
11 *
12 * The instance object is basically just a way of manipulating a set of nested maps, and isn't specific to session data.
13 *
14 * <b>Saving Data</b>
15 *
16 * You can write a value to a users session from your PHP code using the static function {@link Session::set()}. You can add this line in any function or file you wish to save the value.
17 *
18 * <code>
19 * Session::set('MyValue', 6);
20 * </code>
21 *
22 * Saves the value of "6" to the MyValue session data. You can also save arrays or serialized objects in session (but note there may be size restrictions as to how much you can save)
23 *
24 * <code>
25 * // save a variable
26 * $var = 1;
27 * Session::set('MyVar', $var);
28 *
29 * // saves an array
30 * Session::set('MyArrayOfValues', array('1','2','3'));
31 *
32 * // saves an object (you'll have to unserialize it back)
33 * $object = new Object();
34 *
35 * Session::set('MyObject', serialize($object));
36 * </code>
37 *
38 * <b>Accessing Data</b>
39 *
40 * Once you have saved a value to the Session you can access it by using the {@link Session::get()} function.
41 * Like the {@link Session::set()} function you can use this anywhere in your PHP files.
42 *
43 * The values in the comments are the values stored from the previous example.
44 *
45 * <code>
46 * function bar() {
47 * $value = Session::get('MyValue'); // $value = 6
48 * $var = Session::get('MyVar'); // $var = 1
49 * $array = Session::get('MyArrayOfValues'); // $array = array(1,2,3)
50 * $object = Session::get('MyObject', unserialize($object)); // $object = Object()
51 * }
52 * </code>
53 *
54 * You can also get all the values in the session at once. This is useful for debugging.
55 *
56 * <code>
57 * Session::getAll(); // returns an array of all the session values.
58 * </code>
59 *
60 * <b>Clearing Data</b>
61 *
62 * Once you have accessed a value from the Session it doesn't automatically wipe the value from the Session, you have to specifically remove it. To clear a value you can either delete 1 session value by the name that you saved it
63 *
64 * <code>
65 * Session::clear('MyValue'); // myvalue is no longer 6.
66 * </code>
67 *
68 * Or you can clear every single value in the session at once. Note SilverStripe stores some of its own session data including form and page comment information. None of this is vital but clear_all will clear everything.
69 *
70 * <code>
71 * Session::clearAll();
72 * </code>
73 *
74 * @see Cookie
75 * @todo This class is currently really basic and could do with a more well-thought-out implementation.
76 *
77 *
78 * @package sapphire
79 * @subpackage control
80 */
81
82 class Session {
83
84 /**
85 * @var $timeout Set session timeout
86 */
87 protected static $timeout = 0;
88
89 protected static $session_ips = array();
90
91 protected static $cookie_domain;
92
93 protected static $cookie_path;
94
95 protected static $cookie_secure = false;
96
97 /**
98 * Session data
99 */
100 protected $data = array();
101
102 protected $changedData = array();
103
104 /**
105 * Cookie domain, for example 'www.php.net'.
106 *
107 * To make cookies visible on all subdomains then the domain
108 * must be prefixed with a dot like '.php.net'.
109 *
110 * @param string $domain The domain to set
111 */
112 public static function set_cookie_domain($domain) {
113 self::$cookie_domain = $domain;
114 }
115
116 /**
117 * Get the cookie domain.
118 * @return string
119 */
120 public static function get_cookie_domain() {
121 return self::$cookie_domain;
122 }
123
124 /**
125 * Path to set on the domain where the session cookie will work.
126 * Use a single slash ('/') for all paths on the domain.
127 *
128 * @param string $path The path to set
129 */
130 public static function set_cookie_path($path) {
131 self::$cookie_path = $path;
132 }
133
134 /**
135 * Get the path on the domain where the session cookie will work.
136 * @return string
137 */
138 public static function get_cookie_path() {
139 if(self::$cookie_path) {
140 return self::$cookie_path;
141 } else {
142 return Director::baseURL();
143 }
144 }
145
146 /**
147 * Secure cookie, tells the browser to only send it over SSL.
148 * @param boolean $secure
149 */
150 public static function set_cookie_secure($secure) {
151 self::$cookie_secure = (bool) $secure;
152 }
153
154 /**
155 * Get if the cookie is secure
156 * @return boolean
157 */
158 public static function get_cookie_secure() {
159 return (bool) self::$cookie_secure;
160 }
161
162 /**
163 * Create a new session object, with the given starting data
164 *
165 * @param $data Can be an array of data (such as $_SESSION) or another Session object to clone.
166 */
167 function __construct($data) {
168 if($data instanceof Session) $data = $data->inst_getAll();
169
170 $this->data = $data;
171 }
172
173 /**
174 * Provide an <code>array</code> of rules specifing timeouts for IPv4 address ranges or
175 * individual IPv4 addresses. The key is an IP address or range and the value is the time
176 * until the session expires in seconds. For example:
177 *
178 * Session::set_timeout_ips(array(
179 * '127.0.0.1' => 36000
180 * ));
181 *
182 * Any user connecting from 127.0.0.1 (localhost) will have their session expired after 10 hours.
183 *
184 * Session::set_timeout is used to set the timeout value for any users whose address is not in the given IP range.
185 *
186 * @param array $session_ips Array of IPv4 rules.
187 */
188 public static function set_timeout_ips($session_ips) {
189 if(!is_array($session_ips)) {
190 user_error("Session::set_timeout_ips expects an array as its argument", E_USER_NOTICE);
191 self::$session_ips = array();
192 } else {
193 self::$session_ips = $session_ips;
194 }
195 }
196
197 /**
198 * @deprecated 2.5 Use Session::add_to_array($name, $val) instead
199 */
200 public static function addToArray($name, $val) {
201 user_error('Session::addToArray() is deprecated. Please use Session::add_to_array() instead.', E_USER_NOTICE);
202
203 return Session::add_to_array($name, $val);
204 }
205
206 /**
207 * Add a value to a specific key in the session array
208 */
209 public static function add_to_array($name, $val) {
210 return self::current_session()->inst_addToArray($name, $val);
211 }
212
213 /**
214 * Set a key/value pair in the session
215 *
216 * @param string $name Key
217 * @param string $val Value
218 */
219 public static function set($name, $val) {
220 return self::current_session()->inst_set($name, $val);
221 }
222
223 /**
224 * Return a specific value by session key
225 *
226 * @param string $name Key to lookup
227 */
228 public static function get($name) {
229 return self::current_session()->inst_get($name);
230 }
231
232 /**
233 * Return all the values in session
234 *
235 * @return Array
236 */
237 public static function get_all() {
238 return self::current_session()->inst_getAll();
239 }
240
241 /**
242 * @deprecated 2.5 Use Session::get_all()
243 */
244 public static function getAll() {
245 user_error('Session::getAll() is deprecated. Please use Session::get_all() instead.', E_USER_NOTICE);
246
247 return Session::get_all();
248 }
249
250 /**
251 * Clear a given session key, value pair.
252 *
253 * @param string $name Key to lookup
254 */
255 public static function clear($name) {
256 return self::current_session()->inst_clear($name);
257 }
258
259 /**
260 * Clear all the values
261 */
262 public static function clear_all() {
263 $ret = self::current_session()->inst_clearAll();
264 self::$default_session = null;
265
266 return $ret;
267 }
268
269 /**
270 * @deprecated 2.5 Use Session::clear_all()
271 */
272 public static function clearAll() {
273 user_error('Session::clearAll() is deprecated. Please use Session::clear_all() instead.', E_USER_NOTICE);
274
275 return Session::clear_all();
276 }
277
278 /**
279 * Save all the values in our session to $_SESSION
280 */
281 public static function save() {
282 return self::current_session()->inst_save();
283 }
284
285 protected static $default_session = null;
286
287 protected static function current_session() {
288 if (Controller::has_curr() && $session = Controller::curr()->getSession()) {
289 return $session;
290 }
291
292 if(!self::$default_session) self::$default_session = new Session(isset($_SESSION) ? $_SESSION : array());
293 return self::$default_session;
294 }
295
296 public function inst_set($name, $val) {
297 // Quicker execution path for "."-free names
298 if(strpos($name,'.') === false) {
299 $this->data[$name] = $val;
300 $this->changedData[$name] = $val;
301
302 } else {
303 $names = explode('.', $name);
304
305 // We still want to do this even if we have strict path checking for legacy code
306 $var = &$this->data;
307 $diffVar = &$this->changedData;
308
309 foreach($names as $n) {
310 $var = &$var[$n];
311 $diffVar = &$diffVar[$n];
312 }
313
314 $var = $val;
315 $diffVar = $val;
316 }
317 }
318
319 public function inst_addToArray($name, $val) {
320 $names = explode('.', $name);
321
322 // We still want to do this even if we have strict path checking for legacy code
323 $var = &$this->data;
324 $diffVar = &$this->changedData;
325
326 foreach($names as $n) {
327 $var = &$var[$n];
328 $diffVar = &$diffVar[$n];
329 }
330
331 $var[] = $val;
332 $diffVar[sizeof($var)-1] = $val;
333 }
334
335 public function inst_get($name) {
336 // Quicker execution path for "."-free names
337 if(strpos($name,'.') === false) {
338 if(isset($this->data[$name])) return $this->data[$name];
339
340 } else {
341 $names = explode('.', $name);
342
343 if(!isset($this->data)) {
344 return null;
345 }
346
347 $var = $this->data;
348
349 foreach($names as $n) {
350 if(!isset($var[$n])) {
351 return null;
352 }
353 $var = $var[$n];
354 }
355
356 return $var;
357 }
358 }
359
360 public function inst_clear($name) {
361 $names = explode('.', $name);
362
363 // We still want to do this even if we have strict path checking for legacy code
364 $var = &$this->data;
365 $diffVar = &$this->changedData;
366
367 foreach($names as $n) {
368 $var = &$var[$n];
369 $diffVar = &$diffVar[$n];
370 }
371
372 $var = null;
373 $diffVar = null;
374 }
375
376 public function inst_clearAll() {
377 if($this->data && is_array($this->data)) {
378 foreach(array_keys($this->data) as $key) {
379 $this->inst_clear($key);
380 }
381 }
382 }
383
384 public function inst_getAll() {
385 return $this->data;
386 }
387
388 /**
389 * Save data to session
390 * Only save the changes, so that anyone manipulating $_SESSION directly doesn't get burned.
391 */
392 public function inst_save() {
393 $this->recursivelyApply($this->changedData, $_SESSION);
394 }
395
396 /**
397 * Recursively apply the changes represented in $data to $dest.
398 * Used to update $_SESSION
399 */
400 protected function recursivelyApply($data, &$dest) {
401 foreach($data as $k => $v) {
402 if(is_array($v)) {
403 if(!isset($dest[$k])) $dest[$k] = array();
404 $this->recursivelyApply($v, $dest[$k]);
405 } else {
406 $dest[$k] = $v;
407 }
408 }
409 }
410
411 /**
412 * Sets the appropriate form message in session, with type. This will be shown once,
413 * for the form specified.
414 *
415 * @param formname the form name you wish to use ( usually $form->FormName() )
416 * @param messsage the message you wish to add to it
417 * @param type the type of message
418 */
419 public static function setFormMessage($formname,$message,$type){
420 Session::set("FormInfo.$formname.message", $message);
421 Session::set("FormInfo.$formname.type", $type);
422 }
423
424 /**
425 * Initialize session.
426 *
427 * @param string $sid Start the session with a specific ID
428 */
429 public static function start($sid = null) {
430 self::load_config();
431 $path = self::get_cookie_path();
432 $domain = self::get_cookie_domain();
433 $secure = self::get_cookie_secure();
434
435 if(!session_id() && !headers_sent()) {
436 if($domain) {
437 session_set_cookie_params(self::$timeout, $path, $domain, $secure /* secure */, true /* httponly */);
438 } else {
439 session_set_cookie_params(self::$timeout, $path, null, $secure /* secure */, true /* httponly */);
440 }
441
442 // @ is to supress win32 warnings/notices when session wasn't cleaned up properly
443 // There's nothing we can do about this, because it's an operating system function!
444 if($sid) session_id($sid);
445 @session_start();
446 }
447 }
448
449 /**
450 * Destroy the active session.
451 *
452 * @param bool $removeCookie If set to TRUE, removes the user's cookie, FALSE does not remove
453 */
454 public static function destroy($removeCookie = true) {
455 if(session_id()) {
456 if($removeCookie) {
457 setcookie(session_name(), '');
458 unset($_COOKIE[session_name()]);
459 }
460 session_destroy();
461 }
462 }
463
464 /**
465 * Use the Session::$session_ips array to set timeouts based on IP address or IP address
466 * range.
467 *
468 * Note: The use of _sessions.php is deprecated.
469 */
470 public static function load_config() {
471 foreach(self::$session_ips as $sessionIP => $timeout) {
472 if(preg_match('/^([0-9.]+)\s?-\s?([0-9.]+)$/', $sessionIP, $ips)) {
473 if(isset($_SERVER['REMOTE_ADDR'])) {
474 $startIP = ip2long($ips[1]);
475 $endIP = ip2long($ips[2]);
476 $clientIP = ip2long($_SERVER['REMOTE_ADDR']);
477 $minIP = min($startIP, $endIP);
478 $maxIP = max($startIP, $endIP);
479
480 if($minIP <= $clientIP && $clientIP <= $maxIP) {
481 return self::set_timeout($timeout);
482 }
483 }
484 }
485 // TODO - Net masks or something
486 }
487 }
488
489 /**
490 * Set the timeout of a Session value
491 *
492 * @param int $timeout Time until a session expires in seconds. Defaults to expire when browser is closed.
493 */
494 public static function set_timeout($timeout) {
495 self::$timeout = intval($timeout);
496 }
497
498 public static function get_timeout() {
499 return self::$timeout;
500 }
501 }
502