wiki.techinc.nl/tests/phpunit/includes/session/PHPSessionHandlerTest.php
Gergő Tisza a6cdedad8d
Log when SessionManager is emitting cookies
This is very noisy (logs several times in the same request), but
I'm not sure much can be done about that. It is a flaw in
SessionManager, which does call SessionProvider::persist/unpersist
that many times, and relies on cookie deduplication in WebResponse.
But it should give some idea of when cookies are emitted, and does
not log on normal requests (where no cookies are emitted) so it
shouldn't overload the logging backend.

Bug: T264793
Change-Id: I93733d73af1dfcf539a94b17cf5e4de76cc59748
2020-10-07 09:39:23 -07:00

371 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 )
->setMethods( null )
->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, 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, 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' );
\Wikimedia\suppressWarnings();
ini_set( 'session.serialize_handler', $handler );
\Wikimedia\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::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' => [ 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
*/
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 \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 )
->setMethods( null )
->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 ] ],
];
}
}