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