PageRenderTime 57ms CodeModel.GetById 45ms app.highlight 9ms RepoModel.GetById 1ms app.codeStats 0ms

/symphony/lib/core/class.session.php

http://github.com/symphonycms/symphony-2
PHP | 331 lines | 160 code | 33 blank | 138 comment | 23 complexity | 5befe03131915bbe5ac11d7dbcde1093 MD5 | raw file
  1<?php
  2
  3/**
  4 * @package core
  5 */
  6
  7 /**
  8  * The Session class is a handler for all Session related logic in PHP. The functions
  9  * map directly to all handler functions as defined by session_set_save_handler in
 10  * PHP. In Symphony, this function is used in conjunction with the `Cookie` class.
 11  * Based on: http://php.net/manual/en/function.session-set-save-handler.php#81761
 12  * by klose at openriverbed dot de which was based on
 13  * http://php.net/manual/en/function.session-set-save-handler.php#79706 by
 14  * maria at junkies dot jp
 15  *
 16  * @link http://php.net/manual/en/function.session-set-save-handler.php
 17  */
 18
 19class Session
 20{
 21    /**
 22     * If a Session has been created, this will be true, otherwise false
 23     *
 24     * @var boolean
 25     */
 26    private static $_initialized = false;
 27
 28    /**
 29     * Disallow public construction
 30     */
 31    private function __construct()
 32    {
 33    }
 34
 35    /**
 36     * Starts a Session object, only if one doesn't already exist. This function maps
 37     * the Session Handler functions to this classes methods by reading the default
 38     * information from the PHP ini file.
 39     *
 40     * @link http://php.net/manual/en/function.session-set-save-handler.php
 41     * @link http://php.net/manual/en/function.session-set-cookie-params.php
 42     * @param integer $lifetime
 43     *  How long a Session is valid for, by default this is 0, which means it
 44     *  never expires
 45     * @param string $path
 46     *  The path the cookie is valid for on the domain
 47     * @param string $domain
 48     *  The domain this cookie is valid for
 49     * @param boolean $httpOnly
 50     *  Whether this cookie can be read by Javascript. By default the cookie
 51     *  cannot be read by Javascript
 52     * @throws Exception
 53     * @return string|boolean
 54     *  Returns the Session ID on success, or false on error.
 55     */
 56    public static function start($lifetime = 0, $path = '/', $domain = null, $httpOnly = true)
 57    {
 58        if (!self::$_initialized) {
 59            if (!is_object(Symphony::Database()) || !Symphony::Database()->isConnected()) {
 60                throw new Exception('Failed to start session, no Database found.');
 61            }
 62
 63            // Get config
 64            $gcDivisor = Symphony::Configuration()->get('session_gc_divisor', 'symphony');
 65            $strictDomain = Symphony::Configuration()->get('session_strict_domain', 'symphony') === 'yes';
 66
 67            // Set php parameters
 68            if (session_id() == '' && !headers_sent()) {
 69                ini_set('session.use_trans_sid', '0');
 70                ini_set('session.use_strict_mode', '1');
 71                ini_set('session.use_only_cookies', '1');
 72                ini_set('session.gc_maxlifetime', $lifetime);
 73                ini_set('session.gc_probability', '1');
 74                ini_set('session.gc_divisor', $gcDivisor);
 75            }
 76
 77            // Register handler
 78            $handler = new Session;
 79            session_set_save_handler(
 80                [$handler ,'open'],
 81                [$handler ,'close'],
 82                [$handler ,'read'],
 83                [$handler ,'write'],
 84                [$handler ,'destroy'],
 85                [$handler ,'gc']
 86            );
 87
 88            // Set cookie parameters
 89            if ($strictDomain) {
 90                // setting the domain to null makes the cookie valid for the current host only
 91                $domain = null;
 92            } else {
 93                $domain = $domain ? : $handler->getDomain();
 94            }
 95
 96            session_set_cookie_params(
 97                $lifetime,
 98                $handler->createCookieSafePath($path),
 99                $domain,
100                defined('__SECURE__') && __SECURE__,
101                $httpOnly
102            );
103            session_cache_limiter('');
104
105            if (!session_id()) {
106                if (headers_sent()) {
107                    throw new Exception('Headers already sent. Cannot start session.');
108                }
109
110                register_shutdown_function('session_write_close');
111                session_start();
112            }
113
114            self::$_initialized = true;
115        }
116
117        return session_id();
118    }
119
120    /**
121     * Returns a properly formatted ascii string for the cookie path.
122     * Browsers are notoriously bad at parsing the cookie path. They do not
123     * respect the content-encoding header. So we must be careful when dealing
124     * with setups with special characters in their paths.
125     *
126     * @since Symphony 2.7.0
127     **/
128    protected function createCookieSafePath($path)
129    {
130        $path = array_filter(explode('/', $path));
131        if (empty($path)) {
132            return '/';
133        }
134        $path = array_map('rawurlencode', $path);
135        return '/' . implode('/', $path);
136    }
137
138    /**
139     * Returns the current domain for the Session to be saved to, if the installation
140     * is on localhost, this returns null and just allows PHP to take care of setting
141     * the valid domain for the Session, otherwise it will return the non-www version
142     * of the domain host.
143     *
144     * @return string|null
145     *  Null if on localhost, or HTTP_HOST is not set, a string of the domain name sans
146     *  www otherwise
147     */
148    public function getDomain()
149    {
150        if (HTTP_HOST) {
151            if (preg_match('/(localhost|127\.0\.0\.1)/', HTTP_HOST)) {
152                return null; // prevent problems on local setups
153            }
154
155            // Remove leading www and ending :port
156            return preg_replace('/(^www\.|:\d+$)/i', null, HTTP_HOST);
157        }
158
159        return null;
160    }
161
162    /**
163     * Allows the Session to open without any further logic.
164     *
165     * @return boolean
166     *  Always returns true
167     */
168    public function open()
169    {
170        return true;
171    }
172
173    /**
174     * Allows the Session to close without any further logic. Acts as a
175     * destructor function for the Session.
176     *
177     * @return boolean
178     *  Always returns true
179     */
180    public function close()
181    {
182        return true;
183    }
184
185    /**
186     * Given an ID, and some data, save it into `tbl_sessions`. This uses
187     * the ID as a unique key, and will override any existing data. If the
188     * `$data` is deemed to be empty, no row will be saved in the database
189     * unless there is an existing row.
190     *
191     * @param string $id
192     *  The ID of the Session, usually a hash
193     * @param string $data
194     *  The Session information, usually a serialized object of
195     * `$_SESSION[Cookie->_index]`
196     * @throws DatabaseException
197     * @return boolean
198     *  true if the Session information was saved successfully, false otherwise
199     */
200    public function write($id, $data)
201    {
202        // Only prevent this record from saving if there isn't already a record
203        // in the database. This prevents empty Sessions from being created, but
204        // allows them to be nulled.
205        $session_data = $this->read($id);
206        if (!$session_data) {
207            $empty = true;
208            if (function_exists('session_status') && session_status() === PHP_SESSION_ACTIVE) {
209                $unserialized_data = $this->unserialize($data);
210
211                foreach ($unserialized_data as $d) {
212                    if (!empty($d)) {
213                        $empty = false;
214                    }
215                }
216
217                if ($empty) {
218                    return true;
219                }
220            // PHP 7.0 makes the session inactive in write callback,
221            // so we try to detect empty sessions without decoding them
222            } elseif ($data === Symphony::Configuration()->get('cookie_prefix', 'symphony') . '|a:0:{}') {
223                return true;
224            }
225        }
226
227        $fields = array(
228            'session' => $id,
229            'session_expires' => time(),
230            'session_data' => $data
231        );
232
233        return Symphony::Database()
234            ->insert('tbl_sessions')
235            ->values($fields)
236            ->updateOnDuplicateKey()
237            ->execute()
238            ->success();
239    }
240
241    /**
242     * Given raw session data return the unserialized array.
243     * Used to check if the session is really empty before writing.
244     *
245     * @since Symphony 2.3.3
246     * @param string $data
247     *  The serialized session data
248     * @return array
249     *  The unserialised session data
250     */
251    private function unserialize($data)
252    {
253        $hasBuffer = isset($_SESSION);
254        $buffer = $_SESSION;
255        session_decode($data);
256        $session = $_SESSION;
257
258        if ($hasBuffer) {
259            $_SESSION = $buffer;
260        } else {
261            unset($_SESSION);
262        }
263
264        return $session;
265    }
266
267    /**
268     * Given a session's ID, return it's row from `tbl_sessions`
269     *
270     * @param string $id
271     *  The identifier for the Session to fetch
272     * @return string
273     *  The serialised session data
274     */
275    public function read($id)
276    {
277        if (!$id) {
278            return null;
279        }
280
281        return Symphony::Database()
282            ->select(['session_data'])
283            ->from('tbl_sessions')
284            ->where(['session' => $id])
285            ->limit(1)
286            ->execute()
287            ->string('session_data');
288    }
289
290    /**
291     * Given a session's ID, remove it's row from `tbl_sessions`
292     *
293     * @param string $id
294     *  The identifier for the Session to destroy
295     * @throws DatabaseException
296     * @return boolean
297     *  true if the Session was deleted successfully, false otherwise
298     */
299    public function destroy($id)
300    {
301        if (!$id) {
302            return true;
303        }
304
305        return Symphony::Database()
306            ->delete('tbl_sessions')
307            ->where(['session' => $id])
308            ->execute()
309            ->success();
310    }
311
312    /**
313     * The garbage collector, which removes all empty Sessions, or any
314     * Sessions that have expired. This has a 10% chance of firing based
315     * off the `gc_probability`/`gc_divisor`.
316     *
317     * @param integer $max
318     *  The max session lifetime.
319     * @throws DatabaseException
320     * @return boolean
321     *  true on Session deletion, false if an error occurs
322     */
323    public function gc($max)
324    {
325        return Symphony::Database()
326            ->delete('tbl_sessions')
327            ->where(['session_expires' => ['<=' => time() - $max]])
328            ->execute()
329            ->success();
330    }
331}