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