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