Follows-up I361fde0de7f4406bce6ed075ed397effa5be3359. Per T253461, not mass-changing source code, but the use of the native error silencing operator (@) is especially useful in tests because: 1. It requires any/all statements to be explicitly marked. The suppressWarnings/restoreWarnings sections encourage developers to be "lazy" and thus encapsulate more than needed if there are multiple ones near each other, which would ignore potentially important warnings in a test case, which is generally exactly the time when it is really useful to get warnings etc. 2. It avoids leaking state, for example in LBFactoryTest the assertFalse call would throw a PHPUnit assertion error (not meant to be caught by the local catch), and thus won't reach AtEase::restoreWarnings. This then causes later code to end up in a mismatching state and creates a confusing error_reporting state. See .phpcs.xml, where the at operator is allowed for all test code. Change-Id: I68d1725d685e0a7586468bc9de6dc29ceea31b8a
369 lines
11 KiB
PHP
369 lines
11 KiB
PHP
<?php
|
|
|
|
namespace MediaWiki\Session;
|
|
|
|
use BadMethodCallException;
|
|
use MediaWikiIntegrationTestCase;
|
|
use Psr\Log\LogLevel;
|
|
use UnexpectedValueException;
|
|
use Wikimedia\TestingAccessWrapper;
|
|
|
|
/**
|
|
* @group Session
|
|
* @covers MediaWiki\Session\PHPSessionHandler
|
|
*/
|
|
class PHPSessionHandlerTest extends MediaWikiIntegrationTestCase {
|
|
|
|
private function getResetter( &$rProp = null ) {
|
|
$reset = [];
|
|
|
|
$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 \Wikimedia\ScopedCallback(
|
|
[ PHPSessionHandler::class, 'install' ],
|
|
[ $oldManager, $oldStore, $oldLogger ]
|
|
);
|
|
}
|
|
|
|
return $reset;
|
|
}
|
|
|
|
public function testEnableFlags() {
|
|
$handler = TestingAccessWrapper::newFromObject(
|
|
$this->getMockBuilder( PHPSessionHandler::class )
|
|
->onlyMethods( [] )
|
|
->disableOriginalConstructor()
|
|
->getMock()
|
|
);
|
|
|
|
$rProp = new \ReflectionProperty( PHPSessionHandler::class, 'instance' );
|
|
$rProp->setAccessible( true );
|
|
$reset = new \Wikimedia\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();
|
|
// Tolerate debug message, anything else is unexpected
|
|
$logger = new \TestLogger( false, static function ( $m ) {
|
|
return preg_match( '/^SessionManager using store/', $m ) ? null : $m;
|
|
} );
|
|
$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::class ] ],
|
|
'wgObjectCacheSessionExpiry' => 2,
|
|
] );
|
|
|
|
$store = new TestBagOStuff();
|
|
$logger = new \TestLogger( true, static function ( $m ) {
|
|
return (
|
|
// Discard all log events starting with expected prefix
|
|
preg_match( '/^SessionBackend "\{session\}" /', $m )
|
|
// Also discard logs from T264793
|
|
|| preg_match( '/^(Persisting|Unpersisting) session (for|due to)/', $m )
|
|
) ? null : $m;
|
|
} );
|
|
$manager = new SessionManager( [
|
|
'store' => $store,
|
|
'logger' => $logger,
|
|
] );
|
|
PHPSessionHandler::install( $manager );
|
|
$wrap = TestingAccessWrapper::newFromObject( $rProp->getValue() );
|
|
$reset[] = new \Wikimedia\ScopedCallback(
|
|
[ $wrap, 'setEnableFlags' ],
|
|
[ $wrap->enable ? ( $wrap->warn ? 'warn' : 'enable' ) : 'disable' ]
|
|
);
|
|
$wrap->setEnableFlags( 'warn' );
|
|
|
|
@ini_set( 'session.serialize_handler', $handler );
|
|
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::DEBUG, 'SessionManager using store MediaWiki\Session\TestBagOStuff' ],
|
|
[ 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' => [ static function ( &$reason ) {
|
|
$reason = 'Testing';
|
|
return false;
|
|
} ],
|
|
] );
|
|
$this->assertNull( $manager->getSessionById( $id, true ) );
|
|
session_write_close();
|
|
|
|
$this->mergeMwGlobalArrayValue( 'wgHooks', [
|
|
'SessionCheckInfo' => [],
|
|
] );
|
|
$this->assertNotNull( $manager->getSessionById( $id, true ) );
|
|
}
|
|
|
|
public static function provideHandlers() {
|
|
return [
|
|
[ 'php' ],
|
|
[ 'php_binary' ],
|
|
[ 'php_serialize' ],
|
|
];
|
|
}
|
|
|
|
/**
|
|
* @dataProvider provideDisabled
|
|
*/
|
|
public function testDisabled( $method, $args ) {
|
|
$rProp = new \ReflectionProperty( PHPSessionHandler::class, 'instance' );
|
|
$rProp->setAccessible( true );
|
|
$handler = $this->getMockBuilder( PHPSessionHandler::class )
|
|
->onlyMethods( [] )
|
|
->disableOriginalConstructor()
|
|
->getMock();
|
|
TestingAccessWrapper::newFromObject( $handler )->setEnableFlags( 'disable' );
|
|
$oldValue = $rProp->getValue();
|
|
$rProp->setValue( $handler );
|
|
$reset = new \Wikimedia\ScopedCallback( [ $rProp, 'setValue' ], [ $oldValue ] );
|
|
|
|
$this->expectException( BadMethodCallException::class );
|
|
$this->expectExceptionMessage( "Attempt to use PHP session management" );
|
|
$handler->$method( ...$args );
|
|
}
|
|
|
|
public static function provideDisabled() {
|
|
return [
|
|
[ 'open', [ '', '' ] ],
|
|
[ 'read', [ '' ] ],
|
|
[ 'write', [ '', '' ] ],
|
|
[ 'destroy', [ '' ] ],
|
|
];
|
|
}
|
|
|
|
/**
|
|
* @dataProvider provideWrongInstance
|
|
*/
|
|
public function testWrongInstance( $method, $args ) {
|
|
$handler = $this->getMockBuilder( PHPSessionHandler::class )
|
|
->onlyMethods( [] )
|
|
->disableOriginalConstructor()
|
|
->getMock();
|
|
TestingAccessWrapper::newFromObject( $handler )->setEnableFlags( 'enable' );
|
|
|
|
$this->expectException( UnexpectedValueException::class );
|
|
$this->expectExceptionMessageMatches( "/: Wrong instance called!$/" );
|
|
$handler->$method( ...$args );
|
|
}
|
|
|
|
public static function provideWrongInstance() {
|
|
return [
|
|
[ 'open', [ '', '' ] ],
|
|
[ 'close', [] ],
|
|
[ 'read', [ '' ] ],
|
|
[ 'write', [ '', '' ] ],
|
|
[ 'destroy', [ '' ] ],
|
|
[ 'gc', [ 0 ] ],
|
|
];
|
|
}
|
|
|
|
}
|