Localisation updates from https://translatewiki.net.
[mediawiki.git] / tests / phpunit / includes / session / PHPSessionHandlerTest.php
blobe215697edf016b75123b4d6046e65657114e92ba
1 <?php
3 namespace MediaWiki\Tests\Session;
5 use BadMethodCallException;
6 use DummySessionProvider;
7 use MediaWiki\MainConfigNames;
8 use MediaWiki\Session\PHPSessionHandler;
9 use MediaWiki\Session\SessionManager;
10 use MediaWikiIntegrationTestCase;
11 use Psr\Log\LogLevel;
12 use TestLogger;
13 use UnexpectedValueException;
14 use Wikimedia\ScopedCallback;
15 use Wikimedia\TestingAccessWrapper;
17 /**
18 * @group Session
19 * @covers \MediaWiki\Session\PHPSessionHandler
21 class PHPSessionHandlerTest extends MediaWikiIntegrationTestCase {
23 private function getResetter( &$staticAccess = null ) {
24 $reset = [];
26 $staticAccess = TestingAccessWrapper::newFromClass( PHPSessionHandler::class );
27 if ( $staticAccess->instance ) {
28 $old = TestingAccessWrapper::newFromObject( $staticAccess->instance );
29 $oldManager = $old->manager;
30 $oldStore = $old->store;
31 $oldLogger = $old->logger;
32 $reset[] = new ScopedCallback(
33 [ PHPSessionHandler::class, 'install' ],
34 [ $oldManager, $oldStore, $oldLogger ]
38 return $reset;
41 public function testEnableFlags() {
42 $handler = TestingAccessWrapper::newFromObject(
43 $this->createPartialMock( PHPSessionHandler::class, [] )
46 $staticAccess = TestingAccessWrapper::newFromClass( PHPSessionHandler::class );
47 $oldValue = $staticAccess->instance;
48 $reset = new ScopedCallback( static function () use ( $staticAccess, $oldValue ) {
49 $staticAccess->instance = $oldValue;
50 } );
51 $staticAccess->instance = $handler;
53 $handler->setEnableFlags( 'enable' );
54 $this->assertTrue( $handler->enable );
55 $this->assertFalse( $handler->warn );
56 $this->assertTrue( PHPSessionHandler::isEnabled() );
58 $handler->setEnableFlags( 'warn' );
59 $this->assertTrue( $handler->enable );
60 $this->assertTrue( $handler->warn );
61 $this->assertTrue( PHPSessionHandler::isEnabled() );
63 $handler->setEnableFlags( 'disable' );
64 $this->assertFalse( $handler->enable );
65 $this->assertFalse( PHPSessionHandler::isEnabled() );
67 $staticAccess->instance = null;
68 $this->assertFalse( PHPSessionHandler::isEnabled() );
71 public function testInstall() {
72 $reset = $this->getResetter( $staticAccess );
73 $staticAccess->instance = null;
75 session_write_close();
76 ini_set( 'session.use_cookies', 1 );
78 $store = new TestBagOStuff();
79 // Tolerate debug message, anything else is unexpected
80 $logger = new TestLogger( false, static function ( $m ) {
81 return preg_match( '/^SessionManager using store/', $m ) ? null : $m;
82 } );
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' ) );
94 $this->assertNotNull( $staticAccess->instance );
95 $priv = TestingAccessWrapper::newFromObject( $staticAccess->instance );
96 $this->assertSame( $manager, $priv->manager );
97 $this->assertSame( $store, $priv->store );
98 $this->assertSame( $logger, $priv->logger );
102 * @dataProvider provideHandlers
103 * @param string $handler php serialize_handler to use
105 public function testSessionHandling( $handler ) {
106 // Tracked under T352913
107 $this->markTestSkippedIfPhp( '>=', '8.3' );
109 $this->hideDeprecated( '$_SESSION' );
110 $reset = $this->getResetter( $staticAccess );
112 $this->overrideConfigValues( [
113 MainConfigNames::SessionProviders => [ [ 'class' => DummySessionProvider::class ] ],
114 MainConfigNames::ObjectCacheSessionExpiry => 2,
115 ] );
117 $store = new TestBagOStuff();
118 $logger = new TestLogger( true, static function ( $m ) {
119 return (
120 // Discard all log events starting with expected prefix
121 preg_match( '/^SessionBackend "\{session\}" /', $m )
122 // Also discard logs from T264793
123 || preg_match( '/^(Persisting|Unpersisting) session (for|due to)/', $m )
124 ) ? null : $m;
125 } );
126 $manager = new SessionManager( [
127 'store' => $store,
128 'logger' => $logger,
129 ] );
130 PHPSessionHandler::install( $manager );
131 $wrap = TestingAccessWrapper::newFromObject( $staticAccess->instance );
132 $reset[] = new ScopedCallback(
133 [ $wrap, 'setEnableFlags' ],
134 [ $wrap->enable ? ( $wrap->warn ? 'warn' : 'enable' ) : 'disable' ]
136 $wrap->setEnableFlags( 'warn' );
138 @ini_set( 'session.serialize_handler', $handler );
139 if ( ini_get( 'session.serialize_handler' ) !== $handler ) {
140 $this->markTestSkipped( "Cannot set session.serialize_handler to \"$handler\"" );
143 // Session IDs for testing
144 $sessionA = str_repeat( 'a', 32 );
145 $sessionB = str_repeat( 'b', 32 );
146 $sessionC = str_repeat( 'c', 32 );
148 // Set up garbage data in the session
149 $_SESSION['AuthenticationSessionTest'] = 'bogus';
151 session_id( $sessionA );
152 session_start();
153 $this->assertSame( [], $_SESSION );
154 $this->assertSame( $sessionA, session_id() );
156 // Set some data in the session so we can see if it works.
157 $rand = mt_rand();
158 $_SESSION['AuthenticationSessionTest'] = $rand;
159 $expect = [ 'AuthenticationSessionTest' => $rand ];
160 session_write_close();
161 $this->assertSame( [
162 [ LogLevel::DEBUG, 'SessionManager using store MediaWiki\Tests\Session\TestBagOStuff' ],
163 [ LogLevel::WARNING, 'Something wrote to $_SESSION!' ],
164 ], $logger->getBuffer() );
166 // Screw up $_SESSION so we can tell the difference between "this
167 // worked" and "this did nothing"
168 $_SESSION['AuthenticationSessionTest'] = 'bogus';
170 // Re-open the session and see that data was actually reloaded
171 session_start();
172 $this->assertSame( $expect, $_SESSION );
174 // Make sure session_reset() works too.
175 $_SESSION['AuthenticationSessionTest'] = 'bogus';
176 session_reset();
177 $this->assertSame( $expect, $_SESSION );
179 // Re-fill the session, then test that session_destroy() works.
180 $_SESSION['AuthenticationSessionTest'] = $rand;
181 session_write_close();
182 session_start();
183 $this->assertSame( $expect, $_SESSION );
184 session_destroy();
185 session_id( $sessionA );
186 session_start();
187 $this->assertSame( [], $_SESSION );
188 session_write_close();
190 // Test that our session handler won't clone someone else's session
191 session_id( $sessionB );
192 session_start();
193 $this->assertSame( $sessionB, session_id() );
194 $_SESSION['id'] = 'B';
195 session_write_close();
197 session_id( $sessionC );
198 session_start();
199 $this->assertSame( [], $_SESSION );
200 $_SESSION['id'] = 'C';
201 session_write_close();
203 session_id( $sessionB );
204 session_start();
205 $this->assertSame( [ 'id' => 'B' ], $_SESSION );
206 session_write_close();
208 session_id( $sessionC );
209 session_start();
210 $this->assertSame( [ 'id' => 'C' ], $_SESSION );
211 session_destroy();
213 session_id( $sessionB );
214 session_start();
215 $this->assertSame( [ 'id' => 'B' ], $_SESSION );
217 // Test merging between Session and $_SESSION
218 session_write_close();
220 $session = $manager->getEmptySession();
221 $session->set( 'Unchanged', 'setup' );
222 $session->set( 'Unchanged, null', null );
223 $session->set( 'Changed in $_SESSION', 'setup' );
224 $session->set( 'Changed in Session', 'setup' );
225 $session->set( 'Changed in both', 'setup' );
226 $session->set( 'Deleted in Session', 'setup' );
227 $session->set( 'Deleted in $_SESSION', 'setup' );
228 $session->set( 'Deleted in both', 'setup' );
229 $session->set( 'Deleted in Session, changed in $_SESSION', 'setup' );
230 $session->set( 'Deleted in $_SESSION, changed in Session', 'setup' );
231 $session->persist();
232 $session->save();
234 session_id( $session->getId() );
235 session_start();
236 $session->set( 'Added in Session', 'Session' );
237 $session->set( 'Added in both', 'Session' );
238 $session->set( 'Changed in Session', 'Session' );
239 $session->set( 'Changed in both', 'Session' );
240 $session->set( 'Deleted in $_SESSION, changed in Session', 'Session' );
241 $session->remove( 'Deleted in Session' );
242 $session->remove( 'Deleted in both' );
243 $session->remove( 'Deleted in Session, changed in $_SESSION' );
244 $session->save();
245 $_SESSION['Added in $_SESSION'] = '$_SESSION';
246 $_SESSION['Added in both'] = '$_SESSION';
247 $_SESSION['Changed in $_SESSION'] = '$_SESSION';
248 $_SESSION['Changed in both'] = '$_SESSION';
249 $_SESSION['Deleted in Session, changed in $_SESSION'] = '$_SESSION';
250 unset( $_SESSION['Deleted in $_SESSION'] );
251 unset( $_SESSION['Deleted in both'] );
252 unset( $_SESSION['Deleted in $_SESSION, changed in Session'] );
253 session_write_close();
255 $this->assertEquals( [
256 'Added in Session' => 'Session',
257 'Added in $_SESSION' => '$_SESSION',
258 'Added in both' => 'Session',
259 'Unchanged' => 'setup',
260 'Unchanged, null' => null,
261 'Changed in Session' => 'Session',
262 'Changed in $_SESSION' => '$_SESSION',
263 'Changed in both' => 'Session',
264 'Deleted in Session, changed in $_SESSION' => '$_SESSION',
265 'Deleted in $_SESSION, changed in Session' => 'Session',
266 ], iterator_to_array( $session ) );
268 $session->clear();
269 $session->set( 42, 'forty-two' );
270 $session->set( 'forty-two', 42 );
271 $session->set( 'wrong', 43 );
272 $session->persist();
273 $session->save();
275 session_start();
276 $this->assertArrayHasKey( 'forty-two', $_SESSION );
277 $this->assertSame( 42, $_SESSION['forty-two'] );
278 $this->assertArrayHasKey( 'wrong', $_SESSION );
279 unset( $_SESSION['wrong'] );
280 session_write_close();
282 $this->assertEquals( [
283 42 => 'forty-two',
284 'forty-two' => 42,
285 ], iterator_to_array( $session ) );
287 // Test that write doesn't break if the session is invalid
288 $session = $manager->getEmptySession();
289 $session->persist();
290 $id = $session->getId();
291 unset( $session );
292 session_id( $id );
293 session_start();
294 $this->setTemporaryHook(
295 'SessionCheckInfo',
296 static function ( &$reason ) {
297 $reason = 'Testing';
298 return false;
301 $this->assertNull( $manager->getSessionById( $id, true ) );
302 session_write_close();
304 $this->clearHook( 'SessionCheckInfo' );
305 $this->assertNotNull( $manager->getSessionById( $id, true ) );
308 public static function provideHandlers() {
309 return [
310 [ 'php' ],
311 [ 'php_binary' ],
312 [ 'php_serialize' ],
317 * @dataProvider provideDisabled
319 public function testDisabled( $method, $args ) {
320 $staticAccess = TestingAccessWrapper::newFromClass( PHPSessionHandler::class );
321 $handler = $this->createPartialMock( PHPSessionHandler::class, [] );
322 TestingAccessWrapper::newFromObject( $handler )->setEnableFlags( 'disable' );
323 $oldValue = $staticAccess->instance;
324 $staticAccess->instance = $handler;
325 $reset = new ScopedCallback( static function () use ( $staticAccess, $oldValue ) {
326 $staticAccess->instance = $oldValue;
327 } );
329 $this->expectException( BadMethodCallException::class );
330 $this->expectExceptionMessage( "Attempt to use PHP session management" );
331 $handler->$method( ...$args );
334 public static function provideDisabled() {
335 return [
336 [ 'open', [ '', '' ] ],
337 [ 'read', [ '' ] ],
338 [ 'write', [ '', '' ] ],
339 [ 'destroy', [ '' ] ],
344 * @dataProvider provideWrongInstance
346 public function testWrongInstance( $method, $args ) {
347 $handler = $this->createPartialMock( PHPSessionHandler::class, [] );
348 TestingAccessWrapper::newFromObject( $handler )->setEnableFlags( 'enable' );
350 $this->expectException( UnexpectedValueException::class );
351 $this->expectExceptionMessageMatches( "/: Wrong instance called!$/" );
352 $handler->$method( ...$args );
355 public static function provideWrongInstance() {
356 return [
357 [ 'open', [ '', '' ] ],
358 [ 'close', [] ],
359 [ 'read', [ '' ] ],
360 [ 'write', [ '', '' ] ],
361 [ 'destroy', [ '' ] ],
362 [ 'gc', [ 0 ] ],