SessionHandler.php 10.2 KB
Newer Older
1
2
3
4
5
6
7
8
9
10
11
12
13
<?php

/**
 * @file
 * Contains \Drupal\Core\Session\SessionHandler.
 */

namespace Drupal\Core\Session;

use Drupal\Component\Utility\Crypt;
use Drupal\Component\Utility\Settings;
use Drupal\Core\Database\Connection;
use Drupal\Core\Utility\Error;
14
use Symfony\Component\HttpFoundation\RequestStack;
15
16
17
18
19
20
21

/**
 * Default session handler.
 */
class SessionHandler implements \SessionHandlerInterface {

  /**
22
   * The session manager.
23
   *
24
   * @var \Drupal\Core\Session\SessionManagerInterface
25
   */
26
27
28
29
30
31
32
33
  protected $sessionManager;

  /**
   * The request stack.
   *
   * @var \Symfony\Component\HttpFoundation\RequestStack
   */
  protected $requestStack;
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51

  /**
   * The database connection.
   *
   * @var \Drupal\Core\Database\Connection
   */
  protected $connection;

  /**
   * An array containing the sid and data from last read.
   *
   * @var array
   */
  protected $lastRead;

  /**
   * Constructs a new SessionHandler instance.
   *
52
53
54
55
   * @param \Drupal\Core\Session\SessionManagerInterface $session_manager
   *   The session manager.
   * @param \Symfony\Component\HttpFoundation\RequestStack $request_stack
   *   The request stack.
56
57
58
   * @param \Drupal\Core\Database\Connection $connection
   *   The database connection.
   */
59
60
61
  public function __construct(SessionManagerInterface $session_manager, RequestStack $request_stack, Connection $connection) {
    $this->sessionManager = $session_manager;
    $this->requestStack = $request_stack;
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
    $this->connection = $connection;
  }

  /**
   * {@inheritdoc}
   */
  public function open($save_path, $name) {
    return TRUE;
  }

  /**
   * {@inheritdoc}
   */
  public function read($sid) {
    global $user;

    // Handle the case of first time visitors and clients that don't store
    // cookies (eg. web crawlers).
    $insecure_session_name = substr(session_name(), 1);
81
    $cookies = $this->requestStack->getCurrentRequest()->cookies;
82
83
84
85
86
87
88
89
90
91
92
93
    if (!$cookies->has(session_name()) && !$cookies->has($insecure_session_name)) {
      $user = new UserSession();
      return '';
    }

    // Otherwise, if the session is still active, we have a record of the
    // client's session in the database. If it's HTTPS then we are either have a
    // HTTPS session or we are about to log in so we check the sessions table
    // for an anonymous session with the non-HTTPS-only cookie. The session ID
    // that is in the user's cookie is hashed before being stored in the
    // database as a security measure. Thus, we have to hash it to match the
    // database.
94
    if ($this->requestStack->getCurrentRequest()->isSecure()) {
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
      // Try to load a session using the HTTPS-only secure session id.
      $values = $this->connection->query("SELECT u.*, s.* FROM {users} u INNER JOIN {sessions} s ON u.uid = s.uid WHERE s.ssid = :ssid", array(
        ':ssid' => Crypt::hashBase64($sid),
      ))->fetchAssoc();
      if (!$values) {
        // Fallback and try to load the anonymous non-HTTPS session. Use the
        // non-HTTPS session id as the key.
        if ($cookies->has($insecure_session_name)) {
          $values = $this->connection->query("SELECT u.*, s.* FROM {users} u INNER JOIN {sessions} s ON u.uid = s.uid WHERE s.sid = :sid AND s.uid = 0", array(
            ':sid' => Crypt::hashBase64($cookies->get($insecure_session_name)),
          ))->fetchAssoc();
        }
      }
    }
    else {
      // Try to load a session using the non-HTTPS session id.
      $values = $this->connection->query("SELECT u.*, s.* FROM {users} u INNER JOIN {sessions} s ON u.uid = s.uid WHERE s.sid = :sid", array(
        ':sid' => Crypt::hashBase64($sid),
      ))->fetchAssoc();
    }

    // We found the client's session record and they are an authenticated,
    // active user.
    if ($values && $values['uid'] > 0 && $values['status'] == 1) {
      // Add roles element to $user.
      $rids = $this->connection->query("SELECT ur.rid FROM {users_roles} ur WHERE ur.uid = :uid", array(
        ':uid' => $values['uid'],
      ))->fetchCol();
      $values['roles'] = array_merge(array(DRUPAL_AUTHENTICATED_RID), $rids);
      $user = new UserSession($values);
    }
    elseif ($values) {
      // The user is anonymous or blocked. Only preserve two fields from the
      // {sessions} table.
      $user = new UserSession(array(
        'session' => $values['session'],
        'access' => $values['access'],
      ));
    }
    else {
      // The session has expired.
      $user = new UserSession();
    }

    // Store the session that was read for comparison in self::write().
    $this->lastRead = array(
      'sid' => $sid,
      'value' => $user->session,
    );
    return $user->session;
  }

  /**
   * {@inheritdoc}
   */
  public function write($sid, $value) {
    global $user;

    // The exception handler is not active at this point, so we need to do it
    // manually.
    try {
156
      if (!$this->sessionManager->isEnabled()) {
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
        // We don't have anything to do if we are not allowed to save the
        // session.
        return TRUE;
      }
      // Check whether $_SESSION has been changed in this request.
      $is_changed = empty($this->lastRead) || $this->lastRead['sid'] != $sid || $this->lastRead['value'] !== $value;

      // For performance reasons, do not update the sessions table, unless
      // $_SESSION has changed or more than 180 has passed since the last
      // update.
      $needs_update = !$user->getLastAccessedTime() || REQUEST_TIME - $user->getLastAccessedTime() > Settings::get('session_write_interval', 180);

      if ($is_changed || $needs_update) {
        // Either ssid or sid or both will be added from $key below.
        $fields = array(
          'uid' => $user->id(),
173
          'hostname' => $this->requestStack->getCurrentRequest()->getClientIP(),
174
175
176
177
          'session' => $value,
          'timestamp' => REQUEST_TIME,
        );
        // Use the session ID as 'sid' and an empty string as 'ssid' by default.
178
        // read() does not allow empty strings so that's a safe default.
179
180
        $key = array('sid' => Crypt::hashBase64($sid), 'ssid' => '');
        // On HTTPS connections, use the session ID as both 'sid' and 'ssid'.
181
        if ($this->requestStack->getCurrentRequest()->isSecure()) {
182
          $key['ssid'] = Crypt::hashBase64($sid);
183
          $cookies = $this->requestStack->getCurrentRequest()->cookies;
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
          // The "secure pages" setting allows a site to simultaneously use both
          // secure and insecure session cookies. If enabled and both cookies
          // are presented then use both keys. The session ID from the cookie is
          // hashed before being stored in the database as a security measure.
          if (Settings::get('mixed_mode_sessions', FALSE)) {
            $insecure_session_name = substr(session_name(), 1);
            if ($cookies->has($insecure_session_name)) {
              $key['sid'] = Crypt::hashBase64($cookies->get($insecure_session_name));
            }
          }
        }
        elseif (Settings::get('mixed_mode_sessions', FALSE)) {
          unset($key['ssid']);
        }
        $this->connection->merge('sessions')
          ->keys($key)
          ->fields($fields)
          ->execute();
      }
      // Likewise, do not update access time more than once per 180 seconds.
      if ($user->isAuthenticated() && REQUEST_TIME - $user->getLastAccessedTime() > Settings::get('session_write_interval', 180)) {
        $this->connection->update('users')
          ->fields(array(
            'access' => REQUEST_TIME,
          ))
          ->condition('uid', $user->id())
          ->execute();
      }
      return TRUE;
    }
    catch (\Exception $exception) {
      require_once DRUPAL_ROOT . '/core/includes/errors.inc';
      // If we are displaying errors, then do so with no possibility of a
      // further uncaught exception being thrown.
      if (error_displayable()) {
        print '<h1>Uncaught exception thrown in session handler.</h1>';
        print '<p>' . Error::renderExceptionSafe($exception) . '</p><hr />';
      }
      return FALSE;
    }
  }

  /**
   * {@inheritdoc}
   */
  public function close() {
    return TRUE;
  }

  /**
   * {@inheritdoc}
   */
  public function destroy($sid) {
    global $user;

    // Nothing to do if we are not allowed to change the session.
240
    if (!$this->sessionManager->isEnabled()) {
241
242
      return TRUE;
    }
243
    $is_https = $this->requestStack->getCurrentRequest()->isSecure();
244
245
246
247
248
249
    // Delete session data.
    $this->connection->delete('sessions')
      ->condition($is_https ? 'ssid' : 'sid', Crypt::hashBase64($sid))
      ->execute();

    // Reset $_SESSION and $user to prevent a new session from being started
250
    // in \Drupal\Core\Session\SessionManager::save().
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
    $_SESSION = array();
    $user = new AnonymousUserSession();

    // Unset the session cookies.
    $this->deleteCookie(session_name());
    if ($is_https) {
      $this->deleteCookie(substr(session_name(), 1), FALSE);
    }
    elseif (Settings::get('mixed_mode_sessions', FALSE)) {
      $this->deleteCookie('S' . session_name(), TRUE);
    }
    return TRUE;
  }

  /**
   * {@inheritdoc}
   */
  public function gc($lifetime) {
    // Be sure to adjust 'php_value session.gc_maxlifetime' to a large enough
    // value. For example, if you want user sessions to stay in your database
    // for three weeks before deleting them, you need to set gc_maxlifetime
    // to '1814400'. At that value, only after a user doesn't log in after
    // three weeks (1814400 seconds) will his/her session be removed.
    $this->connection->delete('sessions')
      ->condition('timestamp', REQUEST_TIME - $lifetime, '<')
      ->execute();
    return TRUE;
  }

  /**
   * Deletes a session cookie.
   *
   * @param string $name
   *   Name of session cookie to delete.
   * @param bool $secure
   *   Force the secure value of the cookie.
   */
  protected function deleteCookie($name, $secure = NULL) {
289
290
    $cookies = $this->requestStack->getCurrentRequest()->cookies;
    if ($cookies->has($name) || (!$this->requestStack->getCurrentRequest()->isSecure() && $secure === TRUE)) {
291
292
293
294
295
296
297
298
299
300
      $params = session_get_cookie_params();
      if ($secure !== NULL) {
        $params['secure'] = $secure;
      }
      setcookie($name, '', REQUEST_TIME - 3600, $params['path'], $params['domain'], $params['secure'], $params['httponly']);
      $cookies->remove($name);
    }
  }

}