The test sleeps for nine seconds (3 invocations, 3 seconds per invocation) which is difficult to sit through. The test code sets the value of two PHP parameters, session.gc_divisor and session.gc_probability, to 1. This may be to ensure that PHP will invoke the session handler's gc() method when the call is made to session_start() below. But the call to PHPSessionHandler::gc() is immaterial, for two reasons: - PHPSessionHandler::gc() evicts items by calling the deleteObjectsExpiringBefore() on the BagOStuff instance it uses for storage. The only BagOStuff implementation that actually uses that method to evict items is SqlBagOStuff, which we're not using here, and which would be an odd choice of a storage backend for sessions. - PHP calls SessionHandler::gc() _after_ opening the new (or resumed) session and loading its data, so even if deleteObjectsExpiringBefore() actually did anything, it would not influence the result of the test. Bug: T135576 Change-Id: I6e153ec8bfa5972ed45a0e6b7720832692b952fb
369 lines
11 KiB
PHP
369 lines
11 KiB
PHP
<?php
|
|
|
|
namespace MediaWiki\Session;
|
|
|
|
use Psr\Log\LogLevel;
|
|
use MediaWikiTestCase;
|
|
|
|
/**
|
|
* @group Session
|
|
* @covers MediaWiki\Session\PHPSessionHandler
|
|
*/
|
|
class PHPSessionHandlerTest extends MediaWikiTestCase {
|
|
|
|
private function getResetter( &$rProp = null ) {
|
|
$reset = [];
|
|
|
|
// Ignore "headers already sent" warnings during this test
|
|
set_error_handler( function ( $errno, $errstr ) use ( &$warnings ) {
|
|
if ( preg_match( '/headers already sent/', $errstr ) ) {
|
|
return true;
|
|
}
|
|
return false;
|
|
} );
|
|
$reset[] = new \ScopedCallback( 'restore_error_handler' );
|
|
|
|
$rProp = new \ReflectionProperty( PHPSessionHandler::class, 'instance' );
|
|
$rProp->setAccessible( true );
|
|
if ( $rProp->getValue() ) {
|
|
$old = \TestingAccessWrapper::newFromObject( $rProp->getValue() );
|
|
$oldManager = $old->manager;
|
|
$oldStore = $old->store;
|
|
$oldLogger = $old->logger;
|
|
$reset[] = new \ScopedCallback(
|
|
[ PHPSessionHandler::class, 'install' ],
|
|
[ $oldManager, $oldStore, $oldLogger ]
|
|
);
|
|
}
|
|
|
|
return $reset;
|
|
}
|
|
|
|
public function testEnableFlags() {
|
|
$handler = \TestingAccessWrapper::newFromObject(
|
|
$this->getMockBuilder( PHPSessionHandler::class )
|
|
->setMethods( null )
|
|
->disableOriginalConstructor()
|
|
->getMock()
|
|
);
|
|
|
|
$rProp = new \ReflectionProperty( PHPSessionHandler::class, 'instance' );
|
|
$rProp->setAccessible( true );
|
|
$reset = new \ScopedCallback( [ $rProp, 'setValue' ], [ $rProp->getValue() ] );
|
|
$rProp->setValue( $handler );
|
|
|
|
$handler->setEnableFlags( 'enable' );
|
|
$this->assertTrue( $handler->enable );
|
|
$this->assertFalse( $handler->warn );
|
|
$this->assertTrue( PHPSessionHandler::isEnabled() );
|
|
|
|
$handler->setEnableFlags( 'warn' );
|
|
$this->assertTrue( $handler->enable );
|
|
$this->assertTrue( $handler->warn );
|
|
$this->assertTrue( PHPSessionHandler::isEnabled() );
|
|
|
|
$handler->setEnableFlags( 'disable' );
|
|
$this->assertFalse( $handler->enable );
|
|
$this->assertFalse( PHPSessionHandler::isEnabled() );
|
|
|
|
$rProp->setValue( null );
|
|
$this->assertFalse( PHPSessionHandler::isEnabled() );
|
|
}
|
|
|
|
public function testInstall() {
|
|
$reset = $this->getResetter( $rProp );
|
|
$rProp->setValue( null );
|
|
|
|
session_write_close();
|
|
ini_set( 'session.use_cookies', 1 );
|
|
ini_set( 'session.use_trans_sid', 1 );
|
|
|
|
$store = new TestBagOStuff();
|
|
$logger = new \TestLogger();
|
|
$manager = new SessionManager( [
|
|
'store' => $store,
|
|
'logger' => $logger,
|
|
] );
|
|
|
|
$this->assertFalse( PHPSessionHandler::isInstalled() );
|
|
PHPSessionHandler::install( $manager );
|
|
$this->assertTrue( PHPSessionHandler::isInstalled() );
|
|
|
|
$this->assertFalse( wfIniGetBool( 'session.use_cookies' ) );
|
|
$this->assertFalse( wfIniGetBool( 'session.use_trans_sid' ) );
|
|
|
|
$this->assertNotNull( $rProp->getValue() );
|
|
$priv = \TestingAccessWrapper::newFromObject( $rProp->getValue() );
|
|
$this->assertSame( $manager, $priv->manager );
|
|
$this->assertSame( $store, $priv->store );
|
|
$this->assertSame( $logger, $priv->logger );
|
|
}
|
|
|
|
/**
|
|
* @dataProvider provideHandlers
|
|
* @param string $handler php serialize_handler to use
|
|
*/
|
|
public function testSessionHandling( $handler ) {
|
|
$this->hideDeprecated( '$_SESSION' );
|
|
$reset[] = $this->getResetter( $rProp );
|
|
|
|
$this->setMwGlobals( [
|
|
'wgSessionProviders' => [ [ 'class' => 'DummySessionProvider' ] ],
|
|
'wgObjectCacheSessionExpiry' => 2,
|
|
] );
|
|
|
|
$store = new TestBagOStuff();
|
|
$logger = new \TestLogger( true, function ( $m ) {
|
|
// Discard all log events starting with expected prefix
|
|
return preg_match( '/^SessionBackend "\{session\}" /', $m ) ? null : $m;
|
|
} );
|
|
$manager = new SessionManager( [
|
|
'store' => $store,
|
|
'logger' => $logger,
|
|
] );
|
|
PHPSessionHandler::install( $manager );
|
|
$wrap = \TestingAccessWrapper::newFromObject( $rProp->getValue() );
|
|
$reset[] = new \ScopedCallback(
|
|
[ $wrap, 'setEnableFlags' ],
|
|
[ $wrap->enable ? $wrap->warn ? 'warn' : 'enable' : 'disable' ]
|
|
);
|
|
$wrap->setEnableFlags( 'warn' );
|
|
|
|
\MediaWiki\suppressWarnings();
|
|
ini_set( 'session.serialize_handler', $handler );
|
|
\MediaWiki\restoreWarnings();
|
|
if ( ini_get( 'session.serialize_handler' ) !== $handler ) {
|
|
$this->markTestSkipped( "Cannot set session.serialize_handler to \"$handler\"" );
|
|
}
|
|
|
|
// Session IDs for testing
|
|
$sessionA = str_repeat( 'a', 32 );
|
|
$sessionB = str_repeat( 'b', 32 );
|
|
$sessionC = str_repeat( 'c', 32 );
|
|
|
|
// Set up garbage data in the session
|
|
$_SESSION['AuthenticationSessionTest'] = 'bogus';
|
|
|
|
session_id( $sessionA );
|
|
session_start();
|
|
$this->assertSame( [], $_SESSION );
|
|
$this->assertSame( $sessionA, session_id() );
|
|
|
|
// Set some data in the session so we can see if it works.
|
|
$rand = mt_rand();
|
|
$_SESSION['AuthenticationSessionTest'] = $rand;
|
|
$expect = [ 'AuthenticationSessionTest' => $rand ];
|
|
session_write_close();
|
|
$this->assertSame( [
|
|
[ LogLevel::WARNING, 'Something wrote to $_SESSION!' ],
|
|
], $logger->getBuffer() );
|
|
|
|
// Screw up $_SESSION so we can tell the difference between "this
|
|
// worked" and "this did nothing"
|
|
$_SESSION['AuthenticationSessionTest'] = 'bogus';
|
|
|
|
// Re-open the session and see that data was actually reloaded
|
|
session_start();
|
|
$this->assertSame( $expect, $_SESSION );
|
|
|
|
// Make sure session_reset() works too.
|
|
if ( function_exists( 'session_reset' ) ) {
|
|
$_SESSION['AuthenticationSessionTest'] = 'bogus';
|
|
session_reset();
|
|
$this->assertSame( $expect, $_SESSION );
|
|
}
|
|
|
|
// Re-fill the session, then test that session_destroy() works.
|
|
$_SESSION['AuthenticationSessionTest'] = $rand;
|
|
session_write_close();
|
|
session_start();
|
|
$this->assertSame( $expect, $_SESSION );
|
|
session_destroy();
|
|
session_id( $sessionA );
|
|
session_start();
|
|
$this->assertSame( [], $_SESSION );
|
|
session_write_close();
|
|
|
|
// Test that our session handler won't clone someone else's session
|
|
session_id( $sessionB );
|
|
session_start();
|
|
$this->assertSame( $sessionB, session_id() );
|
|
$_SESSION['id'] = 'B';
|
|
session_write_close();
|
|
|
|
session_id( $sessionC );
|
|
session_start();
|
|
$this->assertSame( [], $_SESSION );
|
|
$_SESSION['id'] = 'C';
|
|
session_write_close();
|
|
|
|
session_id( $sessionB );
|
|
session_start();
|
|
$this->assertSame( [ 'id' => 'B' ], $_SESSION );
|
|
session_write_close();
|
|
|
|
session_id( $sessionC );
|
|
session_start();
|
|
$this->assertSame( [ 'id' => 'C' ], $_SESSION );
|
|
session_destroy();
|
|
|
|
session_id( $sessionB );
|
|
session_start();
|
|
$this->assertSame( [ 'id' => 'B' ], $_SESSION );
|
|
|
|
// Test merging between Session and $_SESSION
|
|
session_write_close();
|
|
|
|
$session = $manager->getEmptySession();
|
|
$session->set( 'Unchanged', 'setup' );
|
|
$session->set( 'Unchanged, null', null );
|
|
$session->set( 'Changed in $_SESSION', 'setup' );
|
|
$session->set( 'Changed in Session', 'setup' );
|
|
$session->set( 'Changed in both', 'setup' );
|
|
$session->set( 'Deleted in Session', 'setup' );
|
|
$session->set( 'Deleted in $_SESSION', 'setup' );
|
|
$session->set( 'Deleted in both', 'setup' );
|
|
$session->set( 'Deleted in Session, changed in $_SESSION', 'setup' );
|
|
$session->set( 'Deleted in $_SESSION, changed in Session', 'setup' );
|
|
$session->persist();
|
|
$session->save();
|
|
|
|
session_id( $session->getId() );
|
|
session_start();
|
|
$session->set( 'Added in Session', 'Session' );
|
|
$session->set( 'Added in both', 'Session' );
|
|
$session->set( 'Changed in Session', 'Session' );
|
|
$session->set( 'Changed in both', 'Session' );
|
|
$session->set( 'Deleted in $_SESSION, changed in Session', 'Session' );
|
|
$session->remove( 'Deleted in Session' );
|
|
$session->remove( 'Deleted in both' );
|
|
$session->remove( 'Deleted in Session, changed in $_SESSION' );
|
|
$session->save();
|
|
$_SESSION['Added in $_SESSION'] = '$_SESSION';
|
|
$_SESSION['Added in both'] = '$_SESSION';
|
|
$_SESSION['Changed in $_SESSION'] = '$_SESSION';
|
|
$_SESSION['Changed in both'] = '$_SESSION';
|
|
$_SESSION['Deleted in Session, changed in $_SESSION'] = '$_SESSION';
|
|
unset( $_SESSION['Deleted in $_SESSION'] );
|
|
unset( $_SESSION['Deleted in both'] );
|
|
unset( $_SESSION['Deleted in $_SESSION, changed in Session'] );
|
|
session_write_close();
|
|
|
|
$this->assertEquals( [
|
|
'Added in Session' => 'Session',
|
|
'Added in $_SESSION' => '$_SESSION',
|
|
'Added in both' => 'Session',
|
|
'Unchanged' => 'setup',
|
|
'Unchanged, null' => null,
|
|
'Changed in Session' => 'Session',
|
|
'Changed in $_SESSION' => '$_SESSION',
|
|
'Changed in both' => 'Session',
|
|
'Deleted in Session, changed in $_SESSION' => '$_SESSION',
|
|
'Deleted in $_SESSION, changed in Session' => 'Session',
|
|
], iterator_to_array( $session ) );
|
|
|
|
$session->clear();
|
|
$session->set( 42, 'forty-two' );
|
|
$session->set( 'forty-two', 42 );
|
|
$session->set( 'wrong', 43 );
|
|
$session->persist();
|
|
$session->save();
|
|
|
|
session_start();
|
|
$this->assertArrayHasKey( 'forty-two', $_SESSION );
|
|
$this->assertSame( 42, $_SESSION['forty-two'] );
|
|
$this->assertArrayHasKey( 'wrong', $_SESSION );
|
|
unset( $_SESSION['wrong'] );
|
|
session_write_close();
|
|
|
|
$this->assertEquals( [
|
|
42 => 'forty-two',
|
|
'forty-two' => 42,
|
|
], iterator_to_array( $session ) );
|
|
|
|
// Test that write doesn't break if the session is invalid
|
|
$session = $manager->getEmptySession();
|
|
$session->persist();
|
|
$id = $session->getId();
|
|
unset( $session );
|
|
session_id( $id );
|
|
session_start();
|
|
$this->mergeMwGlobalArrayValue( 'wgHooks', [
|
|
'SessionCheckInfo' => [ function ( &$reason ) {
|
|
$reason = 'Testing';
|
|
return false;
|
|
} ],
|
|
] );
|
|
$this->assertNull( $manager->getSessionById( $id, true ), 'sanity check' );
|
|
session_write_close();
|
|
|
|
$this->mergeMwGlobalArrayValue( 'wgHooks', [
|
|
'SessionCheckInfo' => [],
|
|
] );
|
|
$this->assertNotNull( $manager->getSessionById( $id, true ), 'sanity check' );
|
|
}
|
|
|
|
public static function provideHandlers() {
|
|
return [
|
|
[ 'php' ],
|
|
[ 'php_binary' ],
|
|
[ 'php_serialize' ],
|
|
];
|
|
}
|
|
|
|
/**
|
|
* @dataProvider provideDisabled
|
|
* @expectedException BadMethodCallException
|
|
* @expectedExceptionMessage Attempt to use PHP session management
|
|
*/
|
|
public function testDisabled( $method, $args ) {
|
|
$rProp = new \ReflectionProperty( PHPSessionHandler::class, 'instance' );
|
|
$rProp->setAccessible( true );
|
|
$handler = $this->getMockBuilder( PHPSessionHandler::class )
|
|
->setMethods( null )
|
|
->disableOriginalConstructor()
|
|
->getMock();
|
|
\TestingAccessWrapper::newFromObject( $handler )->setEnableFlags( 'disable' );
|
|
$oldValue = $rProp->getValue();
|
|
$rProp->setValue( $handler );
|
|
$reset = new \ScopedCallback( [ $rProp, 'setValue' ], [ $oldValue ] );
|
|
|
|
call_user_func_array( [ $handler, $method ], $args );
|
|
}
|
|
|
|
public static function provideDisabled() {
|
|
return [
|
|
[ 'open', [ '', '' ] ],
|
|
[ 'read', [ '' ] ],
|
|
[ 'write', [ '', '' ] ],
|
|
[ 'destroy', [ '' ] ],
|
|
];
|
|
}
|
|
|
|
/**
|
|
* @dataProvider provideWrongInstance
|
|
* @expectedException UnexpectedValueException
|
|
* @expectedExceptionMessageRegExp /: Wrong instance called!$/
|
|
*/
|
|
public function testWrongInstance( $method, $args ) {
|
|
$handler = $this->getMockBuilder( PHPSessionHandler::class )
|
|
->setMethods( null )
|
|
->disableOriginalConstructor()
|
|
->getMock();
|
|
\TestingAccessWrapper::newFromObject( $handler )->setEnableFlags( 'enable' );
|
|
|
|
call_user_func_array( [ $handler, $method ], $args );
|
|
}
|
|
|
|
public static function provideWrongInstance() {
|
|
return [
|
|
[ 'open', [ '', '' ] ],
|
|
[ 'close', [] ],
|
|
[ 'read', [ '' ] ],
|
|
[ 'write', [ '', '' ] ],
|
|
[ 'destroy', [ '' ] ],
|
|
[ 'gc', [ 0 ] ],
|
|
];
|
|
}
|
|
|
|
}
|