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