3 namespace Drupal\system\Tests\Session;
5 use Drupal\simpletest\WebTestBase;
8 * Drupal session handling tests.
12 class SessionTest extends WebTestBase {
19 public static $modules = ['session_test'];
21 protected $dumpHeaders = TRUE;
24 * Tests for \Drupal\Core\Session\WriteSafeSessionHandler::setSessionWritable()
25 * ::isSessionWritable and \Drupal\Core\Session\SessionManager::regenerate().
27 public function testSessionSaveRegenerate() {
28 $session_handler = $this->container->get('session_handler.write_safe');
29 $this->assertTrue($session_handler->isSessionWritable(), 'session_handler->isSessionWritable() initially returns TRUE.');
30 $session_handler->setSessionWritable(FALSE);
31 $this->assertFalse($session_handler->isSessionWritable(), '$session_handler->isSessionWritable() returns FALSE after disabling.');
32 $session_handler->setSessionWritable(TRUE);
33 $this->assertTrue($session_handler->isSessionWritable(), '$session_handler->isSessionWritable() returns TRUE after enabling.');
35 // Test session hardening code from SA-2008-044.
36 $user = $this->drupalCreateUser();
39 $this->sessionReset($user->id());
41 // Make sure the session cookie is set as HttpOnly.
42 $this->drupalLogin($user);
43 $this->assertTrue(preg_match('/HttpOnly/i', $this->drupalGetHeader('Set-Cookie', TRUE)), 'Session cookie is set as HttpOnly.');
44 $this->drupalLogout();
46 // Verify that the session is regenerated if a module calls exit
47 // in hook_user_login().
48 $user->name = 'session_test_user';
50 $this->drupalGet('session-test/id');
52 preg_match('/\s*session_id:(.*)\n/', $this->getRawContent(), $matches);
53 $this->assertTrue(!empty($matches[1]), 'Found session ID before logging in.');
54 $original_session = $matches[1];
56 // We cannot use $this->drupalLogin($user); because we exit in
57 // session_test_user_login() which breaks a normal assertion.
59 'name' => $user->getUsername(),
60 'pass' => $user->pass_raw,
62 $this->drupalPostForm('user/login', $edit, t('Log in'));
63 $this->drupalGet('user');
64 $pass = $this->assertText($user->getUsername(), format_string('Found name: %name', ['%name' => $user->getUsername()]), 'User login');
65 $this->_logged_in = $pass;
67 $this->drupalGet('session-test/id');
69 preg_match('/\s*session_id:(.*)\n/', $this->getRawContent(), $matches);
70 $this->assertTrue(!empty($matches[1]), 'Found session ID after logging in.');
71 $this->assertTrue($matches[1] != $original_session, 'Session ID changed after login.');
75 * Test data persistence via the session_test module callbacks.
77 public function testDataPersistence() {
78 $user = $this->drupalCreateUser([]);
80 $this->sessionReset($user->id());
82 $this->drupalLogin($user);
84 $value_1 = $this->randomMachineName();
85 $this->drupalGet('session-test/set/' . $value_1);
86 $this->assertText($value_1, 'The session value was stored.', 'Session');
87 $this->drupalGet('session-test/get');
88 $this->assertText($value_1, 'Session correctly returned the stored data for an authenticated user.', 'Session');
90 // Attempt to write over val_1. If drupal_save_session(FALSE) is working.
91 // properly, val_1 will still be set.
92 $value_2 = $this->randomMachineName();
93 $this->drupalGet('session-test/no-set/' . $value_2);
94 $this->assertText($value_2, 'The session value was correctly passed to session-test/no-set.', 'Session');
95 $this->drupalGet('session-test/get');
96 $this->assertText($value_1, 'Session data is not saved for drupal_save_session(FALSE).', 'Session');
98 // Switch browser cookie to anonymous user, then back to user 1.
99 $this->sessionReset();
100 $this->sessionReset($user->id());
101 $this->assertText($value_1, 'Session data persists through browser close.', 'Session');
103 // Logout the user and make sure the stored value no longer persists.
104 $this->drupalLogout();
105 $this->sessionReset();
106 $this->drupalGet('session-test/get');
107 $this->assertNoText($value_1, "After logout, previous user's session data is not available.", 'Session');
109 // Now try to store some data as an anonymous user.
110 $value_3 = $this->randomMachineName();
111 $this->drupalGet('session-test/set/' . $value_3);
112 $this->assertText($value_3, 'Session data stored for anonymous user.', 'Session');
113 $this->drupalGet('session-test/get');
114 $this->assertText($value_3, 'Session correctly returned the stored data for an anonymous user.', 'Session');
116 // Try to store data when drupal_save_session(FALSE).
117 $value_4 = $this->randomMachineName();
118 $this->drupalGet('session-test/no-set/' . $value_4);
119 $this->assertText($value_4, 'The session value was correctly passed to session-test/no-set.', 'Session');
120 $this->drupalGet('session-test/get');
121 $this->assertText($value_3, 'Session data is not saved for drupal_save_session(FALSE).', 'Session');
123 // Login, the data should persist.
124 $this->drupalLogin($user);
125 $this->sessionReset($user->id());
126 $this->drupalGet('session-test/get');
127 $this->assertNoText($value_1, 'Session has persisted for an authenticated user after logging out and then back in.', 'Session');
129 // Change session and create another user.
130 $user2 = $this->drupalCreateUser([]);
131 $this->sessionReset($user2->id());
132 $this->drupalLogin($user2);
136 * Tests storing data in Session() object.
138 public function testSessionPersistenceOnLogin() {
139 // Store information via hook_user_login().
140 $user = $this->drupalCreateUser();
141 $this->drupalLogin($user);
142 // Test property added to session object form hook_user_login().
143 $this->drupalGet('session-test/get-from-session-object');
144 $this->assertText('foobar', 'Session data is saved in Session() object.', 'Session');
148 * Test that empty anonymous sessions are destroyed.
150 public function testEmptyAnonymousSession() {
151 // Disable the dynamic_page_cache module; it'd cause session_test's debug
152 // output (that is added in
153 // SessionTestSubscriber::onKernelResponseSessionTest()) to not be added.
154 $this->container->get('module_installer')->uninstall(['dynamic_page_cache']);
156 // Verify that no session is automatically created for anonymous user when
157 // page caching is disabled.
158 $this->container->get('module_installer')->uninstall(['page_cache']);
159 $this->drupalGet('');
160 $this->assertSessionCookie(FALSE);
161 $this->assertSessionEmpty(TRUE);
163 // The same behavior is expected when caching is enabled.
164 $this->container->get('module_installer')->install(['page_cache']);
165 $config = $this->config('system.performance');
166 $config->set('cache.page.max_age', 300);
168 $this->drupalGet('');
169 $this->assertSessionCookie(FALSE);
170 // @todo Reinstate when REQUEST and RESPONSE events fire for cached pages.
171 // $this->assertSessionEmpty(TRUE);
172 $this->assertEqual($this->drupalGetHeader('X-Drupal-Cache'), 'MISS', 'Page was not cached.');
174 // Start a new session by setting a message.
175 $this->drupalGet('session-test/set-message');
176 $this->assertSessionCookie(TRUE);
177 $this->assertTrue($this->drupalGetHeader('Set-Cookie'), 'New session was started.');
179 // Display the message, during the same request the session is destroyed
180 // and the session cookie is unset.
181 $this->drupalGet('');
182 $this->assertSessionCookie(FALSE);
183 $this->assertSessionEmpty(FALSE);
184 $this->assertFalse($this->drupalGetHeader('X-Drupal-Cache'), 'Caching was bypassed.');
185 $this->assertText(t('This is a dummy message.'), 'Message was displayed.');
186 $this->assertTrue(preg_match('/SESS\w+=deleted/', $this->drupalGetHeader('Set-Cookie')), 'Session cookie was deleted.');
188 // Verify that session was destroyed.
189 $this->drupalGet('');
190 $this->assertSessionCookie(FALSE);
191 // @todo Reinstate when REQUEST and RESPONSE events fire for cached pages.
192 // $this->assertSessionEmpty(TRUE);
193 $this->assertNoText(t('This is a dummy message.'), 'Message was not cached.');
194 $this->assertEqual($this->drupalGetHeader('X-Drupal-Cache'), 'HIT', 'Page was cached.');
195 $this->assertFalse($this->drupalGetHeader('Set-Cookie'), 'New session was not started.');
197 // Verify that no session is created if drupal_save_session(FALSE) is called.
198 $this->drupalGet('session-test/set-message-but-dont-save');
199 $this->assertSessionCookie(FALSE);
200 $this->assertSessionEmpty(TRUE);
202 // Verify that no message is displayed.
203 $this->drupalGet('');
204 $this->assertSessionCookie(FALSE);
205 // @todo Reinstate when REQUEST and RESPONSE events fire for cached pages.
206 // $this->assertSessionEmpty(TRUE);
207 $this->assertNoText(t('This is a dummy message.'), 'The message was not saved.');
211 * Test that sessions are only saved when necessary.
213 public function testSessionWrite() {
214 $user = $this->drupalCreateUser([]);
215 $this->drupalLogin($user);
217 $sql = 'SELECT u.access, s.timestamp FROM {users_field_data} u INNER JOIN {sessions} s ON u.uid = s.uid WHERE u.uid = :uid';
218 $times1 = db_query($sql, [':uid' => $user->id()])->fetchObject();
220 // Before every request we sleep one second to make sure that if the session
221 // is saved, its timestamp will change.
223 // Modify the session.
225 $this->drupalGet('session-test/set/foo');
226 $times2 = db_query($sql, [':uid' => $user->id()])->fetchObject();
227 $this->assertEqual($times2->access, $times1->access, 'Users table was not updated.');
228 $this->assertNotEqual($times2->timestamp, $times1->timestamp, 'Sessions table was updated.');
230 // Write the same value again, i.e. do not modify the session.
232 $this->drupalGet('session-test/set/foo');
233 $times3 = db_query($sql, [':uid' => $user->id()])->fetchObject();
234 $this->assertEqual($times3->access, $times1->access, 'Users table was not updated.');
235 $this->assertEqual($times3->timestamp, $times2->timestamp, 'Sessions table was not updated.');
237 // Do not change the session.
239 $this->drupalGet('');
240 $times4 = db_query($sql, [':uid' => $user->id()])->fetchObject();
241 $this->assertEqual($times4->access, $times3->access, 'Users table was not updated.');
242 $this->assertEqual($times4->timestamp, $times3->timestamp, 'Sessions table was not updated.');
244 // Force updating of users and sessions table once per second.
245 $this->settingsSet('session_write_interval', 0);
246 // Write that value also into the test settings.php file.
247 $settings['settings']['session_write_interval'] = (object) [
251 $this->writeSettings($settings);
252 $this->drupalGet('');
253 $times5 = db_query($sql, [':uid' => $user->id()])->fetchObject();
254 $this->assertNotEqual($times5->access, $times4->access, 'Users table was updated.');
255 $this->assertNotEqual($times5->timestamp, $times4->timestamp, 'Sessions table was updated.');
259 * Test that empty session IDs are not allowed.
261 public function testEmptySessionID() {
262 $user = $this->drupalCreateUser([]);
263 $this->drupalLogin($user);
264 $this->drupalGet('session-test/is-logged-in');
265 $this->assertResponse(200, 'User is logged in.');
267 // Reset the sid in {sessions} to a blank string. This may exist in the
268 // wild in some cases, although we normally prevent it from happening.
269 db_query("UPDATE {sessions} SET sid = '' WHERE uid = :uid", [':uid' => $user->id()]);
270 // Send a blank sid in the session cookie, and the session should no longer
271 // be valid. Closing the curl handler will stop the previous session ID
274 $this->additionalCurlOptions[CURLOPT_COOKIE] = rawurlencode($this->getSessionName()) . '=;';
275 $this->drupalGet('session-test/id-from-cookie');
276 $this->assertRaw("session_id:\n", 'Session ID is blank as sent from cookie header.');
277 // Assert that we have an anonymous session now.
278 $this->drupalGet('session-test/is-logged-in');
279 $this->assertResponse(403, 'An empty session ID is not allowed.');
283 * Reset the cookie file so that it refers to the specified user.
286 * User id to set as the active session.
288 public function sessionReset($uid = 0) {
289 // Close the internal browser.
291 $this->loggedInUser = FALSE;
293 // Change cookie file for user.
294 $this->cookieFile = \Drupal::service('stream_wrapper_manager')->getViaScheme('temporary')->getDirectoryPath() . '/cookie.' . $uid . '.txt';
295 $this->additionalCurlOptions[CURLOPT_COOKIEFILE] = $this->cookieFile;
296 $this->additionalCurlOptions[CURLOPT_COOKIESESSION] = TRUE;
297 $this->drupalGet('session-test/get');
298 $this->assertResponse(200, 'Session test module is correctly enabled.', 'Session');
302 * Assert whether the SimpleTest browser sent a session cookie.
304 public function assertSessionCookie($sent) {
306 $this->assertNotNull($this->sessionId, 'Session cookie was sent.');
309 $this->assertNull($this->sessionId, 'Session cookie was not sent.');
314 * Assert whether $_SESSION is empty at the beginning of the request.
316 public function assertSessionEmpty($empty) {
318 $this->assertIdentical($this->drupalGetHeader('X-Session-Empty'), '1', 'Session was empty.');
321 $this->assertIdentical($this->drupalGetHeader('X-Session-Empty'), '0', 'Session was not empty.');