root/fSession.php

Revision 836, 14.8 kB (checked in by wbond, 2 months ago)

Updated fSession to make sure fSession::enablePersistence() is called after fSession::ignoreSubdomain(), fSession::setLength() and fSession::setPath()

LineHide Line Numbers
1 <?php
2 /**
3  * Wraps the session control functions and the `$_SESSION` superglobal for a more consistent and safer API
4  *
5  * A `Cannot send session cache limiter` warning will be triggered if ::open(),
6  * ::add(), ::clear(), ::delete(), ::get() or ::set() is called after output has
7  * been sent to the browser. To prevent such a warning, explicitly call ::open()
8  * before generating any output.
9  *
10  * @copyright  Copyright (c) 2007-2010 Will Bond, others
11  * @author     Will Bond [wb] <will@flourishlib.com>
12  * @author     Alex Leeds [al] <alex@kingleeds.com>
13  * @license    http://flourishlib.com/license
14  *
15  * @package    Flourish
16  * @link       http://flourishlib.com/fSession
17  *
18  * @version    1.0.0b11
19  * @changes    1.0.0b11  Updated the class to make sure ::enablePersistence() is called after ::ignoreSubdomain(), ::setLength() and ::setPath() [wb, 2010-05-29]
20  * @changes    1.0.0b10  Fixed some documentation bugs [wb, 2010-03-03]
21  * @changes    1.0.0b9   Fixed a bug in ::destroy() where sessions weren't always being properly destroyed [wb, 2009-12-08]
22  * @changes    1.0.0b8   Fixed a bug that made the unit tests fail on PHP 5.1 [wb, 2009-10-27]
23  * @changes    1.0.0b7   Backwards Compatibility Break - Removed the `$prefix` parameter from the methods ::delete(), ::get() and ::set() - added the methods ::add(), ::enablePersistence(), ::regenerateID() [wb+al, 2009-10-23]
24  * @changes    1.0.0b6   Backwards Compatibility Break - the first parameter of ::clear() was removed, use ::delete() instead [wb, 2009-05-08]
25  * @changes    1.0.0b5   Added documentation about session cache limiter warnings [wb, 2009-05-04]
26  * @changes    1.0.0b4   The class now works with existing sessions [wb, 2009-05-04]
27  * @changes    1.0.0b3   Fixed ::clear() to properly handle when `$key` is `NULL` [wb, 2009-02-05]
28  * @changes    1.0.0b2   Made ::open() public, fixed some consistency issues with setting session options through the class [wb, 2009-01-06]
29  * @changes    1.0.0b    The initial implementation [wb, 2007-06-14]
30  */
31 class fSession
32 {
33     // The following constants allow for nice looking callbacks to static methods
34     const add               = 'fSession::add';
35     const clear             = 'fSession::clear';
36     const close             = 'fSession::close';
37     const delete            = 'fSession::delete';
38     const destroy           = 'fSession::destroy';
39     const enablePersistence = 'fSession::enablePersistence';
40     const get               = 'fSession::get';
41     const ignoreSubdomain   = 'fSession::ignoreSubdomain';
42     const open              = 'fSession::open';
43     const regenerateID      = 'fSession::regenerateID';
44     const reset             = 'fSession::reset';
45     const set               = 'fSession::set';
46     const setLength         = 'fSession::setLength';
47     const setPath           = 'fSession::setPath';
48    
49    
50     /**
51     * The length for a normal session
52     *
53     * @var integer
54     */
55     static private $normal_timespan = NULL;
56    
57     /**
58     * If the session is open
59     *
60     * @var boolean
61     */
62     static private $open = FALSE;
63    
64     /**
65     * The length for a persistent session cookie - one that survives browser restarts
66     *
67     * @var integer
68     */
69     static private $persistent_timespan = NULL;
70    
71     /**
72     * If the session ID was regenerated during this script
73     *
74     * @var boolean
75     */
76     static private $regenerated = FALSE;
77    
78    
79     /**
80     * Adds a value to an already-existing array value, or to a new array value
81     *
82     * @param  string $key     The name to access the array under
83     * @param  mixed  $value   The value to add to the array
84     * @return void
85     */
86     static public function add($key, $value)
87     {
88         self::open();
89         if (!isset($_SESSION[$key])) {
90             $_SESSION[$key] = array();
91         }
92         if (!is_array($_SESSION[$key])) {
93             throw new fProgrammerException(
94                 '%1$s was called for the key, %2$s, which is not an array',
95                 __CLASS__ . '::add()',
96                 $key
97             );
98         }
99         $_SESSION[$key][] = $value;
100     }   
101    
102    
103     /**
104     * Removes all session values with the provided prefix
105     *
106     * This method will not remove session variables used by this class, which
107     * are prefixed with `fSession::`.
108     *
109     * @param  string $prefix  The prefix to clear all session values for
110     * @return void
111     */
112     static public function clear($prefix=NULL)
113     {
114         self::open();
115        
116         $session_type    = $_SESSION['fSession::type'];
117         $session_expires = $_SESSION['fSession::expires'];
118        
119         if ($prefix) {
120             foreach ($_SESSION as $key => $value) {
121                 if (strpos($key, $prefix) === 0) {
122                     unset($_SESSION[$key]);
123                 }
124             }
125         } else {
126             $_SESSION = array();       
127         }
128        
129         $_SESSION['fSession::type']    = $session_type;
130         $_SESSION['fSession::expires'] = $session_expires;
131     }
132    
133    
134     /**
135     * Closes the session for writing, allowing other pages to open the session
136     *
137     * @return void
138     */
139     static public function close()
140     {
141         if (!self::$open) { return; }
142        
143         session_write_close();
144         unset($_SESSION);
145         self::$open = FALSE;
146     }
147    
148    
149     /**
150     * Deletes a value from the session
151     *
152     * @param  string $key  The key of the value to delete
153     * @return void
154     */
155     static public function delete($key)
156     {
157         self::open();
158        
159         unset($_SESSION[$key]);
160     }
161    
162    
163     /**
164     * Destroys the session, removing all values
165     *
166     * @return void
167     */
168     static public function destroy()
169     {
170         self::open();
171         $_SESSION = array();
172         if (isset($_COOKIE[session_name()])) {
173             $params = session_get_cookie_params();
174             setcookie(session_name(), '', time()-43200, $params['path'], $params['domain'], $params['secure']);
175         }
176         session_destroy();
177         self::regenerateID();
178     }
179    
180    
181     /**
182     * Changed the session to use a time-based cookie instead of a session-based cookie
183     *
184     * The length of the time-based cookie is controlled by ::setLength(). When
185     * this method is called, a time-based cookie is used to store the session
186     * ID. This means the session can persist browser restarts. Normally, a
187     * session-based cookie is used, which is wiped when a browser restart
188     * occurs.
189     *
190     * This method should be called during the login process and will normally
191     * be controlled by a checkbox or similar where the user can indicate if
192     * they want to stay logged in for an extended period of time.
193     *
194     * @return void
195     */
196     static public function enablePersistence()
197     {
198         if (self::$persistent_timespan === NULL) {
199             throw new fProgrammerException(
200                 'The method %1$s must be called with the %2$s parameter before calling %3$s',
201                 __CLASS__ . '::setLength()',
202                 '$persistent_timespan',
203                 __CLASS__ . '::enablePersistence()'
204             );   
205         }
206        
207         $current_params = session_get_cookie_params();
208        
209         $params = array(
210             self::$persistent_timespan,
211             $current_params['path'],
212             $current_params['domain'],
213             $current_params['secure']
214         );
215        
216         call_user_func_array('session_set_cookie_params', $params);
217        
218         self::open();
219        
220         $_SESSION['fSession::type'] = 'persistent';
221        
222         if (isset($_COOKIE[session_name()])) {
223             self::regenerateID();
224         }
225     }
226    
227    
228     /**
229     * Gets data from the `$_SESSION` superglobal
230     *
231     * @param  string $key            The name to get the value for
232     * @param  mixed  $default_value  The default value to use if the requested key is not set
233     * @return mixed  The data element requested
234     */
235     static public function get($key, $default_value=NULL)
236     {
237         self::open();
238         return (isset($_SESSION[$key])) ? $_SESSION[$key] : $default_value;
239     }
240    
241    
242     /**
243     * Sets the session to run on the main domain, not just the specific subdomain currently being accessed
244     *
245     * This method should be called after any calls to
246     * [http://php.net/session_set_cookie_params `session_set_cookie_params()`].
247     *
248     * @return void
249     */
250     static public function ignoreSubdomain()
251     {
252         if (self::$open || isset($_SESSION)) {
253             throw new fProgrammerException(
254                 '%1$s must be called before any of %2$s, %3$s, %4$s, %5$s, %6$s, %7$s or %8$s',
255                 __CLASS__ . '::ignoreSubdomain()',
256                 __CLASS__ . '::add()',
257                 __CLASS__ . '::clear()',
258                 __CLASS__ . '::enablePersistence()',
259                 __CLASS__ . '::get()',
260                 __CLASS__ . '::open()',
261                 __CLASS__ . '::set()',
262                 'session_start()'
263             );
264         }
265        
266         $current_params = session_get_cookie_params();
267        
268         $params = array(
269             $current_params['lifetime'],
270             $current_params['path'],
271             preg_replace('#.*?([a-z0-9\\-]+\.[a-z]+)$#iD', '.\1', $_SERVER['SERVER_NAME']),
272             $current_params['secure']
273         );
274        
275         call_user_func_array('session_set_cookie_params', $params);
276     }
277    
278    
279     /**
280     * Opens the session for writing, is automatically called by ::clear(), ::get() and ::set()
281     *
282     * A `Cannot send session cache limiter` warning will be triggered if this,
283     * ::add(), ::clear(), ::delete(), ::get() or ::set() is called after output
284     * has been sent to the browser. To prevent such a warning, explicitly call
285     * this method before generating any output.
286     *
287     * @param  boolean $cookie_only_session_id  If the session id should only be allowed via cookie - this is a security issue and should only be set to `FALSE` when absolutely necessary
288     * @return void
289     */
290     static public function open($cookie_only_session_id=TRUE)
291     {
292         if (self::$open) { return; }
293        
294         self::$open = TRUE;
295        
296         if (self::$normal_timespan === NULL) {
297             self::$normal_timespan = ini_get('session.gc_maxlifetime');   
298         }
299        
300         // If the session is already open, we just piggy-back without setting options
301         if (!isset($_SESSION)) {
302             if ($cookie_only_session_id) {
303                 ini_set('session.use_cookies', 1);
304                 ini_set('session.use_only_cookies', 1);
305             }
306             session_start();
307         }
308        
309         // If the session has existed for too long, reset it
310         if (!isset($_SESSION['fSession::expires']) || $_SESSION['fSession::expires'] < $_SERVER['REQUEST_TIME']) {
311             $_SESSION = array();
312             if (isset($_SESSION['fSession::expires'])) {
313                 self::regenerateID();
314             }
315         }
316        
317         if (!isset($_SESSION['fSession::type'])) {
318             $_SESSION['fSession::type'] = 'normal';   
319         }
320        
321         // We store the expiration time for a session to allow for both normal and persistent sessions
322         if ($_SESSION['fSession::type'] == 'persistent' && self::$persistent_timespan) {
323             $_SESSION['fSession::expires'] = $_SERVER['REQUEST_TIME'] + self::$persistent_timespan;
324            
325         } else {
326             $_SESSION['fSession::expires'] = $_SERVER['REQUEST_TIME'] + self::$normal_timespan;   
327         }
328     }
329    
330    
331     /**
332     * Regenerates the session ID, but only once per script execution
333     *
334     * @internal
335      *
336     * @return void
337     */
338     static public function regenerateID()
339     {
340         if (!self::$regenerated){
341             session_regenerate_id();
342             self::$regenerated = TRUE;
343         }
344     }
345    
346    
347     /**
348     * Resets the configuration of the class
349     *
350     * @internal
351      *
352     * @return void
353     */
354     static public function reset()
355     {
356         self::$normal_timespan     = NULL;
357         self::$persistent_timespan = NULL;
358         self::$regenerated         = FALSE;
359         self::destroy();
360         self::close();   
361     }
362    
363    
364     /**
365     * Sets data to the `$_SESSION` superglobal
366     *
367     * @param  string $key     The name to save the value under
368     * @param  mixed  $value   The value to store
369     * @return void
370     */
371     static public function set($key, $value)
372     {
373         self::open();
374         $_SESSION[$key] = $value;
375     }
376    
377    
378     /**
379     * Sets the minimum length of a session - PHP might not clean up the session data right away once this timespan has elapsed
380     *
381     * Please be sure to set a custom session path via ::setPath() to ensure
382     * another site on the server does not garbage collect the session files
383     * from this site!
384     *
385     * Both of the timespan can accept either a integer timespan in seconds,
386     * or an english description of a timespan (e.g. `'30 minutes'`, `'1 hour'`,
387     * `'1 day 2 hours'`).
388     *
389     * @param  string|integer $normal_timespan      The normal, session-based cookie, length for the session
390     * @param  string|integer $persistent_timespan  The persistent, timed-based cookie, length for the session - this is enabled by calling ::enabledPersistence() during login
391     * @return void
392     */
393     static public function setLength($normal_timespan, $persistent_timespan=NULL)
394     {
395         if (self::$open || isset($_SESSION)) {
396             throw new fProgrammerException(
397                 '%1$s must be called before any of %2$s, %3$s, %4$s, %5$s, %6$s, %7$s or %8$s',
398                 __CLASS__ . '::setLength()',
399                 __CLASS__ . '::add()',
400                 __CLASS__ . '::clear()',
401                 __CLASS__ . '::enablePersistence()',
402                 __CLASS__ . '::get()',
403                 __CLASS__ . '::open()',
404                 __CLASS__ . '::set()',
405                 'session_start()'
406             );
407         }
408        
409         $seconds = (!is_numeric($normal_timespan)) ? strtotime($normal_timespan) - time() : $normal_timespan;
410         self::$normal_timespan = $seconds;
411        
412         if ($persistent_timespan) {
413             $seconds = (!is_numeric($persistent_timespan)) ? strtotime($persistent_timespan) - time() : $persistent_timespan;   
414             self::$persistent_timespan = $seconds;
415         }
416        
417         ini_set('session.gc_maxlifetime', $seconds);
418     }
419    
420    
421     /**
422     * Sets the path to store session files in
423     *
424     * This method should always be called with a non-standard directory
425     * whenever ::setLength() is called to ensure that another site on the
426     * server does not garbage collect the session files for this site.
427     *
428     * Standard session directories usually include `/tmp` and `/var/tmp`.
429     *
430     * @param  string|fDirectory $directory  The directory to store session files in
431     * @return void
432     */
433     static public function setPath($directory)
434     {
435         if (self::$open || isset($_SESSION)) {
436             throw new fProgrammerException(
437                 '%1$s must be called before any of %2$s, %3$s, %4$s, %5$s, %6$s, %7$s or %8$s',
438                 __CLASS__ . '::setPath()',
439                 __CLASS__ . '::add()',
440                 __CLASS__ . '::clear()',
441                 __CLASS__ . '::enablePersistence()',
442                 __CLASS__ . '::get()',
443                 __CLASS__ . '::open()',
444                 __CLASS__ . '::set()',
445                 'session_start()'
446             );
447         }
448        
449         if (!$directory instanceof fDirectory) {
450             $directory = new fDirectory($directory);   
451         }
452        
453         if (!$directory->isWritable()) {
454             throw new fEnvironmentException(
455                 'The directory specified, %s, is not writable',
456                 $directory->getPath()
457             );   
458         }
459        
460         session_save_path($directory->getPath());
461     }
462    
463    
464     /**
465     * Forces use as a static class
466     *
467     * @return fSession
468     */
469     private function __construct() { }
470 }
471  
472  
473  
474 /**
475  * Copyright (c) 2007-2010 Will Bond <will@flourishlib.com>, others
476  *
477  * Permission is hereby granted, free of charge, to any person obtaining a copy
478  * of this software and associated documentation files (the "Software"), to deal
479  * in the Software without restriction, including without limitation the rights
480  * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
481  * copies of the Software, and to permit persons to whom the Software is
482  * furnished to do so, subject to the following conditions:
483  *
484  * The above copyright notice and this permission notice shall be included in
485  * all copies or substantial portions of the Software.
486  *
487  * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
488  * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
489  * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
490  * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
491  * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
492  * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
493  * THE SOFTWARE.
494  */