session: Add debug message for the used store class
[lhc/web/wiklou.git] / tests / phpunit / includes / session / PHPSessionHandlerTest.php
1 <?php
2
3 namespace MediaWiki\Session;
4
5 use Psr\Log\LogLevel;
6 use MediaWikiTestCase;
7 use Wikimedia\TestingAccessWrapper;
8
9 /**
10 * @group Session
11 * @covers MediaWiki\Session\PHPSessionHandler
12 */
13 class PHPSessionHandlerTest extends MediaWikiTestCase {
14
15 private function getResetter( &$rProp = null ) {
16 $reset = [];
17
18 $rProp = new \ReflectionProperty( PHPSessionHandler::class, 'instance' );
19 $rProp->setAccessible( true );
20 if ( $rProp->getValue() ) {
21 $old = TestingAccessWrapper::newFromObject( $rProp->getValue() );
22 $oldManager = $old->manager;
23 $oldStore = $old->store;
24 $oldLogger = $old->logger;
25 $reset[] = new \Wikimedia\ScopedCallback(
26 [ PHPSessionHandler::class, 'install' ],
27 [ $oldManager, $oldStore, $oldLogger ]
28 );
29 }
30
31 return $reset;
32 }
33
34 public function testEnableFlags() {
35 $handler = TestingAccessWrapper::newFromObject(
36 $this->getMockBuilder( PHPSessionHandler::class )
37 ->setMethods( null )
38 ->disableOriginalConstructor()
39 ->getMock()
40 );
41
42 $rProp = new \ReflectionProperty( PHPSessionHandler::class, 'instance' );
43 $rProp->setAccessible( true );
44 $reset = new \Wikimedia\ScopedCallback( [ $rProp, 'setValue' ], [ $rProp->getValue() ] );
45 $rProp->setValue( $handler );
46
47 $handler->setEnableFlags( 'enable' );
48 $this->assertTrue( $handler->enable );
49 $this->assertFalse( $handler->warn );
50 $this->assertTrue( PHPSessionHandler::isEnabled() );
51
52 $handler->setEnableFlags( 'warn' );
53 $this->assertTrue( $handler->enable );
54 $this->assertTrue( $handler->warn );
55 $this->assertTrue( PHPSessionHandler::isEnabled() );
56
57 $handler->setEnableFlags( 'disable' );
58 $this->assertFalse( $handler->enable );
59 $this->assertFalse( PHPSessionHandler::isEnabled() );
60
61 $rProp->setValue( null );
62 $this->assertFalse( PHPSessionHandler::isEnabled() );
63 }
64
65 public function testInstall() {
66 $reset = $this->getResetter( $rProp );
67 $rProp->setValue( null );
68
69 session_write_close();
70 ini_set( 'session.use_cookies', 1 );
71 ini_set( 'session.use_trans_sid', 1 );
72
73 $store = new TestBagOStuff();
74 // Tolerate debug message, anything else is unexpected
75 $logger = new \TestLogger( false, function ( $m ) {
76 return preg_match( '/^SessionManager using store/', $m ) ? null : $m;
77 } );
78 $manager = new SessionManager( [
79 'store' => $store,
80 'logger' => $logger,
81 ] );
82
83 $this->assertFalse( PHPSessionHandler::isInstalled() );
84 PHPSessionHandler::install( $manager );
85 $this->assertTrue( PHPSessionHandler::isInstalled() );
86
87 $this->assertFalse( wfIniGetBool( 'session.use_cookies' ) );
88 $this->assertFalse( wfIniGetBool( 'session.use_trans_sid' ) );
89
90 $this->assertNotNull( $rProp->getValue() );
91 $priv = TestingAccessWrapper::newFromObject( $rProp->getValue() );
92 $this->assertSame( $manager, $priv->manager );
93 $this->assertSame( $store, $priv->store );
94 $this->assertSame( $logger, $priv->logger );
95 }
96
97 /**
98 * @dataProvider provideHandlers
99 * @param string $handler php serialize_handler to use
100 */
101 public function testSessionHandling( $handler ) {
102 $this->hideDeprecated( '$_SESSION' );
103 $reset[] = $this->getResetter( $rProp );
104
105 $this->setMwGlobals( [
106 'wgSessionProviders' => [ [ 'class' => \DummySessionProvider::class ] ],
107 'wgObjectCacheSessionExpiry' => 2,
108 ] );
109
110 $store = new TestBagOStuff();
111 $logger = new \TestLogger( true, function ( $m ) {
112 // Discard all log events starting with expected prefix
113 return preg_match( '/^SessionBackend "\{session\}" /', $m ) ? null : $m;
114 } );
115 $manager = new SessionManager( [
116 'store' => $store,
117 'logger' => $logger,
118 ] );
119 PHPSessionHandler::install( $manager );
120 $wrap = TestingAccessWrapper::newFromObject( $rProp->getValue() );
121 $reset[] = new \Wikimedia\ScopedCallback(
122 [ $wrap, 'setEnableFlags' ],
123 [ $wrap->enable ? ( $wrap->warn ? 'warn' : 'enable' ) : 'disable' ]
124 );
125 $wrap->setEnableFlags( 'warn' );
126
127 \Wikimedia\suppressWarnings();
128 ini_set( 'session.serialize_handler', $handler );
129 \Wikimedia\restoreWarnings();
130 if ( ini_get( 'session.serialize_handler' ) !== $handler ) {
131 $this->markTestSkipped( "Cannot set session.serialize_handler to \"$handler\"" );
132 }
133
134 // Session IDs for testing
135 $sessionA = str_repeat( 'a', 32 );
136 $sessionB = str_repeat( 'b', 32 );
137 $sessionC = str_repeat( 'c', 32 );
138
139 // Set up garbage data in the session
140 $_SESSION['AuthenticationSessionTest'] = 'bogus';
141
142 session_id( $sessionA );
143 session_start();
144 $this->assertSame( [], $_SESSION );
145 $this->assertSame( $sessionA, session_id() );
146
147 // Set some data in the session so we can see if it works.
148 $rand = mt_rand();
149 $_SESSION['AuthenticationSessionTest'] = $rand;
150 $expect = [ 'AuthenticationSessionTest' => $rand ];
151 session_write_close();
152 $this->assertSame( [
153 [ LogLevel::DEBUG, 'SessionManager using store MediaWiki\Session\TestBagOStuff' ],
154 [ LogLevel::WARNING, 'Something wrote to $_SESSION!' ],
155 ], $logger->getBuffer() );
156
157 // Screw up $_SESSION so we can tell the difference between "this
158 // worked" and "this did nothing"
159 $_SESSION['AuthenticationSessionTest'] = 'bogus';
160
161 // Re-open the session and see that data was actually reloaded
162 session_start();
163 $this->assertSame( $expect, $_SESSION );
164
165 // Make sure session_reset() works too.
166 if ( function_exists( 'session_reset' ) ) {
167 $_SESSION['AuthenticationSessionTest'] = 'bogus';
168 session_reset();
169 $this->assertSame( $expect, $_SESSION );
170 }
171
172 // Re-fill the session, then test that session_destroy() works.
173 $_SESSION['AuthenticationSessionTest'] = $rand;
174 session_write_close();
175 session_start();
176 $this->assertSame( $expect, $_SESSION );
177 session_destroy();
178 session_id( $sessionA );
179 session_start();
180 $this->assertSame( [], $_SESSION );
181 session_write_close();
182
183 // Test that our session handler won't clone someone else's session
184 session_id( $sessionB );
185 session_start();
186 $this->assertSame( $sessionB, session_id() );
187 $_SESSION['id'] = 'B';
188 session_write_close();
189
190 session_id( $sessionC );
191 session_start();
192 $this->assertSame( [], $_SESSION );
193 $_SESSION['id'] = 'C';
194 session_write_close();
195
196 session_id( $sessionB );
197 session_start();
198 $this->assertSame( [ 'id' => 'B' ], $_SESSION );
199 session_write_close();
200
201 session_id( $sessionC );
202 session_start();
203 $this->assertSame( [ 'id' => 'C' ], $_SESSION );
204 session_destroy();
205
206 session_id( $sessionB );
207 session_start();
208 $this->assertSame( [ 'id' => 'B' ], $_SESSION );
209
210 // Test merging between Session and $_SESSION
211 session_write_close();
212
213 $session = $manager->getEmptySession();
214 $session->set( 'Unchanged', 'setup' );
215 $session->set( 'Unchanged, null', null );
216 $session->set( 'Changed in $_SESSION', 'setup' );
217 $session->set( 'Changed in Session', 'setup' );
218 $session->set( 'Changed in both', 'setup' );
219 $session->set( 'Deleted in Session', 'setup' );
220 $session->set( 'Deleted in $_SESSION', 'setup' );
221 $session->set( 'Deleted in both', 'setup' );
222 $session->set( 'Deleted in Session, changed in $_SESSION', 'setup' );
223 $session->set( 'Deleted in $_SESSION, changed in Session', 'setup' );
224 $session->persist();
225 $session->save();
226
227 session_id( $session->getId() );
228 session_start();
229 $session->set( 'Added in Session', 'Session' );
230 $session->set( 'Added in both', 'Session' );
231 $session->set( 'Changed in Session', 'Session' );
232 $session->set( 'Changed in both', 'Session' );
233 $session->set( 'Deleted in $_SESSION, changed in Session', 'Session' );
234 $session->remove( 'Deleted in Session' );
235 $session->remove( 'Deleted in both' );
236 $session->remove( 'Deleted in Session, changed in $_SESSION' );
237 $session->save();
238 $_SESSION['Added in $_SESSION'] = '$_SESSION';
239 $_SESSION['Added in both'] = '$_SESSION';
240 $_SESSION['Changed in $_SESSION'] = '$_SESSION';
241 $_SESSION['Changed in both'] = '$_SESSION';
242 $_SESSION['Deleted in Session, changed in $_SESSION'] = '$_SESSION';
243 unset( $_SESSION['Deleted in $_SESSION'] );
244 unset( $_SESSION['Deleted in both'] );
245 unset( $_SESSION['Deleted in $_SESSION, changed in Session'] );
246 session_write_close();
247
248 $this->assertEquals( [
249 'Added in Session' => 'Session',
250 'Added in $_SESSION' => '$_SESSION',
251 'Added in both' => 'Session',
252 'Unchanged' => 'setup',
253 'Unchanged, null' => null,
254 'Changed in Session' => 'Session',
255 'Changed in $_SESSION' => '$_SESSION',
256 'Changed in both' => 'Session',
257 'Deleted in Session, changed in $_SESSION' => '$_SESSION',
258 'Deleted in $_SESSION, changed in Session' => 'Session',
259 ], iterator_to_array( $session ) );
260
261 $session->clear();
262 $session->set( 42, 'forty-two' );
263 $session->set( 'forty-two', 42 );
264 $session->set( 'wrong', 43 );
265 $session->persist();
266 $session->save();
267
268 session_start();
269 $this->assertArrayHasKey( 'forty-two', $_SESSION );
270 $this->assertSame( 42, $_SESSION['forty-two'] );
271 $this->assertArrayHasKey( 'wrong', $_SESSION );
272 unset( $_SESSION['wrong'] );
273 session_write_close();
274
275 $this->assertEquals( [
276 42 => 'forty-two',
277 'forty-two' => 42,
278 ], iterator_to_array( $session ) );
279
280 // Test that write doesn't break if the session is invalid
281 $session = $manager->getEmptySession();
282 $session->persist();
283 $id = $session->getId();
284 unset( $session );
285 session_id( $id );
286 session_start();
287 $this->mergeMwGlobalArrayValue( 'wgHooks', [
288 'SessionCheckInfo' => [ function ( &$reason ) {
289 $reason = 'Testing';
290 return false;
291 } ],
292 ] );
293 $this->assertNull( $manager->getSessionById( $id, true ), 'sanity check' );
294 session_write_close();
295
296 $this->mergeMwGlobalArrayValue( 'wgHooks', [
297 'SessionCheckInfo' => [],
298 ] );
299 $this->assertNotNull( $manager->getSessionById( $id, true ), 'sanity check' );
300 }
301
302 public static function provideHandlers() {
303 return [
304 [ 'php' ],
305 [ 'php_binary' ],
306 [ 'php_serialize' ],
307 ];
308 }
309
310 /**
311 * @dataProvider provideDisabled
312 * @expectedException BadMethodCallException
313 * @expectedExceptionMessage Attempt to use PHP session management
314 */
315 public function testDisabled( $method, $args ) {
316 $rProp = new \ReflectionProperty( PHPSessionHandler::class, 'instance' );
317 $rProp->setAccessible( true );
318 $handler = $this->getMockBuilder( PHPSessionHandler::class )
319 ->setMethods( null )
320 ->disableOriginalConstructor()
321 ->getMock();
322 TestingAccessWrapper::newFromObject( $handler )->setEnableFlags( 'disable' );
323 $oldValue = $rProp->getValue();
324 $rProp->setValue( $handler );
325 $reset = new \Wikimedia\ScopedCallback( [ $rProp, 'setValue' ], [ $oldValue ] );
326
327 call_user_func_array( [ $handler, $method ], $args );
328 }
329
330 public static function provideDisabled() {
331 return [
332 [ 'open', [ '', '' ] ],
333 [ 'read', [ '' ] ],
334 [ 'write', [ '', '' ] ],
335 [ 'destroy', [ '' ] ],
336 ];
337 }
338
339 /**
340 * @dataProvider provideWrongInstance
341 * @expectedException UnexpectedValueException
342 * @expectedExceptionMessageRegExp /: Wrong instance called!$/
343 */
344 public function testWrongInstance( $method, $args ) {
345 $handler = $this->getMockBuilder( PHPSessionHandler::class )
346 ->setMethods( null )
347 ->disableOriginalConstructor()
348 ->getMock();
349 TestingAccessWrapper::newFromObject( $handler )->setEnableFlags( 'enable' );
350
351 call_user_func_array( [ $handler, $method ], $args );
352 }
353
354 public static function provideWrongInstance() {
355 return [
356 [ 'open', [ '', '' ] ],
357 [ 'close', [] ],
358 [ 'read', [ '' ] ],
359 [ 'write', [ '', '' ] ],
360 [ 'destroy', [ '' ] ],
361 [ 'gc', [ 0 ] ],
362 ];
363 }
364
365 }