Add SessionManager

SessionManager is a general-purpose session management framework, rather
than the cookie-based sessions that PHP wants to provide us.

While fallback is provided for using $_SESSION and other PHP session
management functions, they should be avoided in favor of using
SessionManager directly.

For proof-of-concept extensions, see OAuth change Ib40b221 and
CentralAuth change I27ccabdb.

Bug: T111296
Change-Id: Ic1ffea74f3ccc8f93c8a23b795ecab6f06abca72
This commit is contained in:
Brad Jorsch 2015-09-22 10:33:24 -04:00 committed by Gergő Tisza
parent 8ff78d13df
commit a73c5b7395
64 changed files with 9725 additions and 663 deletions

View file

@ -62,6 +62,30 @@ production.
$wgSharedDB and $wgSharedTables are properly set even on the "central" wiki
that all others are sharing from and that $wgLocalDatabases is set to the
full list of sharing wikis on all those wikis.
* Massive overhaul to session handling:
** $wgSessionsInObjectCache is no longer supported and must be true, due to
MediaWiki\Session\SessionManager. $wgSessionHandler is similarly no longer
used.
** ObjectCacheSessionHandler is removed, replaced with
MediaWiki\Session\PhpSessionHandler.
** PHP session handling in general ($_SESSION, session_id(), and so on) is
deprecated. Use MediaWiki\Session\SessionManager instead. A new config
variable, $wgPHPSessionHandling, is available to cause use of $_SESSION to
issue a deprecation warning or to cause most PHP session handling to throw
exceptions.
** Deprecated UserSetCookies hook. Session-handling extensions should generally
be creating a custom subclass of CookieSessionProvider. Other extensions
messing with cookies can no longer count on user data being saved in cookies
versus other methods.
** Deprecated UserLoadFromSession hook, extensions should create a
MediaWiki\Session\SessionProvider.
** The User cannot be loaded from session until after Setup.php completes.
Attempts to do so will be ignored and the User will remain unloaded.
* MediaWiki will now auto-create users as necessary, removing the need for
extensions to do so. An 'autocreateaccount' right is added to allow
auto-creation when 'createaccount' is not granted to all users.
* Deprecated AuthPluginAutoCreate hook in favor of LocalUserCreated.
* Most cookie-handling methods in User are deprecated.
=== New features in 1.27 ===
* $wgDataCenterId and $wgDataCenterRoles where added, which will serve as
@ -106,6 +130,10 @@ production.
* It is now possible to patrol file uploads (both for new files and new versions
of existing files). Special:NewFiles has gained an option to filter by patrol
status. This functionality can be disabled using $wgUseFilePatrol.
* MediaWiki\Session infrastructure allows for easier use of session mechanisms
other than the usual cookies.
** SessionMetadata and SessionCheckInfo hooks allow for setting and checking
custom session metadata.
=== External library changes in 1.27 ===
@ -119,6 +147,7 @@ production.
* Added wikimedia/cldr-plural-rule-parser v1.0.0.
* Added wikimedia/relpath v1.0.3.
* Added wikimedia/running-stat v1.1.0.
* Added wikimedia/php-session-serializer v1.0.3.
==== Removed and replaced external libraries ====

View file

@ -778,6 +778,18 @@ $wgAutoloadLocalClasses = array(
'MediaWiki\\Logger\\Monolog\\WikiProcessor' => __DIR__ . '/includes/debug/logger/monolog/WikiProcessor.php',
'MediaWiki\\Logger\\NullSpi' => __DIR__ . '/includes/debug/logger/NullSpi.php',
'MediaWiki\\Logger\\Spi' => __DIR__ . '/includes/debug/logger/Spi.php',
'MediaWiki\\Session\\CookieSessionProvider' => __DIR__ . '/includes/session/CookieSessionProvider.php',
'MediaWiki\\Session\\ImmutableSessionProviderWithCookie' => __DIR__ . '/includes/session/ImmutableSessionProviderWithCookie.php',
'MediaWiki\\Session\\PHPSessionHandler' => __DIR__ . '/includes/session/PHPSessionHandler.php',
'MediaWiki\\Session\\Session' => __DIR__ . '/includes/session/Session.php',
'MediaWiki\\Session\\SessionBackend' => __DIR__ . '/includes/session/SessionBackend.php',
'MediaWiki\\Session\\SessionId' => __DIR__ . '/includes/session/SessionId.php',
'MediaWiki\\Session\\SessionInfo' => __DIR__ . '/includes/session/SessionInfo.php',
'MediaWiki\\Session\\SessionManager' => __DIR__ . '/includes/session/SessionManager.php',
'MediaWiki\\Session\\SessionManagerInterface' => __DIR__ . '/includes/session/SessionManagerInterface.php',
'MediaWiki\\Session\\SessionProvider' => __DIR__ . '/includes/session/SessionProvider.php',
'MediaWiki\\Session\\SessionProviderInterface' => __DIR__ . '/includes/session/SessionProviderInterface.php',
'MediaWiki\\Session\\UserInfo' => __DIR__ . '/includes/session/UserInfo.php',
'MediaWiki\\Site\\MediaWikiPageNameNormalizer' => __DIR__ . '/includes/site/MediaWikiPageNameNormalizer.php',
'MediaWiki\\Tidy\\Html5Depurate' => __DIR__ . '/includes/tidy/Html5Depurate.php',
'MediaWiki\\Tidy\\RaggettBase' => __DIR__ . '/includes/tidy/RaggettBase.php',
@ -862,7 +874,6 @@ $wgAutoloadLocalClasses = array(
'ORAField' => __DIR__ . '/includes/db/DatabaseOracle.php',
'ORAResult' => __DIR__ . '/includes/db/DatabaseOracle.php',
'ObjectCache' => __DIR__ . '/includes/objectcache/ObjectCache.php',
'ObjectCacheSessionHandler' => __DIR__ . '/includes/objectcache/ObjectCacheSessionHandler.php',
'ObjectFactory' => __DIR__ . '/includes/libs/ObjectFactory.php',
'ObjectFileCache' => __DIR__ . '/includes/cache/ObjectFileCache.php',
'OldChangesList' => __DIR__ . '/includes/changes/OldChangesList.php',

View file

@ -31,6 +31,7 @@
"wikimedia/cldr-plural-rule-parser": "1.0.0",
"wikimedia/composer-merge-plugin": "1.3.0",
"wikimedia/ip-set": "1.0.1",
"wikimedia/php-session-serializer": "1.0.3",
"wikimedia/relpath": "1.0.3",
"wikimedia/running-stat": "1.1.0",
"wikimedia/utfnormal": "1.0.3",

View file

@ -741,8 +741,9 @@ viewing.
redirect was followed.
&$article: target article (object)
'AuthPluginAutoCreate': Called when creating a local account for an user logged
in from an external authentication method.
'AuthPluginAutoCreate': DEPRECATED! Use the 'LocalUserCreated' hook instead.
Called when creating a local account for an user logged in from an external
authentication method.
$user: User object created locally
'AuthPluginSetup': Update or replace authentication plugin object ($wgAuth).
@ -2574,6 +2575,20 @@ $targetUser: the user whom to send watchlist email notification
$title: the page title
$enotif: EmailNotification object
'SessionCheckInfo': Validate a MediaWiki\Session\SessionInfo as it's being
loaded from storage. Return false to prevent it from being used.
&$reason: String rejection reason to be logged
$info: MediaWiki\Session\SessionInfo being validated
$request: WebRequest being loaded from
$metadata: Array|false Metadata array for the MediaWiki\Session\Session
$data: Array|false Data array for the MediaWiki\Session\Session
'SessionMetadata': Add metadata to a session being saved.
$backend: MediaWiki\Session\SessionBackend being saved.
&$metadata: Array Metadata to be stored. Add new keys here.
$requests: Array of WebRequests potentially being saved to. Generally 0-1 real
request and 0+ FauxRequests.
'SetupAfterCache': Called in Setup.php, after cache objects are set
'ShortPagesQuery': Allow extensions to modify the query used by
@ -3292,8 +3307,9 @@ $name: user name
$user: user object
&$s: database query object
'UserLoadFromSession': Called to authenticate users on external/environmental
means; occurs before session is loaded.
'UserLoadFromSession': DEPRECATED! Create a MediaWiki\Session\SessionProvider instead.
Called to authenticate users on external/environmental means; occurs before
session is loaded.
$user: user object being loaded
&$result: set this to a boolean value to abort the normal authentication
process
@ -3384,9 +3400,13 @@ $user: User object
'UserSaveSettings': Called when saving user settings.
$user: User object
'UserSetCookies': Called when setting user cookies.
'UserSetCookies': DEPRECATED! If you're trying to replace core session cookie
handling, you want to create a subclass of MediaWiki\Session\CookieSessionProvider
instead. Otherwise, you can no longer count on user data being saved to cookies
versus some other mechanism.
Called when setting user cookies.
$user: User object
&$session: session array, will be added to $_SESSION
&$session: session array, will be added to the session
&$cookies: cookies array mapping cookie name to its value
'UserSetEmail': Called when changing user email address.

View file

@ -2181,7 +2181,7 @@ $wgMessageCacheType = CACHE_ANYTHING;
$wgParserCacheType = CACHE_ANYTHING;
/**
* The cache type for storing session data. Used if $wgSessionsInObjectCache is true.
* The cache type for storing session data.
*
* For available types see $wgMainCacheType.
*/
@ -2316,30 +2316,29 @@ $wgParserCacheExpireTime = 86400;
*
* @deprecated since 1.20; Use $wgSessionsInObjectCache
*/
$wgSessionsInMemcached = false;
$wgSessionsInMemcached = true;
/**
* Store sessions in an object cache, configured by $wgSessionCacheType. This
* can be useful to improve performance, or to avoid the locking behavior of
* PHP's default session handler, which tends to prevent multiple requests for
* the same user from acting concurrently.
* @deprecated since 1.27, session data is always stored in object cache.
*/
$wgSessionsInObjectCache = false;
$wgSessionsInObjectCache = true;
/**
* The expiry time to use for session storage when $wgSessionsInObjectCache is
* enabled, in seconds.
* The expiry time to use for session storage, in seconds.
*/
$wgObjectCacheSessionExpiry = 3600;
/**
* This is used for setting php's session.save_handler. In practice, you will
* almost never need to change this ever. Other options might be 'user' or
* 'session_mysql.' Setting to null skips setting this entirely (which might be
* useful if you're doing cross-application sessions, see bug 11381)
* @deprecated since 1.27, MediaWiki\\Session\\SessionManager doesn't use PHP session storage.
*/
$wgSessionHandler = null;
/**
* Whether to use PHP session handling ($_SESSION and session_*() functions)
* @var string 'enable', 'warn', or 'disable'
*/
$wgPHPSessionHandling = 'enable';
/**
* If enabled, will send MemCached debugging information to $wgDebugLogFile
*/
@ -4660,6 +4659,24 @@ $wgUserrightsInterwikiDelimiter = '@';
*/
$wgSecureLogin = false;
/**
* MediaWiki\Session\SessionProvider configuration.
*
* Value is an array of ObjectFactory specifications for the SessionProviders
* to be used. Keys in the array are ignored. Order is not significant.
*
* @since 1.27
*/
$wgSessionProviders = array(
'MediaWiki\\Session\\CookieSessionProvider' => array(
'class' => 'MediaWiki\\Session\\CookieSessionProvider',
'args' => array( array(
'priority' => 30,
'callUserSetCookiesHook' => true,
) ),
),
);
/** @} */ # end user accounts }
/************************************************************************//**

View file

@ -61,6 +61,10 @@ class DerivativeRequest extends FauxRequest {
return $this->base->getAllHeaders();
}
public function getSession() {
return $this->base->getSession();
}
public function getSessionData( $key ) {
return $this->base->getSessionData( $key );
}

View file

@ -23,6 +23,8 @@
* @file
*/
use MediaWiki\Session\SessionManager;
/**
* WebRequest clone which takes values from a provided array.
*
@ -30,7 +32,6 @@
*/
class FauxRequest extends WebRequest {
private $wasPosted = false;
private $session = array();
private $requestUrl;
protected $cookies = array();
@ -38,7 +39,8 @@ class FauxRequest extends WebRequest {
* @param array $data Array of *non*-urlencoded key => value pairs, the
* fake GET/POST values
* @param bool $wasPosted Whether to treat the data as POST
* @param array|null $session Session array or null
* @param MediaWiki\\Session\\Session|array|null $session Session, session
* data array, or null
* @param string $protocol 'http' or 'https'
* @throws MWException
*/
@ -53,8 +55,16 @@ class FauxRequest extends WebRequest {
throw new MWException( "FauxRequest() got bogus data" );
}
$this->wasPosted = $wasPosted;
if ( $session ) {
$this->session = $session;
if ( $session instanceof MediaWiki\Session\Session ) {
$this->sessionId = $session->getSessionId();
} elseif ( is_array( $session ) ) {
$mwsession = SessionManager::singleton()->getEmptySession( $this );
$this->sessionId = $mwsession->getSessionId();
foreach ( $session as $key => $value ) {
$mwsession->set( $key, $value );
}
} elseif ( $session !== null ) {
throw new MWException( "FauxRequest() got bogus session" );
}
$this->protocol = $protocol;
}
@ -140,10 +150,6 @@ class FauxRequest extends WebRequest {
}
}
public function checkSessionCookie() {
return false;
}
/**
* @since 1.25
*/
@ -186,31 +192,15 @@ class FauxRequest extends WebRequest {
}
/**
* @param string $key
* @return array|null
*/
public function getSessionData( $key ) {
if ( isset( $this->session[$key] ) ) {
return $this->session[$key];
public function getSessionArray() {
if ( $this->sessionId !== null ) {
return iterator_to_array( $this->getSession() );
}
return null;
}
/**
* @param string $key
* @param array $data
*/
public function setSessionData( $key, $data ) {
$this->session[$key] = $data;
}
/**
* @return array|mixed|null
*/
public function getSessionArray() {
return $this->session;
}
/**
* FauxRequests shouldn't depend on raw request data (but that could be implemented here)
* @return string

View file

@ -26,6 +26,7 @@ if ( !defined( 'MEDIAWIKI' ) ) {
use Liuggio\StatsdClient\Sender\SocketSender;
use MediaWiki\Logger\LoggerFactory;
use MediaWiki\Session\SessionManager;
// Hide compatibility functions from Doxygen
/// @cond
@ -3007,9 +3008,12 @@ function wfBaseConvert( $input, $sourceBase, $destBase, $pad = 1,
/**
* Check if there is sufficient entropy in php's built-in session generation
*
* @deprecated since 1.27, PHP's session generation isn't used with
* MediaWiki\\Session\\SessionManager
* @return bool True = there is sufficient entropy
*/
function wfCheckEntropy() {
wfDeprecated( __FUNCTION__, '1.27' );
return (
( wfIsWindows() && version_compare( PHP_VERSION, '5.3.3', '>=' ) )
|| ini_get( 'session.entropy_file' )
@ -3018,83 +3022,65 @@ function wfCheckEntropy() {
}
/**
* Override session_id before session startup if php's built-in
* session generation code is not secure.
* @deprecated since 1.27, PHP's session generation isn't used with
* MediaWiki\\Session\\SessionManager
*/
function wfFixSessionID() {
// If the cookie or session id is already set we already have a session and should abort
if ( isset( $_COOKIE[session_name()] ) || session_id() ) {
return;
}
// PHP's built-in session entropy is enabled if:
// - entropy_file is set or you're on Windows with php 5.3.3+
// - AND entropy_length is > 0
// We treat it as disabled if it doesn't have an entropy length of at least 32
$entropyEnabled = wfCheckEntropy();
// If built-in entropy is not enabled or not sufficient override PHP's
// built in session id generation code
if ( !$entropyEnabled ) {
wfDebug( __METHOD__ . ": PHP's built in entropy is disabled or not sufficient, " .
"overriding session id generation using our cryptrand source.\n" );
session_id( MWCryptRand::generateHex( 32 ) );
}
wfDeprecated( __FUNCTION__, '1.27' );
}
/**
* Reset the session_id
* Reset the session id
*
* @deprecated since 1.27, use MediaWiki\\Session\\SessionManager instead
* @since 1.22
*/
function wfResetSessionID() {
global $wgCookieSecure;
$oldSessionId = session_id();
$cookieParams = session_get_cookie_params();
if ( wfCheckEntropy() && $wgCookieSecure == $cookieParams['secure'] ) {
session_regenerate_id( false );
} else {
$tmp = $_SESSION;
session_destroy();
wfSetupSession( MWCryptRand::generateHex( 32 ) );
$_SESSION = $tmp;
wfDeprecated( __FUNCTION__, '1.27' );
$session = SessionManager::getGlobalSession();
$delay = $session->delaySave();
$session->resetId();
// Make sure a session is started, since that's what the old
// wfResetSessionID() did.
if ( session_id() !== $session->getId() ) {
wfSetupSession( $session->getId() );
}
$newSessionId = session_id();
ScopedCallback::consume( $delay );
}
/**
* Initialise php session
*
* @param bool $sessionId
* @deprecated since 1.27, use MediaWiki\\Session\\SessionManager instead.
* Generally, "using" SessionManager will be calling ->getSessionById() or
* ::getGlobalSession() (depending on whether you were passing $sessionId
* here), then calling $session->persist().
* @param bool|string $sessionId
*/
function wfSetupSession( $sessionId = false ) {
global $wgSessionsInObjectCache, $wgSessionHandler;
global $wgCookiePath, $wgCookieDomain, $wgCookieSecure, $wgCookieHttpOnly;
wfDeprecated( __FUNCTION__, '1.27' );
if ( $wgSessionsInObjectCache ) {
ObjectCacheSessionHandler::install();
} elseif ( $wgSessionHandler && $wgSessionHandler != ini_get( 'session.save_handler' ) ) {
# Only set this if $wgSessionHandler isn't null and session.save_handler
# hasn't already been set to the desired value (that causes errors)
ini_set( 'session.save_handler', $wgSessionHandler );
// If they're calling this, they probably want our session management even
// if NO_SESSION was set for Setup.php.
if ( !MediaWiki\Session\PHPSessionHandler::isInstalled() ) {
MediaWiki\Session\PHPSessionHandler::install( SessionManager::singleton() );
}
session_set_cookie_params(
0, $wgCookiePath, $wgCookieDomain, $wgCookieSecure, $wgCookieHttpOnly );
session_cache_limiter( 'private, must-revalidate' );
if ( $sessionId ) {
session_id( $sessionId );
} else {
wfFixSessionID();
}
MediaWiki\suppressWarnings();
session_start();
MediaWiki\restoreWarnings();
$session = SessionManager::getGlobalSession();
$session->persist();
if ( $wgSessionsInObjectCache ) {
ObjectCacheSessionHandler::renewCurrentSession();
if ( session_id() !== $session->getId() ) {
session_id( $session->getId() );
}
MediaWiki\quietCall( 'session_start' );
}
/**

View file

@ -664,8 +664,10 @@ class MediaWiki {
if (
$request->getProtocol() == 'http' &&
(
$request->getSession()->shouldForceHTTPS() ||
// Check the cookie manually, for paranoia
$request->getCookie( 'forceHTTPS', '' ) ||
// check for prefixed version for currently logged in users
// check for prefixed version that was used for a time in older MW versions
$request->getCookie( 'forceHTTPS' ) ||
// Avoid checking the user and groups unless it's enabled.
(

View file

@ -21,6 +21,7 @@
*/
use MediaWiki\Logger\LoggerFactory;
use MediaWiki\Session\SessionManager;
use WrappedString\WrappedString;
/**
@ -1977,11 +1978,9 @@ class OutputPage extends ContextSource {
if ( $cookies === null ) {
$config = $this->getConfig();
$cookies = array_merge(
SessionManager::singleton()->getVaryCookies(),
array(
$config->get( 'CookiePrefix' ) . 'Token',
$config->get( 'CookiePrefix' ) . 'LoggedOut',
"forceHTTPS",
session_name()
'forceHTTPS',
),
$config->get( 'CacheVaryCookies' )
);
@ -2033,6 +2032,9 @@ class OutputPage extends ContextSource {
* @return string
*/
public function getVaryHeader() {
foreach ( SessionManager::singleton()->getVaryHeaders() as $header => $options ) {
$this->addVaryHeader( $header, $options );
}
return 'Vary: ' . join( ', ', array_keys( $this->mVaryHeader ) );
}
@ -2050,6 +2052,10 @@ class OutputPage extends ContextSource {
}
$this->addVaryHeader( 'Cookie', $cookiesOption );
foreach ( SessionManager::singleton()->getVaryHeaders() as $header => $options ) {
$this->addVaryHeader( $header, $options );
}
$headers = array();
foreach ( $this->mVaryHeader as $header => $option ) {
$newheader = $header;
@ -2173,8 +2179,8 @@ class OutputPage extends ContextSource {
if ( $this->mEnableClientCache ) {
if (
$config->get( 'UseSquid' ) && session_id() == '' && !$this->isPrintable() &&
$this->mCdnMaxage != 0 && !$this->haveCacheVaryCookies()
$config->get( 'UseSquid' ) && !SessionManager::getGlobalSession()->isPersistent() &&
!$this->isPrintable() && $this->mCdnMaxage != 0 && !$this->haveCacheVaryCookies()
) {
if ( $config->get( 'UseESI' ) ) {
# We'll purge the proxy cache explicitly, but require end user agents

View file

@ -497,10 +497,25 @@ if ( $wgMaximalPasswordLength !== false ) {
$wgPasswordPolicy['policies']['default']['MaximalPasswordLength'] = $wgMaximalPasswordLength;
}
// Backwards compatibility with deprecated alias
// Must be before call to wfSetupSession()
if ( $wgSessionsInMemcached ) {
$wgSessionsInObjectCache = true;
// Backwards compatibility warning
if ( !$wgSessionsInObjectCache && !$wgSessionsInMemcached ) {
wfDeprecated( '$wgSessionsInObjectCache = false', '1.27' );
if ( $wgSessionHandler ) {
wfDeprecated( '$wgSessionsHandler', '1.27' );
}
$cacheType = get_class( ObjectCache::getInstance( $wgSessionCacheType ) );
wfDebugLog(
"Session data will be stored in \"$cacheType\" cache with " .
"expiry $wgObjectCacheSessionExpiry seconds"
);
}
$wgSessionsInObjectCache = true;
if ( $wgPHPSessionHandling !== 'enable' &&
$wgPHPSessionHandling !== 'warn' &&
$wgPHPSessionHandling !== 'disable'
) {
$wgPHPSessionHandling = 'warn';
}
Profiler::instance()->scopedProfileOut( $ps_default );
@ -655,20 +670,6 @@ Profiler::instance()->scopedProfileOut( $ps_memcached );
// Most of the config is out, some might want to run hooks here.
Hooks::run( 'SetupAfterCache' );
$ps_session = Profiler::instance()->scopedProfileIn( $fname . '-session' );
if ( !defined( 'MW_NO_SESSION' ) && !$wgCommandLineMode ) {
// If session.auto_start is there, we can't touch session name
if ( !wfIniGetBool( 'session.auto_start' ) ) {
session_name( $wgSessionName ? $wgSessionName : $wgCookiePrefix . '_session' );
}
if ( $wgRequest->checkSessionCookie() || isset( $_COOKIE[$wgCookiePrefix . 'Token'] ) ) {
wfSetupSession();
}
}
Profiler::instance()->scopedProfileOut( $ps_session );
$ps_globals = Profiler::instance()->scopedProfileIn( $fname . '-globals' );
/**
@ -681,6 +682,56 @@ $wgContLang->initContLang();
// Now that variant lists may be available...
$wgRequest->interpolateTitle();
if ( !is_object( $wgAuth ) ) {
$wgAuth = new AuthPlugin;
Hooks::run( 'AuthPluginSetup', array( &$wgAuth ) );
}
// Set up the session
$ps_session = Profiler::instance()->scopedProfileIn( $fname . '-session' );
if ( !defined( 'MW_NO_SESSION' ) && !$wgCommandLineMode ) {
// If session.auto_start is there, we can't touch session name
if ( $wgPHPSessionHandling !== 'disable' && !wfIniGetBool( 'session.auto_start' ) ) {
session_name( $wgSessionName ? $wgSessionName : $wgCookiePrefix . '_session' );
}
// Create the SessionManager singleton and set up our session handler
MediaWiki\Session\PHPSessionHandler::install(
MediaWiki\Session\SessionManager::singleton()
);
// Initialize the session
try {
$session = MediaWiki\Session\SessionManager::getGlobalSession();
} catch ( OverflowException $ex ) {
if ( isset( $ex->sessionInfos ) && count( $ex->sessionInfos ) >= 2 ) {
// The exception is because the request had multiple possible
// sessions tied for top priority. Report this to the user.
$list = array();
foreach ( $ex->sessionInfos as $info ) {
$list[] = $info->getProvider()->describe( $wgContLang );
}
$list = $wgContLang->listToText( $list );
throw new HttpError( 400,
Message::newFromKey( 'sessionmanager-tie', $list )->inLanguage( $wgContLang )->plain()
);
}
// Not the one we want, rethrow
throw $ex;
}
$session->renew();
if ( MediaWiki\Session\PHPSessionHandler::isEnabled() &&
( $session->isPersistent() || $session->shouldRememberUser() )
) {
// Start the PHP-session for backwards compatibility
session_id( $session->getId() );
MediaWiki\quietCall( 'session_start' );
}
}
Profiler::instance()->scopedProfileOut( $ps_session );
/**
* @var User $wgUser
*/
@ -701,11 +752,6 @@ $wgOut = RequestContext::getMain()->getOutput(); // BackCompat
*/
$wgParser = new StubObject( 'wgParser', $wgParserConf['class'], array( $wgParserConf ) );
if ( !is_object( $wgAuth ) ) {
$wgAuth = new AuthPlugin;
Hooks::run( 'AuthPluginSetup', array( &$wgAuth ) );
}
/**
* @var Title $wgTitle
*/
@ -737,6 +783,16 @@ foreach ( $wgExtensionFunctions as $func ) {
Profiler::instance()->scopedProfileOut( $ps_ext_func );
}
// If the session user has a 0 id but a valid name, that means we need to
// autocreate it.
$sessionUser = MediaWiki\Session\SessionManager::getGlobalSession()->getUser();
if ( $sessionUser->getId() === 0 && User::isValidUserName( $sessionUser->getName() ) ) {
$ps_autocreate = Profiler::instance()->scopedProfileIn( $fname . '-autocreate' );
MediaWiki\Session\SessionManager::autoCreateUser( $sessionUser );
Profiler::instance()->scopedProfileOut( $ps_autocreate );
}
unset( $sessionUser );
wfDebug( "Fully initialised\n" );
$wgFullyInitialised = true;

View file

@ -23,6 +23,8 @@
* @file
*/
use MediaWiki\Session\SessionManager;
/**
* The WebRequest class encapsulates getting at data passed in the
* URL or via a POSTed form stripping illegal input characters and
@ -63,6 +65,13 @@ class WebRequest {
*/
protected $protocol;
/**
* @var \\MediaWiki\\Session\\SessionId|null Session ID to use for this
* request. We can't save the session directly due to reference cycles not
* working too well (slow GC in Zend and never collected in HHVM).
*/
protected $sessionId = null;
public function __construct() {
$this->requestTime = isset( $_SERVER['REQUEST_TIME_FLOAT'] )
? $_SERVER['REQUEST_TIME_FLOAT'] : microtime( true );
@ -638,18 +647,44 @@ class WebRequest {
}
/**
* Returns true if there is a session cookie set.
* Return the session for this request
* @since 1.27
* @note For performance, keep the session locally if you will be making
* much use of it instead of calling this method repeatedly.
* @return MediaWiki\\Session\\Session
*/
public function getSession() {
if ( $this->sessionId !== null ) {
return SessionManager::singleton()->getSessionById( (string)$this->sessionId, false, $this );
}
$session = SessionManager::singleton()->getSessionForRequest( $this );
$this->sessionId = $session->getSessionId();
return $session;
}
/**
* Set the session for this request
* @since 1.27
* @private For use by MediaWiki\\Session classes only
* @param MediaWiki\\Session\\SessionId $sessionId
*/
public function setSessionId( MediaWiki\Session\SessionId $sessionId ) {
$this->sessionId = $sessionId;
}
/**
* Returns true if the request has a persistent session.
* This does not necessarily mean that the user is logged in!
*
* If you want to check for an open session, use session_id()
* instead; that will also tell you if the session was opened
* during the current request (in which case the cookie will
* be sent back to the client at the end of the script run).
*
* @deprecated since 1.27, use
* \\MediaWiki\\Session\\SessionManager::singleton()->getPersistedSessionId()
* instead.
* @return bool
*/
public function checkSessionCookie() {
return isset( $_COOKIE[session_name()] );
wfDeprecated( __METHOD__, '1.27' );
return SessionManager::singleton()->getPersistedSessionId( $this ) !== null;
}
/**
@ -907,26 +942,25 @@ class WebRequest {
}
/**
* Get data from $_SESSION
* Get data from the session
*
* @param string $key Name of key in $_SESSION
* @note Prefer $this->getSession() instead if making multiple calls.
* @param string $key Name of key in the session
* @return mixed
*/
public function getSessionData( $key ) {
if ( !isset( $_SESSION[$key] ) ) {
return null;
}
return $_SESSION[$key];
return $this->getSession()->get( $key );
}
/**
* Set session data
*
* @param string $key Name of key in $_SESSION
* @note Prefer $this->getSession() instead if making multiple calls.
* @param string $key Name of key in the session
* @param mixed $data
*/
public function setSessionData( $key, $data ) {
$_SESSION[$key] = $data;
return $this->getSession()->set( $key, $data );
}
/**

View file

@ -27,6 +27,11 @@
*/
class WebResponse {
/** @var array Used to record set cookies, because PHP's setcookie() will
* happily send an identical Set-Cookie to the client.
*/
protected static $setCookies = array();
/**
* Output an HTTP header, wrapper for PHP's header()
* @param string $string Header to output
@ -62,6 +67,15 @@ class WebResponse {
HttpStatus::header( $code );
}
/**
* Test if headers have been sent
* @since 1.27
* @return bool
*/
public function headersSent() {
return headers_sent();
}
/**
* Set the browser cookie
* @param string $name The name of the cookie.
@ -115,25 +129,26 @@ class WebResponse {
$func = $options['raw'] ? 'setrawcookie' : 'setcookie';
if ( Hooks::run( 'WebResponseSetCookie', array( &$name, &$value, &$expire, $options ) ) ) {
wfDebugLog( 'cookie',
$func . ': "' . implode( '", "',
array(
$options['prefix'] . $name,
$value,
$expire,
$options['path'],
$options['domain'],
$options['secure'],
$options['httpOnly'] ) ) . '"' );
call_user_func( $func,
$options['prefix'] . $name,
$value,
$expire,
$options['path'],
$options['domain'],
$options['secure'],
$options['httpOnly'] );
$cookie = $options['prefix'] . $name;
$data = array(
(string)$cookie,
(string)$value,
(int)$expire,
(string)$options['path'],
(string)$options['domain'],
(bool)$options['secure'],
(bool)$options['httpOnly'],
);
if ( !isset( self::$setCookies[$cookie] ) ||
self::$setCookies[$cookie] !== array( $func, $data )
) {
wfDebugLog( 'cookie', $func . ': "' . implode( '", "', $data ) . '"' );
if ( call_user_func_array( $func, $data ) ) {
self::$setCookies[$cookie] = array( $func, $data );
}
} else {
wfDebugLog( 'cookie', 'already set ' . $func . ': "' . implode( '", "', $data ) . '"' );
}
}
}
@ -156,7 +171,7 @@ class WebResponse {
*/
class FauxResponse extends WebResponse {
private $headers;
private $cookies;
private $cookies = array();
private $code;
/**
@ -192,6 +207,10 @@ class FauxResponse extends WebResponse {
$this->code = intval( $code );
}
public function headersSent() {
return false;
}
/**
* @param string $key The name of the header to get (case insensitive).
* @return string|null The header value (if set); null otherwise.

View file

@ -83,7 +83,8 @@ class RawAction extends FormlessAction {
$response->header( 'Content-type: ' . $contentType . '; charset=UTF-8' );
// Output may contain user-specific data;
// vary generated content for open sessions on private wikis
$privateCache = !User::isEveryoneAllowed( 'read' ) && ( $smaxage == 0 || session_id() != '' );
$privateCache = !User::isEveryoneAllowed( 'read' ) &&
( $smaxage == 0 || MediaWiki\Session\SessionManager::getGlobalSession()->isPersistent() );
// Don't accidentally cache cookies if user is logged in (T55032)
$privateCache = $privateCache || $this->getUser()->isLoggedIn();
$mode = $privateCache ? 'private' : 'public';

View file

@ -32,10 +32,8 @@ class SubmitAction extends EditAction {
}
public function show() {
if ( session_id() === '' ) {
// Send a cookie so anons get talk message notifications
wfSetupSession();
}
// Send a cookie so anons get talk message notifications
MediaWiki\Session\SessionManager::getGlobalSession()->persist();
parent::show();
}

View file

@ -59,10 +59,8 @@ class ApiCreateAccount extends ApiBase {
$params = $this->extractRequestParams();
// Init session if necessary
if ( session_id() == '' ) {
wfSetupSession();
}
// Make sure session is persisted
MediaWiki\Session\SessionManager::getGlobalSession()->persist();
if ( $params['mailpassword'] && !$params['email'] ) {
$this->dieUsageMsg( 'noemail' );

View file

@ -62,9 +62,19 @@ class ApiLogin extends ApiBase {
$result = array();
// Init session if necessary
if ( session_id() == '' ) {
wfSetupSession();
// Make sure session is persisted
$session = MediaWiki\Session\SessionManager::getGlobalSession();
$session->persist();
// Make sure it's possible to log in
if ( !$session->canSetUser() ) {
$this->getResult()->addValue( null, 'login', array(
'result' => 'Aborted',
'reason' => 'Cannot log in when using ' .
$session->getProvider()->describe( Language::factory( 'en' ) ),
) );
return;
}
$context = new DerivativeContext( $this->getContext() );
@ -107,7 +117,7 @@ class ApiLogin extends ApiBase {
// SessionManager/AuthManager are *really* going to break it.
$result['lgtoken'] = $user->getToken();
$result['cookieprefix'] = $this->getConfig()->get( 'CookiePrefix' );
$result['sessionid'] = session_id();
$result['sessionid'] = MediaWiki\Session\SessionManager::getGlobalSession()->getId();
break;
case LoginForm::NEED_TOKEN:
@ -116,7 +126,7 @@ class ApiLogin extends ApiBase {
// @todo: See above about deprecation
$result['cookieprefix'] = $this->getConfig()->get( 'CookiePrefix' );
$result['sessionid'] = session_id();
$result['sessionid'] = MediaWiki\Session\SessionManager::getGlobalSession()->getId();
break;
case LoginForm::WRONG_TOKEN:

View file

@ -33,6 +33,16 @@
class ApiLogout extends ApiBase {
public function execute() {
// Make sure it's possible to log out
$session = MediaWiki\Session\SessionManager::getGlobalSession();
if ( !$session->canSetUser() ) {
$this->dieUsage(
'Cannot log out when using ' .
$session->getProvider()->describe( Language::factory( 'en' ) ),
'cannotlogout'
);
}
$user = $this->getUser();
$oldName = $user->getName();
$user->logout();

View file

@ -769,7 +769,7 @@ class ApiMain extends ApiBase {
return;
}
// Logged out, send normal public headers below
} elseif ( session_id() != '' ) {
} elseif ( MediaWiki\Session\SessionManager::getGlobalSession()->isPersistent() ) {
// Logged in or otherwise has session (e.g. anonymous users who have edited)
// Mark request private
$response->header( "Cache-Control: $privateCache" );

View file

@ -513,7 +513,7 @@ class RequestContext implements IContextSource, MutableContext {
return array(
'ip' => $this->getRequest()->getIP(),
'headers' => $this->getRequest()->getAllHeaders(),
'sessionId' => session_id(),
'sessionId' => MediaWiki\Session\SessionManager::getGlobalSession()->getId(),
'userId' => $this->getUser()->getId()
);
}
@ -541,7 +541,9 @@ class RequestContext implements IContextSource, MutableContext {
* @since 1.21
*/
public static function importScopedSession( array $params ) {
if ( session_id() != '' && strlen( $params['sessionId'] ) ) {
if ( strlen( $params['sessionId'] ) &&
MediaWiki\Session\SessionManager::getGlobalSession()->isPersistent()
) {
// Sanity check to avoid sending random cookies for the wrong users.
// This method should only called by CLI scripts or by HTTP job runners.
throw new MWException( "Sessions can only be imported when none is active." );
@ -563,23 +565,37 @@ class RequestContext implements IContextSource, MutableContext {
global $wgRequest, $wgUser;
$context = RequestContext::getMain();
// Commit and close any current session
session_write_close(); // persist
session_id( '' ); // detach
$_SESSION = array(); // clear in-memory array
// Remove any user IP or agent information
$context->setRequest( new FauxRequest() );
if ( MediaWiki\Session\PHPSessionHandler::isEnabled() ) {
session_write_close(); // persist
session_id( '' ); // detach
$_SESSION = array(); // clear in-memory array
}
// Get new session, if applicable
$session = null;
if ( strlen( $params['sessionId'] ) ) { // don't make a new random ID
$session = MediaWiki\Session\SessionManager::singleton()
->getSessionById( $params['sessionId'] );
}
// Remove any user IP or agent information, and attach the request
// with the new session.
$context->setRequest( new FauxRequest( array(), false, $session ) );
$wgRequest = $context->getRequest(); // b/c
// Now that all private information is detached from the user, it should
// be safe to load the new user. If errors occur or an exception is thrown
// and caught (leaving the main context in a mixed state), there is no risk
// of the User object being attached to the wrong IP, headers, or session.
$context->setUser( $user );
$wgUser = $context->getUser(); // b/c
if ( strlen( $params['sessionId'] ) ) { // don't make a new random ID
wfSetupSession( $params['sessionId'] ); // sets $_SESSION
if ( $session && MediaWiki\Session\PHPSessionHandler::isEnabled() ) {
session_id( $session->getId() );
MediaWiki\quietCall( 'session_start' );
}
$request = new FauxRequest( array(), false, $_SESSION );
$request = new FauxRequest( array(), false, $session );
$request->setIP( $params['ip'] );
foreach ( $params['headers'] as $name => $value ) {
$request->setHeader( $name, $value );

View file

@ -93,10 +93,10 @@ class UploadFromUrlJob extends Job {
$this->params['url'] )->text()
);
} else {
wfSetupSession( $this->params['sessionId'] );
$this->storeResultInSession( 'Warning',
$session = MediaWiki\Session\SessionManager::singleton()
->getSessionById( $this->params['sessionId'] );
$this->storeResultInSession( $session, 'Warning',
'warnings', $warnings );
session_write_close();
}
return true;
@ -139,15 +139,15 @@ class UploadFromUrlJob extends Job {
)->text() );
}
} else {
wfSetupSession( $this->params['sessionId'] );
$session = MediaWiki\Session\SessionManager::singleton()
->getSessionById( $this->params['sessionId'] );
if ( $status->isOk() ) {
$this->storeResultInSession( 'Success',
$this->storeResultInSession( $session, 'Success',
'filename', $this->upload->getLocalFile()->getName() );
} else {
$this->storeResultInSession( 'Failure',
$this->storeResultInSession( $session, 'Failure',
'errors', $status->getErrorsArray() );
}
session_write_close();
}
}
@ -155,33 +155,55 @@ class UploadFromUrlJob extends Job {
* Store a result in the session data. Note that the caller is responsible
* for appropriate session_start and session_write_close calls.
*
* @param MediaWiki\\Session\\Session $session Session to store result into
* @param string $result The result (Success|Warning|Failure)
* @param string $dataKey The key of the extra data
* @param mixed $dataValue The extra data itself
*/
protected function storeResultInSession( $result, $dataKey, $dataValue ) {
$session =& self::getSessionData( $this->params['sessionKey'] );
$session['result'] = $result;
$session[$dataKey] = $dataValue;
protected function storeResultInSession(
MediaWiki\Session\Session $session, $result, $dataKey, $dataValue
) {
$data = self::getSessionData( $session, $this->params['sessionKey'] );
$data['result'] = $result;
$data[$dataKey] = $dataValue;
self::setSessionData( $session, $this->params['sessionKey'], $data );
}
/**
* Initialize the session data. Sets the initial result to queued.
*/
public function initializeSessionData() {
$session =& self::getSessionData( $this->params['sessionKey'] );
$session['result'] = 'Queued';
$session = MediaWiki\Session\SessionManager::getGlobalSession();
$data = self::getSessionData( $session, $this->params['sessionKey'] );
$data['result'] = 'Queued';
self::setSessionData( $session, $this->params['sessionKey'], $data );
}
/**
* @param MediaWiki\\Session\\Session $session
* @param string $key
* @return mixed
*/
public static function &getSessionData( $key ) {
if ( !isset( $_SESSION[self::SESSION_KEYNAME][$key] ) ) {
$_SESSION[self::SESSION_KEYNAME][$key] = array();
public static function getSessionData( MediaWiki\Session\Session $session, $key ) {
$data = $session->get( self::SESSION_KEYNAME );
if ( !is_array( $data ) || !isset( $data[$key] ) ) {
self::setSessionData( $session, $key, array() );
return array();
}
return $data[$key];
}
return $_SESSION[self::SESSION_KEYNAME][$key];
/**
* @param MediaWiki\\Session\\Session $session
* @param string $key
* @param mixed $value
*/
public static function setSessionData( MediaWiki\Session\Session $session, $key, $value ) {
$data = $session->get( self::SESSION_KEYNAME, array() );
if ( !is_array( $data ) ) {
$data = array();
}
$data[$key] = $value;
$session->set( self::SESSION_KEYNAME, $data );
}
}

View file

@ -1,207 +0,0 @@
<?php
/**
* Session storage in object cache.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along
* with this program; if not, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
* http://www.gnu.org/copyleft/gpl.html
*
* @file
* @ingroup Cache
*/
use MediaWiki\Logger\LoggerFactory;
/**
* Session storage in object cache.
* Used if $wgSessionsInObjectCache is true.
*
* @ingroup Cache
*/
class ObjectCacheSessionHandler {
/** @var array Map of (session ID => SHA-1 of the data) */
protected static $hashCache = array();
/**
* Install a session handler for the current web request
*/
static function install() {
session_set_save_handler(
array( __CLASS__, 'open' ),
array( __CLASS__, 'close' ),
array( __CLASS__, 'read' ),
array( __CLASS__, 'write' ),
array( __CLASS__, 'destroy' ),
array( __CLASS__, 'gc' ) );
// It's necessary to register a shutdown function to call session_write_close(),
// because by the time the request shutdown function for the session module is
// called, the BagOStuff has already been destroyed. Shutdown functions registered
// this way are called before object destruction.
register_shutdown_function( array( __CLASS__, 'handleShutdown' ) );
}
/**
* Get the cache storage object to use for session storage
* @return BagOStuff
*/
protected static function getCache() {
global $wgSessionCacheType;
return ObjectCache::getInstance( $wgSessionCacheType );
}
/**
* Get a cache key for the given session id.
*
* @param string $id Session id
* @return string Cache key
*/
protected static function getKey( $id ) {
return wfMemcKey( 'session', $id );
}
/**
* @param mixed $data
* @return string
*/
protected static function getHash( $data ) {
return sha1( serialize( $data ) );
}
/**
* Callback when opening a session.
*
* @param string $save_path Path used to store session files, unused
* @param string $session_name Session name
* @return bool Success
*/
static function open( $save_path, $session_name ) {
return true;
}
/**
* Callback when closing a session.
* NOP.
*
* @return bool Success
*/
static function close() {
return true;
}
/**
* Callback when reading session data.
*
* @param string $id Session id
* @return mixed Session data
*/
static function read( $id ) {
$stime = microtime( true );
$data = self::getCache()->get( self::getKey( $id ) );
$real = microtime( true ) - $stime;
RequestContext::getMain()->getStats()->timing( "session.read", 1000 * $real );
self::$hashCache = array( $id => self::getHash( $data ) );
return ( $data === false ) ? '' : $data;
}
/**
* Callback when writing session data.
*
* @param string $id Session id
* @param string $data Session data
* @return bool Success
*/
static function write( $id, $data ) {
global $wgObjectCacheSessionExpiry;
// Only issue a write if anything changed (PHP 5.6 already does this)
if ( !isset( self::$hashCache[$id] )
|| self::getHash( $data ) !== self::$hashCache[$id]
) {
$stime = microtime( true );
self::getCache()->set( self::getKey( $id ), $data, $wgObjectCacheSessionExpiry );
$real = microtime( true ) - $stime;
RequestContext::getMain()->getStats()->timing( "session.write", 1000 * $real );
}
return true;
}
/**
* Callback to destroy a session when calling session_destroy().
*
* @param string $id Session id
* @return bool Success
*/
static function destroy( $id ) {
$stime = microtime( true );
self::getCache()->delete( self::getKey( $id ) );
$real = microtime( true ) - $stime;
RequestContext::getMain()->getStats()->timing( "session.destroy", 1000 * $real );
return true;
}
/**
* Callback to execute garbage collection.
* NOP: Object caches perform garbage collection implicitly
*
* @param int $maxlifetime Maximum session life time
* @return bool Success
*/
static function gc( $maxlifetime ) {
return true;
}
/**
* Shutdown function.
* See the comment inside ObjectCacheSessionHandler::install for rationale.
*/
static function handleShutdown() {
session_write_close();
}
/**
* Pre-emptive session renewal function
*/
static function renewCurrentSession() {
global $wgObjectCacheSessionExpiry;
// Once a session is at half TTL, renew it
$window = $wgObjectCacheSessionExpiry / 2;
$logger = LoggerFactory::getInstance( 'SessionHandler' );
$now = microtime( true );
// Session are only written in object stores when $_SESSION changes,
// which also renews the TTL ($wgObjectCacheSessionExpiry). If a user
// is active but not causing session data changes, it may suddenly
// expire as they view a form, blocking the first submission.
// Make a dummy change every so often to avoid this.
if ( !isset( $_SESSION['wsExpiresUnix'] ) ) {
$_SESSION['wsExpiresUnix'] = $now + $wgObjectCacheSessionExpiry;
$logger->info( "Set expiry for session " . session_id(), array() );
} elseif ( ( $now + $window ) > $_SESSION['wsExpiresUnix'] ) {
$_SESSION['wsExpiresUnix'] = $now + $wgObjectCacheSessionExpiry;
$logger->info( "Renewed session " . session_id(), array() );
}
}
}

View file

@ -0,0 +1,324 @@
<?php
/**
* MediaWiki cookie-based session provider interface
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along
* with this program; if not, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
* http://www.gnu.org/copyleft/gpl.html
*
* @file
* @ingroup Session
*/
namespace MediaWiki\Session;
use Config;
use User;
use WebRequest;
/**
* A CookieSessionProvider persists sessions using cookies
*
* @ingroup Session
* @since 1.27
*/
class CookieSessionProvider extends SessionProvider {
protected $params = array();
protected $cookieOptions = array();
/**
* @param array $params Keys include:
* - priority: (required) Priority of the returned sessions
* - callUserSetCookiesHook: Whether to call the deprecated hook
* - sessionName: Session cookie name. Doesn't honor 'prefix'. Defaults to
* $wgSessionName, or $wgCookiePrefix . '_session' if that is unset.
* - cookieOptions: Options to pass to WebRequest::setCookie():
* - prefix: Cookie prefix, defaults to $wgCookiePrefix
* - path: Cookie path, defaults to $wgCookiePath
* - domain: Cookie domain, defaults to $wgCookieDomain
* - secure: Cookie secure flag, defaults to $wgCookieSecure
* - httpOnly: Cookie httpOnly flag, defaults to $wgCookieHttpOnly
*/
public function __construct( $params = array() ) {
parent::__construct();
$params += array(
'cookieOptions' => array(),
// @codeCoverageIgnoreStart
);
// @codeCoverageIgnoreEnd
if ( !isset( $params['priority'] ) ) {
throw new \InvalidArgumentException( __METHOD__ . ': priority must be specified' );
}
if ( $params['priority'] < SessionInfo::MIN_PRIORITY ||
$params['priority'] > SessionInfo::MAX_PRIORITY
) {
throw new \InvalidArgumentException( __METHOD__ . ': Invalid priority' );
}
if ( !is_array( $params['cookieOptions'] ) ) {
throw new \InvalidArgumentException( __METHOD__ . ': cookieOptions must be an array' );
}
$this->priority = $params['priority'];
$this->cookieOptions = $params['cookieOptions'];
$this->params = $params;
unset( $this->params['priority'] );
unset( $this->params['cookieOptions'] );
}
public function setConfig( Config $config ) {
parent::setConfig( $config );
// @codeCoverageIgnoreStart
$this->params += array(
// @codeCoverageIgnoreEnd
'callUserSetCookiesHook' => false,
'sessionName' =>
$config->get( 'SessionName' ) ?: $config->get( 'CookiePrefix' ) . '_session',
);
// @codeCoverageIgnoreStart
$this->cookieOptions += array(
// @codeCoverageIgnoreEnd
'prefix' => $config->get( 'CookiePrefix' ),
'path' => $config->get( 'CookiePath' ),
'domain' => $config->get( 'CookieDomain' ),
'secure' => $config->get( 'CookieSecure' ),
'httpOnly' => $config->get( 'CookieHttpOnly' ),
);
}
public function provideSessionInfo( WebRequest $request ) {
$info = array(
'id' => $request->getCookie( $this->params['sessionName'], '' )
);
if ( !SessionManager::validateSessionId( $info['id'] ) ) {
unset( $info['id'] );
}
list( $userId, $userName, $token ) = $this->getUserInfoFromCookies( $request );
if ( $userId !== null ) {
try {
$userInfo = UserInfo::newFromId( $userId );
} catch ( \InvalidArgumentException $ex ) {
return null;
}
// Sanity check
if ( $userName !== null && $userInfo->getName() !== $userName ) {
return null;
}
if ( $token !== null ) {
if ( !hash_equals( $userInfo->getToken(), $token ) ) {
return null;
}
$info['userInfo'] = $userInfo->verified();
} elseif ( isset( $info['id'] ) ) { // No point if no session ID
$info['userInfo'] = $userInfo;
}
}
if ( !$info ) {
return null;
}
$info += array(
'provider' => $this,
'persisted' => isset( $info['id'] ),
'forceHTTPS' => $request->getCookie( 'forceHTTPS', '', false )
);
return new SessionInfo( $this->priority, $info );
}
public function persistsSessionId() {
return true;
}
public function canChangeUser() {
return true;
}
public function persistSession( SessionBackend $session, WebRequest $request ) {
$response = $request->response();
if ( $response->headersSent() ) {
// Can't do anything now
$this->logger->debug( __METHOD__ . ': Headers already sent' );
return;
}
$user = $session->getUser();
$cookies = $this->cookieDataToExport( $user, $session->shouldRememberUser() );
$sessionData = $this->sessionDataToExport( $user );
// Legacy hook
if ( $this->params['callUserSetCookiesHook'] && !$user->isAnon() ) {
\Hooks::run( 'UserSetCookies', array( $user, &$sessionData, &$cookies ) );
}
$options = $this->cookieOptions;
if ( $session->shouldForceHTTPS() || $user->requiresHTTPS() ) {
$response->setCookie( 'forceHTTPS', 'true', $session->shouldRememberUser() ? 0 : null,
array( 'prefix' => '', 'secure' => false ) + $options );
$options['secure'] = true;
}
$response->setCookie( $this->params['sessionName'], $session->getId(), null,
array( 'prefix' => '' ) + $options
);
$extendedCookies = $this->config->get( 'ExtendedLoginCookies' );
$extendedExpiry = $this->config->get( 'ExtendedLoginCookieExpiration' );
foreach ( $cookies as $key => $value ) {
if ( $value === false ) {
$response->clearCookie( $key, $options );
} else {
if ( $extendedExpiry !== null && in_array( $key, $extendedCookies ) ) {
$expiry = time() + (int)$extendedExpiry;
} else {
$expiry = 0; // Default cookie expiration
}
$response->setCookie( $key, (string)$value, $expiry, $options );
}
}
$this->setLoggedOutCookie( $session->getLoggedOutTimestamp(), $request );
if ( $sessionData ) {
$session->addData( $sessionData );
}
}
public function unpersistSession( WebRequest $request ) {
$response = $request->response();
if ( $response->headersSent() ) {
// Can't do anything now
$this->logger->debug( __METHOD__ . ': Headers already sent' );
return;
}
$cookies = array(
'UserID' => false,
'Token' => false,
);
$response->clearCookie(
$this->params['sessionName'], array( 'prefix' => '' ) + $this->cookieOptions
);
foreach ( $cookies as $key => $value ) {
$response->clearCookie( $key, $this->cookieOptions );
}
$response->clearCookie( 'forceHTTPS',
array( 'prefix' => '', 'secure' => false ) + $this->cookieOptions );
}
/**
* Set the "logged out" cookie
* @param int $loggedOut timestamp
* @param WebRequest $request
*/
protected function setLoggedOutCookie( $loggedOut, WebRequest $request ) {
if ( $loggedOut + 86400 > time() &&
$loggedOut !== (int)$request->getCookie( 'LoggedOut', $this->cookieOptions['prefix'] )
) {
$request->response()->setCookie( 'LoggedOut', $loggedOut, $loggedOut + 86400,
$this->cookieOptions );
}
}
public function getVaryCookies() {
return array(
// Vary on token and session because those are the real authn
// determiners. UserID and UserName don't matter without those.
$this->cookieOptions['prefix'] . 'Token',
$this->cookieOptions['prefix'] . 'LoggedOut',
$this->params['sessionName'],
'forceHTTPS',
);
}
public function suggestLoginUsername( WebRequest $request ) {
$name = $request->getCookie( 'UserName', $this->cookieOptions['prefix'] );
if ( $name !== null ) {
$name = User::getCanonicalName( $name, 'usable' );
}
return $name === false ? null : $name;
}
/**
* Fetch the user identity from cookies
* @return array (int|null $id, string|null $token)
*/
protected function getUserInfoFromCookies( $request ) {
$prefix = $this->cookieOptions['prefix'];
return array(
$request->getCookie( 'UserID', $prefix ),
$request->getCookie( 'UserName', $prefix ),
$request->getCookie( 'Token', $prefix ),
);
}
/**
* Return the data to store in cookies
* @param User $user
* @param bool $remember
* @return array $cookies Set value false to unset the cookie
*/
protected function cookieDataToExport( $user, $remember ) {
if ( $user->isAnon() ) {
return array(
'UserID' => false,
'Token' => false,
);
} else {
return array(
'UserID' => $user->getId(),
'UserName' => $user->getName(),
'Token' => $remember ? (string)$user->getToken() : false,
);
}
}
/**
* Return extra data to store in the session
* @param User $user
* @return array $session
*/
protected function sessionDataToExport( $user ) {
// If we're calling the legacy hook, we should populate $session
// like User::setCookies() did.
if ( !$user->isAnon() && $this->params['callUserSetCookiesHook'] ) {
return array(
'wsUserID' => $user->getId(),
'wsToken' => $user->getToken(),
'wsUserName' => $user->getName(),
);
}
return array();
}
public function whyNoSession() {
return wfMessage( 'sessionprovider-nocookies' );
}
}

View file

@ -0,0 +1,153 @@
<?php
/**
* MediaWiki session provider base class
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along
* with this program; if not, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
* http://www.gnu.org/copyleft/gpl.html
*
* @file
* @ingroup Session
*/
namespace MediaWiki\Session;
use WebRequest;
/**
* An ImmutableSessionProviderWithCookie doesn't persist the user, but
* optionally can use a cookie to support multiple IDs per session.
*
* As mentioned in the documentation for SessionProvider, many methods that are
* technically "cannot persist ID" could be turned into "can persist ID but
* not changing User" using a session cookie. This class implements such an
* optional session cookie.
*
* @ingroup Session
* @since 1.27
*/
abstract class ImmutableSessionProviderWithCookie extends SessionProvider {
/** @var string|null */
protected $sessionCookieName = null;
protected $sessionCookieOptions = array();
/**
* @param array $params Keys include:
* - sessionCookieName: Session cookie name, if multiple sessions per
* client are to be supported.
* - sessionCookieOptions: Options to pass to WebResponse::setCookie().
*/
public function __construct( $params = array() ) {
parent::__construct();
if ( isset( $params['sessionCookieName'] ) ) {
if ( !is_string( $params['sessionCookieName'] ) ) {
throw new \InvalidArgumentException( 'sessionCookieName must be a string' );
}
$this->sessionCookieName = $params['sessionCookieName'];
}
if ( isset( $params['sessionCookieOptions'] ) ) {
if ( !is_array( $params['sessionCookieOptions'] ) ) {
throw new \InvalidArgumentException( 'sessionCookieOptions must be an array' );
}
$this->sessionCookieOptions = $params['sessionCookieOptions'];
}
}
/**
* Get the session ID from the cookie, if any.
*
* Only call this if $this->sessionCookieName !== null. If
* sessionCookieName is null, do some logic (probably involving a call to
* $this->hashToSessionId()) to create the single session ID corresponding
* to this WebRequest instead of calling this method.
*
* @param WebRequest $request
* @return string|null
*/
protected function getSessionIdFromCookie( WebRequest $request ) {
if ( $this->sessionCookieName === null ) {
throw new \BadMethodCallException(
__METHOD__ . ' may not be called when $this->sessionCookieName === null'
);
}
$prefix = isset( $this->sessionCookieOptions['prefix'] )
? $this->sessionCookieOptions['prefix']
: $this->config->get( 'CookiePrefix' );
$id = $request->getCookie( $this->sessionCookieName, $prefix );
return SessionManager::validateSessionId( $id ) ? $id : null;
}
public function persistsSessionId() {
return $this->sessionCookieName !== null;
}
public function canChangeUser() {
return false;
}
public function persistSession( SessionBackend $session, WebRequest $request ) {
if ( $this->sessionCookieName === null ) {
return;
}
$response = $request->response();
if ( $response->headersSent() ) {
// Can't do anything now
$this->logger->debug( __METHOD__ . ': Headers already sent' );
return;
}
$options = $this->sessionCookieOptions;
if ( $session->shouldForceHTTPS() || $session->getUser()->requiresHTTPS() ) {
$response->setCookie( 'forceHTTPS', 'true', $session->shouldRememberUser() ? 0 : null,
array( 'prefix' => '', 'secure' => false ) + $options );
$options['secure'] = true;
}
$response->setCookie( $this->sessionCookieName, $session->getId(), null, $options );
}
public function unpersistSession( WebRequest $request ) {
if ( $this->sessionCookieName === null ) {
return;
}
$response = $request->response();
if ( $response->headersSent() ) {
// Can't do anything now
$this->logger->debug( __METHOD__ . ': Headers already sent' );
return;
}
$response->clearCookie( $this->sessionCookieName, $this->sessionCookieOptions );
}
public function getVaryCookies() {
if ( $this->sessionCookieName === null ) {
return array();
}
$prefix = isset( $this->sessionCookieOptions['prefix'] )
? $this->sessionCookieOptions['prefix']
: $this->config->get( 'CookiePrefix' );
return array( $prefix . $this->sessionCookieName );
}
public function whyNoSession() {
return wfMessage( 'sessionprovider-nocookies' );
}
}

View file

@ -0,0 +1,369 @@
<?php
/**
* Session storage in object cache.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along
* with this program; if not, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
* http://www.gnu.org/copyleft/gpl.html
*
* @file
* @ingroup Session
*/
namespace MediaWiki\Session;
use Psr\Log\LoggerInterface;
use BagOStuff;
/**
* Adapter for PHP's session handling
* @todo Once we drop support for PHP < 5.4, use SessionHandlerInterface
* (should just be a matter of adding "implements SessionHandlerInterface" and
* changing the session_set_save_handler() call).
* @ingroup Session
* @since 1.27
*/
class PHPSessionHandler {
/** @var PHPSessionHandler */
protected static $instance = null;
/** @var bool Whether PHP session handling is enabled */
protected $enable = false;
protected $warn = true;
/** @var SessionManager|null */
protected $manager;
/** @var BagOStuff|null */
protected $store;
/** @var LoggerInterface */
protected $logger;
/** @var array Track original session fields for later modification check */
protected $sessionFieldCache = array();
protected function __construct( SessionManager $manager ) {
$this->setEnableFlags(
\RequestContext::getMain()->getConfig()->get( 'PHPSessionHandling' )
);
$manager->setupPHPSessionHandler( $this );
}
/**
* Set $this->enable and $this->warn
*
* Separate just because there doesn't seem to be a good way to test it
* otherwise.
*
* @param string $PHPSessionHandling See $wgPHPSessionHandling
*/
private function setEnableFlags( $PHPSessionHandling ) {
switch ( $PHPSessionHandling ) {
case 'enable':
$this->enable = true;
$this->warn = false;
break;
case 'warn':
$this->enable = true;
$this->warn = true;
break;
case 'disable':
$this->enable = false;
$this->warn = false;
break;
}
}
/**
* Test whether the handler is installed
* @return bool
*/
public static function isInstalled() {
return (bool)self::$instance;
}
/**
* Test whether the handler is installed and enabled
* @return bool
*/
public static function isEnabled() {
return self::$instance && self::$instance->enable;
}
/**
* Install a session handler for the current web request
* @param SessionManager $manager
*/
public static function install( SessionManager $manager ) {
if ( self::$instance ) {
$manager->setupPHPSessionHandler( self::$instance );
return;
}
self::$instance = new self( $manager );
// Close any auto-started session, before we replace it
session_write_close();
// Tell PHP not to mess with cookies itself
ini_set( 'session.use_cookies', 0 );
ini_set( 'session.use_trans_sid', 0 );
// Also set a sane serialization handler
\Wikimedia\PhpSessionSerializer::setSerializeHandler();
session_set_save_handler(
array( self::$instance, 'open' ),
array( self::$instance, 'close' ),
array( self::$instance, 'read' ),
array( self::$instance, 'write' ),
array( self::$instance, 'destroy' ),
array( self::$instance, 'gc' )
);
// It's necessary to register a shutdown function to call session_write_close(),
// because by the time the request shutdown function for the session module is
// called, other needed objects may have already been destroyed. Shutdown functions
// registered this way are called before object destruction.
register_shutdown_function( array( self::$instance, 'handleShutdown' ) );
}
/**
* Set the manager, store, and logger
* @private Use self::install().
* @param SessionManager $manager
* @param BagOStuff $store
* @param LoggerInterface $store
*/
public function setManager(
SessionManager $manager, BagOStuff $store, LoggerInterface $logger
) {
if ( $this->manager !== $manager ) {
// Close any existing session before we change stores
if ( $this->manager ) {
session_write_close();
}
$this->manager = $manager;
$this->store = $store;
$this->logger = $logger;
\Wikimedia\PhpSessionSerializer::setLogger( $this->logger );
}
}
/**
* Initialize the session (handler)
* @private For internal use only
* @param string $save_path Path used to store session files (ignored)
* @param string $session_name Session name (ignored)
* @return bool Success
*/
public function open( $save_path, $session_name ) {
if ( self::$instance !== $this ) {
throw new \UnexpectedValueException( __METHOD__ . ': Wrong instance called!' );
}
if ( !$this->enable ) {
throw new \BadMethodCallException( 'Attempt to use PHP session management' );
}
return true;
}
/**
* Close the session (handler)
* @private For internal use only
* @return bool Success
*/
public function close() {
if ( self::$instance !== $this ) {
throw new \UnexpectedValueException( __METHOD__ . ': Wrong instance called!' );
}
$this->sessionFieldCache = array();
return true;
}
/**
* Read session data
* @private For internal use only
* @param string $id Session id
* @return string Session data
*/
public function read( $id ) {
if ( self::$instance !== $this ) {
throw new \UnexpectedValueException( __METHOD__ . ': Wrong instance called!' );
}
if ( !$this->enable ) {
throw new \BadMethodCallException( 'Attempt to use PHP session management' );
}
$session = $this->manager->getSessionById( $id, true );
if ( !$session ) {
return '';
}
$session->persist();
$data = iterator_to_array( $session );
$this->sessionFieldCache[$id] = $data;
return (string)\Wikimedia\PhpSessionSerializer::encode( $data );
}
/**
* Write session data
* @private For internal use only
* @param string $id Session id
* @param string $dataStr Session data. Not that you should ever call this
* directly, but note that this has the same issues with code injection
* via user-controlled data as does PHP's unserialize function.
* @return bool Success
*/
public function write( $id, $dataStr ) {
if ( self::$instance !== $this ) {
throw new \UnexpectedValueException( __METHOD__ . ': Wrong instance called!' );
}
if ( !$this->enable ) {
throw new \BadMethodCallException( 'Attempt to use PHP session management' );
}
$session = $this->manager->getSessionById( $id );
// First, decode the string PHP handed us
$data = \Wikimedia\PhpSessionSerializer::decode( $dataStr );
if ( $data === null ) {
// @codeCoverageIgnoreStart
return false;
// @codeCoverageIgnoreEnd
}
// Now merge the data into the Session object.
$changed = false;
$cache = isset( $this->sessionFieldCache[$id] ) ? $this->sessionFieldCache[$id] : array();
foreach ( $data as $key => $value ) {
if ( !isset( $cache[$key] ) ) {
if ( $session->exists( $key ) ) {
// New in both, so ignore and log
$this->logger->warning(
__METHOD__ . ": Key \"$key\" added in both Session and \$_SESSION!"
);
} else {
// New in $_SESSION, keep it
$session->set( $key, $value );
$changed = true;
}
} elseif ( $cache[$key] === $value ) {
// Unchanged in $_SESSION, so ignore it
} elseif ( !$session->exists( $key ) ) {
// Deleted in Session, keep but log
$this->logger->warning(
__METHOD__ . ": Key \"$key\" deleted in Session and changed in \$_SESSION!"
);
$session->set( $key, $value );
$changed = true;
} elseif ( $cache[$key] === $session->get( $key ) ) {
// Unchanged in Session, so keep it
$session->set( $key, $value );
$changed = true;
} else {
// Changed in both, so ignore and log
$this->logger->warning(
__METHOD__ . ": Key \"$key\" changed in both Session and \$_SESSION!"
);
}
}
// Anything deleted in $_SESSION and unchanged in Session should be deleted too
// (but not if $_SESSION can't represent it at all)
\Wikimedia\PhpSessionSerializer::setLogger( new \Psr\Log\NullLogger() );
foreach ( $cache as $key => $value ) {
if ( !isset( $data[$key] ) && $session->exists( $key ) &&
\Wikimedia\PhpSessionSerializer::encode( array( $key => true ) )
) {
if ( $cache[$key] === $session->get( $key ) ) {
// Unchanged in Session, delete it
$session->remove( $key );
$changed = true;
} else {
// Changed in Session, ignore deletion and log
$this->logger->warning(
__METHOD__ . ": Key \"$key\" changed in Session and deleted in \$_SESSION!"
);
}
}
}
\Wikimedia\PhpSessionSerializer::setLogger( $this->logger );
// Save and update cache if anything changed
if ( $changed ) {
if ( $this->warn ) {
wfDeprecated( '$_SESSION', '1.27' );
$this->logger->warning( 'Something wrote to $_SESSION!' );
}
$session->save();
$this->sessionFieldCache[$id] = iterator_to_array( $session );
}
$session->persist();
return true;
}
/**
* Destroy a session
* @private For internal use only
* @param string $id Session id
* @return bool Success
*/
public function destroy( $id ) {
if ( self::$instance !== $this ) {
throw new \UnexpectedValueException( __METHOD__ . ': Wrong instance called!' );
}
if ( !$this->enable ) {
throw new \BadMethodCallException( 'Attempt to use PHP session management' );
}
$session = $this->manager->getSessionById( $id, true );
if ( $session ) {
$session->clear();
}
return true;
}
/**
* Execute garbage collection.
* @private For internal use only
* @param int $maxlifetime Maximum session life time (ignored)
* @return bool Success
*/
public function gc( $maxlifetime ) {
if ( self::$instance !== $this ) {
throw new \UnexpectedValueException( __METHOD__ . ': Wrong instance called!' );
}
$before = date( 'YmdHis', time() );
$this->store->deleteObjectsExpiringBefore( $before );
return true;
}
/**
* Shutdown function.
*
* See the comment inside self::install for rationale.
* @codeCoverageIgnore
* @private For internal use only
*/
public function handleShutdown() {
if ( $this->enable ) {
session_write_close();
}
}
}

View file

@ -0,0 +1,364 @@
<?php
/**
* MediaWiki session
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along
* with this program; if not, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
* http://www.gnu.org/copyleft/gpl.html
*
* @file
* @ingroup Session
*/
namespace MediaWiki\Session;
use User;
use WebRequest;
/**
* Manages data for an an authenticated session
*
* A Session represents the fact that the current HTTP request is part of a
* session. There are two broad types of Sessions, based on whether they
* return true or false from self::canSetUser():
* * When true (mutable), the Session identifies multiple requests as part of
* a session generically, with no tie to a particular user.
* * When false (immutable), the Session identifies multiple requests as part
* of a session by identifying and authenticating the request itself as
* belonging to a particular user.
*
* The Session object also serves as a replacement for PHP's $_SESSION,
* managing access to per-session data.
*
* @todo Once we drop support for PHP 5.3.3, implementing ArrayAccess would be nice.
* @ingroup Session
* @since 1.27
*/
final class Session implements \Countable, \Iterator {
/** @var SessionBackend Session backend */
private $backend;
/** @var int Session index */
private $index;
/**
* @param SessionBackend $backend
* @param int $index
*/
public function __construct( SessionBackend $backend, $index ) {
$this->backend = $backend;
$this->index = $index;
}
public function __destruct() {
$this->backend->deregisterSession( $this->index );
}
/**
* Returns the session ID
* @return string
*/
public function getId() {
return $this->backend->getId();
}
/**
* Returns the SessionId object
* @private For internal use by WebRequest
* @return SessionId
*/
public function getSessionId() {
return $this->backend->getSessionId();
}
/**
* Changes the session ID
* @return string New ID (might be the same as the old)
*/
public function resetId() {
return $this->backend->resetId();
}
/**
* Fetch the SessionProvider for this session
* @return SessionProviderInterface
*/
public function getProvider() {
return $this->backend->getProvider();
}
/**
* Indicate whether this session is persisted across requests
*
* For example, if cookies are set.
*
* @return bool
*/
public function isPersistent() {
return $this->backend->isPersistent();
}
/**
* Make this session persisted across requests
*
* If the session is already persistent, equivalent to calling
* $this->renew().
*/
public function persist() {
$this->backend->persist();
}
/**
* Indicate whether the user should be remembered independently of the
* session ID.
* @return bool
*/
public function shouldRememberUser() {
return $this->backend->shouldRememberUser();
}
/**
* Set whether the user should be remembered independently of the session
* ID.
* @param bool $remember
*/
public function setRememberUser( $remember ) {
$this->backend->setRememberUser( $remember );
}
/**
* Returns the request associated with this session
* @return WebRequest
*/
public function getRequest() {
return $this->backend->getRequest( $this->index );
}
/**
* Returns the authenticated user for this session
* @return User
*/
public function getUser() {
return $this->backend->getUser();
}
/**
* Indicate whether the session user info can be changed
* @return bool
*/
public function canSetUser() {
return $this->backend->canSetUser();
}
/**
* Set a new user for this session
* @note This should only be called when the user has been authenticated
* @param User $user User to set on the session.
* This may become a "UserValue" in the future, or User may be refactored
* into such.
*/
public function setUser( $user ) {
$this->backend->setUser( $user );
}
/**
* Get a suggested username for the login form
* @return string|null
*/
public function suggestLoginUsername() {
return $this->backend->suggestLoginUsername( $this->index );
}
/**
* Whether HTTPS should be forced
* @return bool
*/
public function shouldForceHTTPS() {
return $this->backend->shouldForceHTTPS();
}
/**
* Set whether HTTPS should be forced
* @param bool $force
*/
public function setForceHTTPS( $force ) {
$this->backend->setForceHTTPS( $force );
}
/**
* Fetch the "logged out" timestamp
* @return int
*/
public function getLoggedOutTimestamp() {
return $this->backend->getLoggedOutTimestamp();
}
/**
* Set the "logged out" timestamp
* @param int $ts
*/
public function setLoggedOutTimestamp( $ts ) {
$this->backend->setLoggedOutTimestamp( $ts );
}
/**
* Fetch provider metadata
* @protected For use by SessionProvider subclasses only
* @return mixed
*/
public function getProviderMetadata() {
return $this->backend->getProviderMetadata();
}
/**
* Delete all session data and clear the user (if possible)
*/
public function clear() {
$data = &$this->backend->getData();
if ( $data ) {
$data = array();
$this->backend->dirty();
}
if ( $this->backend->canSetUser() ) {
$this->backend->setUser( new User );
}
$this->backend->save();
}
/**
* Renew the session
*
* Resets the TTL in the backend store if the session is near expiring, and
* re-persists the session to any active WebRequests if persistent.
*/
public function renew() {
$this->backend->renew();
}
/**
* Fetch a copy of this session attached to an alternative WebRequest
*
* Actions on the copy will affect this session too, and vice versa.
*
* @param WebRequest $request Any existing session associated with this
* WebRequest object will be overwritten.
* @return Session
*/
public function sessionWithRequest( WebRequest $request ) {
$request->setSessionId( $this->backend->getSessionId() );
return $this->backend->getSession( $request );
}
/**
* Fetch a value from the session
* @param string|int $key
* @param mixed $default
* @return mixed
*/
public function get( $key, $default = null ) {
$data = &$this->backend->getData();
return array_key_exists( $key, $data ) ? $data[$key] : $default;
}
/**
* Test if a value exists in the session
* @param string|int $key
* @return bool
*/
public function exists( $key ) {
$data = &$this->backend->getData();
return array_key_exists( $key, $data );
}
/**
* Set a value in the session
* @param string|int $key
* @param mixed $value
*/
public function set( $key, $value ) {
$data = &$this->backend->getData();
if ( !array_key_exists( $key, $data ) || $data[$key] !== $value ) {
$data[$key] = $value;
$this->backend->dirty();
}
}
/**
* Remove a value from the session
* @param string|int $key
*/
public function remove( $key ) {
$data = &$this->backend->getData();
if ( array_key_exists( $key, $data ) ) {
unset( $data[$key] );
$this->backend->dirty();
}
}
/**
* Delay automatic saving while multiple updates are being made
*
* Calls to save() or clear() will not be delayed.
*
* @return \ScopedCallback When this goes out of scope, a save will be triggered
*/
public function delaySave() {
return $this->backend->delaySave();
}
/**
* Save the session
*/
public function save() {
$this->backend->save();
}
/**
* @name Interface methods
* @{
*/
public function count() {
$data = &$this->backend->getData();
return count( $data );
}
public function current() {
$data = &$this->backend->getData();
return current( $data );
}
public function key() {
$data = &$this->backend->getData();
return key( $data );
}
public function next() {
$data = &$this->backend->getData();
next( $data );
}
public function rewind() {
$data = &$this->backend->getData();
reset( $data );
}
public function valid() {
$data = &$this->backend->getData();
return key( $data ) !== null;
}
/**@}*/
}

View file

@ -0,0 +1,624 @@
<?php
/**
* MediaWiki session backend
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along
* with this program; if not, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
* http://www.gnu.org/copyleft/gpl.html
*
* @file
* @ingroup Session
*/
namespace MediaWiki\Session;
use BagOStuff;
use Psr\Log\LoggerInterface;
use User;
use WebRequest;
/**
* This is the actual workhorse for Session.
*
* Most code does not need to use this class, you want \\MediaWiki\\Session\\Session.
* The exceptions are SessionProviders and SessionMetadata hook functions,
* which get an instance of this class rather than Session.
*
* The reasons for this split are:
* 1. A session can be attached to multiple requests, but we want the Session
* object to have some features that correspond to just one of those
* requests.
* 2. We want reasonable garbage collection behavior, but we also want the
* SessionManager to hold a reference to every active session so it can be
* saved when the request ends.
*
* @ingroup Session
* @since 1.27
*/
final class SessionBackend {
/** @var SessionId */
private $id;
private $persist = false;
private $remember = false;
private $forceHTTPS = false;
/** @var array|null */
private $data = null;
private $forcePersist = false;
private $metaDirty = false;
private $dataDirty = false;
/** @var string Used to detect subarray modifications */
private $dataHash = null;
/** @var BagOStuff */
private $store;
/** @var LoggerInterface */
private $logger;
/** @var int */
private $lifetime;
/** @var User */
private $user;
private $curIndex = 0;
/** @var WebRequest[] Session requests */
private $requests = array();
/** @var SessionProvider provider */
private $provider;
/** @var array|null provider-specified metadata */
private $providerMetadata = null;
private $expires = 0;
private $loggedOut = 0;
private $delaySave = 0;
private $usePhpSessionHandling = true;
private $checkPHPSessionRecursionGuard = false;
/**
* @param SessionId $id Session ID object
* @param SessionInfo $info Session info to populate from
* @param BagOStuff $store Backend data store
* @param LoggerInterface $logger
* @param int $lifetime Session data lifetime in seconds
*/
public function __construct(
SessionId $id, SessionInfo $info, BagOStuff $store, LoggerInterface $logger, $lifetime
) {
$phpSessionHandling = \RequestContext::getMain()->getConfig()->get( 'PHPSessionHandling' );
$this->usePhpSessionHandling = $phpSessionHandling !== 'disable';
if ( $info->getUserInfo() && !$info->getUserInfo()->isVerified() ) {
throw new \InvalidArgumentException(
"Refusing to create session for unverified user {$info->getUserInfo()}"
);
}
if ( $info->getProvider() === null ) {
throw new \InvalidArgumentException( 'Cannot create session without a provider' );
}
if ( $info->getId() !== $id->getId() ) {
throw new \InvalidArgumentException( 'SessionId and SessionInfo don\'t match' );
}
$this->id = $id;
$this->user = $info->getUserInfo() ? $info->getUserInfo()->getUser() : new User;
$this->store = $store;
$this->logger = $logger;
$this->lifetime = $lifetime;
$this->provider = $info->getProvider();
$this->persist = $info->wasPersisted();
$this->remember = $info->wasRemembered();
$this->forceHTTPS = $info->forceHTTPS();
$this->providerMetadata = $info->getProviderMetadata();
$blob = $store->get( wfMemcKey( 'MWSession', (string)$this->id ) );
if ( !is_array( $blob ) ||
!isset( $blob['metadata'] ) || !is_array( $blob['metadata'] ) ||
!isset( $blob['data'] ) || !is_array( $blob['data'] )
) {
$this->data = array();
$this->dataDirty = true;
$this->metaDirty = true;
$this->logger->debug( "SessionBackend $this->id is unsaved, marking dirty in constructor" );
} else {
$this->data = $blob['data'];
if ( isset( $blob['metadata']['loggedOut'] ) ) {
$this->loggedOut = (int)$blob['metadata']['loggedOut'];
}
if ( isset( $blob['metadata']['expires'] ) ) {
$this->expires = (int)$blob['metadata']['expires'];
} else {
$this->metaDirty = true;
$this->logger->debug(
"SessionBackend $this->id metadata dirty due to missing expiration timestamp"
);
}
}
$this->dataHash = md5( serialize( $this->data ) );
}
/**
* Return a new Session for this backend
* @param WebRequest $request
* @return Session
*/
public function getSession( WebRequest $request ) {
$index = ++$this->curIndex;
$this->requests[$index] = $request;
$session = new Session( $this, $index );
return $session;
}
/**
* Deregister a Session
* @private For use by \\MediaWiki\\Session\\Session::__destruct() only
* @param int $index
*/
public function deregisterSession( $index ) {
unset( $this->requests[$index] );
if ( !count( $this->requests ) ) {
$this->save( true );
$this->provider->getManager()->deregisterSessionBackend( $this );
}
}
/**
* Returns the session ID.
* @return string
*/
public function getId() {
return (string)$this->id;
}
/**
* Fetch the SessionId object
* @private For internal use by WebRequest
* @return SessionId
*/
public function getSessionId() {
return $this->id;
}
/**
* Changes the session ID
* @return string New ID (might be the same as the old)
*/
public function resetId() {
if ( $this->provider->persistsSessionId() ) {
$oldId = (string)$this->id;
$restart = $this->usePhpSessionHandling && $oldId === session_id() &&
PHPSessionHandler::isEnabled();
if ( $restart ) {
// If this session is the one behind PHP's $_SESSION, we need
// to close then reopen it.
session_write_close();
}
$this->provider->getManager()->changeBackendId( $this );
$this->provider->sessionIdWasReset( $this, $oldId );
$this->metaDirty = true;
$this->logger->debug(
"SessionBackend $this->id metadata dirty due to ID reset (formerly $oldId)"
);
if ( $restart ) {
session_id( (string)$this->id );
\MediaWiki\quietCall( 'session_start' );
}
$this->autosave();
// Delete the data for the old session ID now
$this->store->delete( wfMemcKey( 'MWSession', $oldId ) );
}
}
/**
* Fetch the SessionProvider for this session
* @return SessionProviderInterface
*/
public function getProvider() {
return $this->provider;
}
/**
* Indicate whether this session is persisted across requests
*
* For example, if cookies are set.
*
* @return bool
*/
public function isPersistent() {
return $this->persist;
}
/**
* Make this session persisted across requests
*
* If the session is already persistent, equivalent to calling
* $this->renew().
*/
public function persist() {
if ( !$this->persist ) {
$this->persist = true;
$this->forcePersist = true;
$this->logger->debug( "SessionBackend $this->id force-persist due to persist()" );
$this->autosave();
} else {
$this->renew();
}
}
/**
* Indicate whether the user should be remembered independently of the
* session ID.
* @return bool
*/
public function shouldRememberUser() {
return $this->remember;
}
/**
* Set whether the user should be remembered independently of the session
* ID.
* @param bool $remember
*/
public function setRememberUser( $remember ) {
if ( $this->remember !== (bool)$remember ) {
$this->remember = (bool)$remember;
$this->metaDirty = true;
$this->logger->debug( "SessionBackend $this->id metadata dirty due to remember-user change" );
$this->autosave();
}
}
/**
* Returns the request associated with a Session
* @param int $index Session index
* @return WebRequest
*/
public function getRequest( $index ) {
if ( !isset( $this->requests[$index] ) ) {
throw new \InvalidArgumentException( 'Invalid session index' );
}
return $this->requests[$index];
}
/**
* Returns the authenticated user for this session
* @return User
*/
public function getUser() {
return $this->user;
}
/**
* Indicate whether the session user info can be changed
* @return bool
*/
public function canSetUser() {
return $this->provider->canChangeUser();
}
/**
* Set a new user for this session
* @note This should only be called when the user has been authenticated via a login process
* @param User $user User to set on the session.
* This may become a "UserValue" in the future, or User may be refactored
* into such.
*/
public function setUser( $user ) {
if ( !$this->canSetUser() ) {
throw new \BadMethodCallException(
'Cannot set user on this session; check $session->canSetUser() first'
);
}
$this->user = $user;
$this->metaDirty = true;
$this->logger->debug( "SessionBackend $this->id metadata dirty due to user change" );
$this->autosave();
}
/**
* Get a suggested username for the login form
* @param int $index Session index
* @return string|null
*/
public function suggestLoginUsername( $index ) {
if ( !isset( $this->requests[$index] ) ) {
throw new \InvalidArgumentException( 'Invalid session index' );
}
return $this->provider->suggestLoginUsername( $this->requests[$index] );
}
/**
* Whether HTTPS should be forced
* @return bool
*/
public function shouldForceHTTPS() {
return $this->forceHTTPS;
}
/**
* Set whether HTTPS should be forced
* @param bool $force
*/
public function setForceHTTPS( $force ) {
if ( $this->forceHTTPS !== (bool)$force ) {
$this->forceHTTPS = (bool)$force;
$this->metaDirty = true;
$this->logger->debug( "SessionBackend $this->id metadata dirty due to force-HTTPS change" );
$this->autosave();
}
}
/**
* Fetch the "logged out" timestamp
* @return int
*/
public function getLoggedOutTimestamp() {
return $this->loggedOut;
}
/**
* Set the "logged out" timestamp
* @param int $ts
*/
public function setLoggedOutTimestamp( $ts = null ) {
$ts = (int)$ts;
if ( $this->loggedOut !== $ts ) {
$this->loggedOut = $ts;
$this->metaDirty = true;
$this->logger->debug(
"SessionBackend $this->id metadata dirty due to logged-out-timestamp change"
);
$this->autosave();
}
}
/**
* Fetch provider metadata
* @protected For use by SessionProvider subclasses only
* @return mixed
*/
public function getProviderMetadata() {
return $this->providerMetadata;
}
/**
* Fetch the session data array
*
* Note the caller is responsible for calling $this->dirty() if anything in
* the array is changed.
*
* @private For use by \\MediaWiki\\Session\\Session only.
* @return array
*/
public function &getData() {
return $this->data;
}
/**
* Add data to the session.
*
* Overwrites any existing data under the same keys.
*
* @param array $newData Key-value pairs to add to the session
*/
public function addData( array $newData ) {
$data = &$this->getData();
foreach ( $newData as $key => $value ) {
if ( !array_key_exists( $key, $data ) || $data[$key] !== $value ) {
$data[$key] = $value;
$this->dataDirty = true;
$this->logger->debug(
"SessionBackend $this->id data dirty due to addData(): " . wfGetAllCallers( 5 )
);
}
}
}
/**
* Mark data as dirty
* @private For use by \\MediaWiki\\Session\\Session only.
*/
public function dirty() {
$this->dataDirty = true;
$this->logger->debug(
"SessionBackend $this->id data dirty due to dirty(): " . wfGetAllCallers( 5 )
);
}
/**
* Renew the session by resaving everything
*
* Resets the TTL in the backend store if the session is near expiring, and
* re-persists the session to any active WebRequests if persistent.
*/
public function renew() {
if ( time() + $this->lifetime / 2 > $this->expires ) {
$this->metaDirty = true;
$this->logger->debug(
"SessionBackend $this->id metadata dirty for renew(): " . wfGetAllCallers( 5 )
);
if ( $this->persist ) {
$this->forcePersist = true;
$this->logger->debug(
"SessionBackend $this->id force-persist for renew(): " . wfGetAllCallers( 5 )
);
}
}
$this->autosave();
}
/**
* Delay automatic saving while multiple updates are being made
*
* Calls to save() will not be delayed.
*
* @return \ScopedCallback When this goes out of scope, a save will be triggered
*/
public function delaySave() {
$that = $this;
$this->delaySave++;
$ref = &$this->delaySave;
return new \ScopedCallback( function () use ( $that, &$ref ) {
if ( --$ref <= 0 ) {
$ref = 0;
$that->save();
}
} );
}
/**
* Save and persist session data, unless delayed
*/
private function autosave() {
if ( $this->delaySave <= 0 ) {
$this->save();
}
}
/**
* Save and persist session data
* @param bool $closing Whether the session is being closed
*/
public function save( $closing = false ) {
if ( $this->provider->getManager()->isUserSessionPrevented( $this->user->getName() ) ) {
$this->logger->debug(
"SessionBackend $this->id not saving, " .
"user {$this->user} was passed to SessionManager::preventSessionsForUser"
);
return;
}
// Ensure the user has a token
// @codeCoverageIgnoreStart
$anon = $this->user->isAnon();
if ( !$anon && !$this->user->getToken() ) {
$this->logger->debug(
"SessionBackend $this->id creating token for user {$this->user} on save"
);
$this->user->setToken();
if ( !wfReadOnly() ) {
$this->user->saveSettings();
}
$this->metaDirty = true;
}
// @codeCoverageIgnoreEnd
if ( !$this->metaDirty && !$this->dataDirty &&
$this->dataHash !== md5( serialize( $this->data ) )
) {
$this->logger->debug( "SessionBackend $this->id data dirty due to hash mismatch, " .
"$this->dataHash !== " . md5( serialize( $this->data ) ) );
$this->dataDirty = true;
}
if ( !$this->metaDirty && !$this->dataDirty && !$this->forcePersist ) {
return;
}
$this->logger->debug( "SessionBackend $this->id save: " .
'dataDirty=' . (int)$this->dataDirty . ' ' .
'metaDirty=' . (int)$this->metaDirty . ' ' .
'forcePersist=' . (int)$this->forcePersist
);
// Persist to the provider, if flagged
if ( $this->persist && ( $this->metaDirty || $this->forcePersist ) ) {
foreach ( $this->requests as $request ) {
$request->setSessionId( $this->getSessionId() );
$this->provider->persistSession( $this, $request );
}
if ( !$closing ) {
$this->checkPHPSession();
}
}
$this->forcePersist = false;
if ( !$this->metaDirty && !$this->dataDirty ) {
return;
}
// Save session data to store, if necessary
$metadata = $origMetadata = array(
'provider' => (string)$this->provider,
'providerMetadata' => $this->providerMetadata,
'userId' => $anon ? 0 : $this->user->getId(),
'userName' => $anon ? null : $this->user->getName(),
'userToken' => $anon ? null : $this->user->getToken(),
'remember' => !$anon && $this->remember,
'forceHTTPS' => $this->forceHTTPS,
'expires' => time() + $this->lifetime,
'loggedOut' => $this->loggedOut,
);
\Hooks::run( 'SessionMetadata', array( $this, &$metadata, $this->requests ) );
foreach ( $origMetadata as $k => $v ) {
if ( $metadata[$k] !== $v ) {
throw new \UnexpectedValueException( "SessionMetadata hook changed metadata key \"$k\"" );
}
}
$this->store->set(
wfMemcKey( 'MWSession', (string)$this->id ),
array(
'data' => $this->data,
'metadata' => $metadata,
),
$metadata['expires']
);
$this->metaDirty = false;
$this->dataDirty = false;
$this->dataHash = md5( serialize( $this->data ) );
$this->expires = $metadata['expires'];
}
/**
* For backwards compatibility, open the PHP session when the global
* session is persisted
*/
private function checkPHPSession() {
if ( !$this->checkPHPSessionRecursionGuard ) {
$this->checkPHPSessionRecursionGuard = true;
$ref = &$this->checkPHPSessionRecursionGuard;
$reset = new \ScopedCallback( function () use ( &$ref ) {
$ref = false;
} );
if ( $this->usePhpSessionHandling && session_id() === '' && PHPSessionHandler::isEnabled() &&
SessionManager::getGlobalSession()->getId() === (string)$this->id
) {
$this->logger->debug( "SessionBackend $this->id: Taking over PHP session" );
session_id( (string)$this->id );
\MediaWiki\quietCall( 'session_start' );
}
}
}
}

View file

@ -0,0 +1,70 @@
<?php
/**
* MediaWiki session ID holder
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along
* with this program; if not, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
* http://www.gnu.org/copyleft/gpl.html
*
* @file
* @ingroup Session
*/
namespace MediaWiki\Session;
/**
* Value object holding the session ID in a manner that can be globally
* updated.
*
* This class exists because we want WebRequest to refer to the session, but it
* can't hold the Session itself due to issues with circular references and it
* can't just hold the ID as a string because we need to be able to update the
* ID when SessionBackend::resetId() is called.
*
* @ingroup Session
* @since 1.27
*/
final class SessionId {
/** @var string */
private $id;
/**
* @param string $id
*/
public function __construct( $id ) {
$this->id = $id;
}
/**
* Get the ID
* @return string
*/
public function getId() {
return $this->id;
}
/**
* Set the ID
* @private For use by \\MediaWiki\\Session\\SessionManager only
* @param string $id
*/
public function setId( $id ) {
$this->id = $id;
}
public function __toString() {
return $this->id;
}
}

View file

@ -0,0 +1,270 @@
<?php
/**
* MediaWiki session info
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along
* with this program; if not, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
* http://www.gnu.org/copyleft/gpl.html
*
* @file
* @ingroup Session
*/
namespace MediaWiki\Session;
use Psr\Log\LoggerInterface;
use BagOStuff;
use WebRequest;
/**
* Value object returned by SessionProvider
*
* This holds the data necessary to construct a Session.
*
* @ingroup Session
* @since 1.27
*/
class SessionInfo {
/** Minimum allowed priority */
const MIN_PRIORITY = 1;
/** Maximum allowed priority */
const MAX_PRIORITY = 100;
/** @var SessionProvider|null */
private $provider;
/** @var string */
private $id;
/** @var int */
private $priority;
/** @var UserInfo|null */
private $userInfo = null;
private $persisted = false;
private $remembered = false;
private $forceHTTPS = false;
private $idIsSafe = false;
/** @var array|null */
private $providerMetadata = null;
/**
* @param int $priority Session priority
* @param array $data
* - provider: (SessionProvider|null) If not given, the provider will be
* determined from the saved session data.
* - id: (string|null) Session ID
* - userInfo: (UserInfo|null) User known from the request. If
* $provider->canChangeUser() is false, a verified user
* must be provided.
* - persisted: (bool) Whether this session was persisted
* - remembered: (bool) Whether the verified user was remembered.
* Defaults to true.
* - forceHTTPS: (bool) Whether to force HTTPS for this session
* - metadata: (array) Provider metadata, to be returned by
* Session::getProviderMetadata().
* - idIsSafe: (bool) Set true if the 'id' did not come from the user.
* Generally you'll use this from SessionProvider::newEmptySession(),
* and not from any other method.
* - copyFrom: (SessionInfo) SessionInfo to copy other data items from.
*/
public function __construct( $priority, array $data ) {
if ( $priority < self::MIN_PRIORITY || $priority > self::MAX_PRIORITY ) {
throw new \InvalidArgumentException( 'Invalid priority' );
}
if ( isset( $data['copyFrom'] ) ) {
$from = $data['copyFrom'];
if ( !$from instanceof SessionInfo ) {
throw new \InvalidArgumentException( 'Invalid copyFrom' );
}
$data += array(
'provider' => $from->provider,
'id' => $from->id,
'userInfo' => $from->userInfo,
'persisted' => $from->persisted,
'remembered' => $from->remembered,
'forceHTTPS' => $from->forceHTTPS,
'metadata' => $from->providerMetadata,
'idIsSafe' => $from->idIsSafe,
// @codeCoverageIgnoreStart
);
// @codeCoverageIgnoreEnd
} else {
$data += array(
'provider' => null,
'id' => null,
'userInfo' => null,
'persisted' => false,
'remembered' => true,
'forceHTTPS' => false,
'metadata' => null,
'idIsSafe' => false,
// @codeCoverageIgnoreStart
);
// @codeCoverageIgnoreEnd
}
if ( $data['id'] !== null && !SessionManager::validateSessionId( $data['id'] ) ) {
throw new \InvalidArgumentException( 'Invalid session ID' );
}
if ( $data['userInfo'] !== null && !$data['userInfo'] instanceof UserInfo ) {
throw new \InvalidArgumentException( 'Invalid userInfo' );
}
if ( !$data['provider'] && $data['id'] === null ) {
throw new \InvalidArgumentException(
'Must supply an ID when no provider is given'
);
}
if ( $data['metadata'] !== null && !is_array( $data['metadata'] ) ) {
throw new \InvalidArgumentException( 'Invalid metadata' );
}
$this->provider = $data['provider'];
if ( $data['id'] !== null ) {
$this->id = $data['id'];
$this->idIsSafe = $data['idIsSafe'];
} else {
$this->id = $this->provider->getManager()->generateSessionId();
$this->idIsSafe = true;
}
$this->priority = (int)$priority;
$this->userInfo = $data['userInfo'];
$this->persisted = (bool)$data['persisted'];
if ( $data['provider'] !== null ) {
if ( $this->userInfo !== null && !$this->userInfo->isAnon() && $this->userInfo->isVerified() ) {
$this->remembered = (bool)$data['remembered'];
}
$this->providerMetadata = $data['metadata'];
}
$this->forceHTTPS = (bool)$data['forceHTTPS'];
}
/**
* Return the provider
* @return SessionProvider|null
*/
final public function getProvider() {
return $this->provider;
}
/**
* Return the session ID
* @return string
*/
final public function getId() {
return $this->id;
}
/**
* Indicate whether the ID is "safe"
*
* The ID is safe in the following cases:
* - The ID was randomly generated by the constructor.
* - The ID was found in the backend data store.
* - $this->getProvider()->persistsSessionId() is false.
* - The constructor was explicitly told it's safe using the 'idIsSafe'
* parameter.
*
* @return bool
*/
final public function isIdSafe() {
return $this->idIsSafe;
}
/**
* Return the priority
* @return int
*/
final public function getPriority() {
return $this->priority;
}
/**
* Return the user
* @return UserInfo|null
*/
final public function getUserInfo() {
return $this->userInfo;
}
/**
* Return whether the session is persisted
*
* i.e. a session ID was given to the constuctor
*
* @return bool
*/
final public function wasPersisted() {
return $this->persisted;
}
/**
* Return provider metadata
* @return array|null
*/
final public function getProviderMetadata() {
return $this->providerMetadata;
}
/**
* Return whether the user was remembered
*
* For providers that can persist the user separately from the session,
* the human using it may not actually *want* that to be done. For example,
* a cookie-based provider can set cookies that are longer-lived than the
* backend session data, but on a public terminal the human likely doesn't
* want those cookies set.
*
* This is false unless a non-anonymous verified user was passed to
* the SessionInfo constructor by the provider, and the provider didn't
* pass false for the 'remembered' data item.
*
* @return bool
*/
final public function wasRemembered() {
return $this->remembered;
}
/**
* Whether this session should only be used over HTTPS
* @return bool
*/
final public function forceHTTPS() {
return $this->forceHTTPS;
}
public function __toString() {
return '[' . $this->getPriority() . ']' .
( $this->getProvider() ?: 'null' ) .
( $this->userInfo ?: '<null>' ) . $this->getId();
}
/**
* Compare two SessionInfo objects by priority
* @param SessionInfo $a
* @param SessionInfo $b
* @return int Negative if $a < $b, positive if $a > $b, zero if equal
*/
public static function compare( $a, $b ) {
return $a->getPriority() - $b->getPriority();
}
}

View file

@ -0,0 +1,997 @@
<?php
/**
* MediaWiki\Session entry point
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along
* with this program; if not, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
* http://www.gnu.org/copyleft/gpl.html
*
* @file
* @ingroup Session
*/
namespace MediaWiki\Session;
use Psr\Log\LoggerInterface;
use BagOStuff;
use Config;
use FauxRequest;
use Language;
use Message;
use User;
use WebRequest;
/**
* This serves as the entry point to the MediaWiki session handling system.
*
* @ingroup Session
* @since 1.27
*/
final class SessionManager implements SessionManagerInterface {
/** @var SessionManager|null */
private static $instance = null;
/** @var Session|null */
private static $globalSession = null;
/** @var WebRequest|null */
private static $globalSessionRequest = null;
/** @var LoggerInterface */
private $logger;
/** @var Config */
private $config;
/** @var BagOStuff|null */
private $store;
/** @var SessionProvider[] */
private $sessionProviders = null;
/** @var string[] */
private $varyCookies = null;
/** @var array */
private $varyHeaders = null;
/** @var SessionBackend[] */
private $allSessionBackends = array();
/** @var SessionId[] */
private $allSessionIds = array();
/** @var string[] */
private $preventUsers = array();
/**
* Get the global SessionManager
* @return SessionManagerInterface
* (really a SessionManager, but this is to make IDEs less confused)
*/
public static function singleton() {
if ( self::$instance === null ) {
self::$instance = new self();
}
return self::$instance;
}
/**
* Get the "global" session
*
* If PHP's session_id() has been set, returns that session. Otherwise
* returns the session for RequestContext::getMain()->getRequest().
*
* @return Session
*/
public static function getGlobalSession() {
if ( !PHPSessionHandler::isEnabled() ) {
$id = '';
} else {
$id = session_id();
}
$request = \RequestContext::getMain()->getRequest();
if (
!self::$globalSession // No global session is set up yet
|| self::$globalSessionRequest !== $request // The global WebRequest changed
|| $id !== '' && self::$globalSession->getId() !== $id // Someone messed with session_id()
) {
self::$globalSessionRequest = $request;
if ( $id === '' ) {
// session_id() wasn't used, so fetch the Session from the WebRequest.
// We use $request->getSession() instead of $singleton->getSessionForRequest()
// because doing the latter would require a public
// "$request->getSessionId()" method that would confuse end
// users by returning SessionId|null where they'd expect it to
// be short for $request->getSession()->getId(), and would
// wind up being a duplicate of the code in
// $request->getSession() anyway.
self::$globalSession = $request->getSession();
} else {
// Someone used session_id(), so we need to follow suit.
// Note this overwrites whatever session might already be
// associated with $request with the one for $id.
self::$globalSession = self::singleton()->getSessionById( $id, false, $request );
}
}
return self::$globalSession;
}
/**
* @param array $options
* - config: Config to fetch configuration from. Defaults to the default 'main' config.
* - logger: LoggerInterface to use for logging. Defaults to the 'session' channel.
* - store: BagOStuff to store session data in.
*/
public function __construct( $options = array() ) {
if ( isset( $options['config'] ) ) {
$this->config = $options['config'];
if ( !$this->config instanceof Config ) {
throw new \InvalidArgumentException(
'$options[\'config\'] must be an instance of Config'
);
}
} else {
$this->config = \ConfigFactory::getDefaultInstance()->makeConfig( 'main' );
}
if ( isset( $options['logger'] ) ) {
if ( !$options['logger'] instanceof LoggerInterface ) {
throw new \InvalidArgumentException(
'$options[\'logger\'] must be an instance of LoggerInterface'
);
}
$this->setLogger( $options['logger'] );
} else {
$this->setLogger( \MediaWiki\Logger\LoggerFactory::getInstance( 'session' ) );
}
if ( isset( $options['store'] ) ) {
if ( !$options['store'] instanceof BagOStuff ) {
throw new \InvalidArgumentException(
'$options[\'store\'] must be an instance of BagOStuff'
);
}
$this->store = $options['store'];
} else {
$this->store = \ObjectCache::getInstance( $this->config->get( 'SessionCacheType' ) );
$this->store->setLogger( $this->logger );
}
register_shutdown_function( array( $this, 'shutdown' ) );
}
public function setLogger( LoggerInterface $logger ) {
$this->logger = $logger;
}
public function getPersistedSessionId( WebRequest $request ) {
$info = $this->getSessionInfoForRequest( $request );
if ( $info && $info->wasPersisted() ) {
return $info->getId();
} else {
return null;
}
}
public function getSessionForRequest( WebRequest $request ) {
$info = $this->getSessionInfoForRequest( $request );
if ( !$info ) {
$session = $this->getEmptySession( $request );
} else {
$session = $this->getSessionFromInfo( $info, $request );
}
return $session;
}
public function getSessionById( $id, $noEmpty = false, WebRequest $request = null ) {
if ( !self::validateSessionId( $id ) ) {
throw new \InvalidArgumentException( 'Invalid session ID' );
}
if ( !$request ) {
$request = new FauxRequest;
}
$session = null;
// Test this here to provide a better log message for the common case
// of "no such ID"
$key = wfMemcKey( 'MWSession', $id );
if ( is_array( $this->store->get( $key ) ) ) {
$info = new SessionInfo( SessionInfo::MIN_PRIORITY, array( 'id' => $id, 'idIsSafe' => true ) );
if ( $this->loadSessionInfoFromStore( $info, $request ) ) {
$session = $this->getSessionFromInfo( $info, $request );
}
}
if ( !$noEmpty && $session === null ) {
$ex = null;
try {
$session = $this->getEmptySessionInternal( $request, $id );
} catch ( \Exception $ex ) {
$this->logger->error( __METHOD__ . ': failed to create empty session: ' .
$ex->getMessage() );
$session = null;
}
if ( $session === null ) {
throw new \UnexpectedValueException(
'Can neither load the session nor create an empty session', 0, $ex
);
}
}
return $session;
}
public function getEmptySession( WebRequest $request = null ) {
return $this->getEmptySessionInternal( $request );
}
/**
* @see SessionManagerInterface::getEmptySession
* @param WebRequest|null $request
* @param string|null $id ID to force on the new session
* @return Session
*/
private function getEmptySessionInternal( WebRequest $request = null, $id = null ) {
if ( $id !== null ) {
if ( !self::validateSessionId( $id ) ) {
throw new \InvalidArgumentException( 'Invalid session ID' );
}
$key = wfMemcKey( 'MWSession', $id );
if ( is_array( $this->store->get( $key ) ) ) {
throw new \InvalidArgumentException( 'Session ID already exists' );
}
}
if ( !$request ) {
$request = new FauxRequest;
}
$infos = array();
foreach ( $this->getProviders() as $provider ) {
$info = $provider->newSessionInfo( $id );
if ( !$info ) {
continue;
}
if ( $info->getProvider() !== $provider ) {
throw new \UnexpectedValueException(
"$provider returned an empty session info for a different provider: $info"
);
}
if ( $id !== null && $info->getId() !== $id ) {
throw new \UnexpectedValueException(
"$provider returned empty session info with a wrong id: " .
$info->getId() . ' != ' . $id
);
}
if ( !$info->isIdSafe() ) {
throw new \UnexpectedValueException(
"$provider returned empty session info with id flagged unsafe"
);
}
$compare = $infos ? SessionInfo::compare( $infos[0], $info ) : -1;
if ( $compare > 0 ) {
continue;
}
if ( $compare === 0 ) {
$infos[] = $info;
} else {
$infos = array( $info );
}
}
// Make sure there's exactly one
if ( count( $infos ) > 1 ) {
throw new \UnexpectedValueException(
'Multiple empty sessions tied for top priority: ' . join( ', ', $infos )
);
} elseif ( count( $infos ) < 1 ) {
throw new \UnexpectedValueException( 'No provider could provide an empty session!' );
}
return $this->getSessionFromInfo( $infos[0], $request );
}
public function getVaryHeaders() {
if ( $this->varyHeaders === null ) {
$headers = array();
foreach ( $this->getProviders() as $provider ) {
foreach ( $provider->getVaryHeaders() as $header => $options ) {
if ( !isset( $headers[$header] ) ) {
$headers[$header] = array();
}
if ( is_array( $options ) ) {
$headers[$header] = array_unique( array_merge( $headers[$header], $options ) );
}
}
}
$this->varyHeaders = $headers;
}
return $this->varyHeaders;
}
public function getVaryCookies() {
if ( $this->varyCookies === null ) {
$cookies = array();
foreach ( $this->getProviders() as $provider ) {
$cookies = array_merge( $cookies, $provider->getVaryCookies() );
}
$this->varyCookies = array_values( array_unique( $cookies ) );
}
return $this->varyCookies;
}
/**
* Validate a session ID
* @param string $id
* @return bool
*/
public static function validateSessionId( $id ) {
return is_string( $id ) && preg_match( '/^[a-zA-Z0-9_-]{32,}$/', $id );
}
/**
* @name Internal methods
* @{
*/
/**
* Auto-create the given user, if necessary
* @private Don't call this yourself. Let Setup.php do it for you at the right time.
* @note This more properly belongs in AuthManager, but we need it now.
* When AuthManager comes, this will be deprecated and will pass-through
* to the corresponding AuthManager method.
* @param User $user User to auto-create
* @return bool Success
*/
public static function autoCreateUser( User $user ) {
global $wgAuth;
$logger = self::singleton()->logger;
// Much of this code is based on that in CentralAuth
// Try the local user from the slave DB
$localId = User::idFromName( $user->getName() );
// Fetch the user ID from the master, so that we don't try to create the user
// when they already exist, due to replication lag
// @codeCoverageIgnoreStart
if ( !$localId && wfGetLB()->getReaderIndex() != 0 ) {
$localId = User::idFromName( $user->getName(), User::READ_LATEST );
}
// @codeCoverageIgnoreEnd
if ( $localId ) {
// User exists after all.
$user->setId( $localId );
$user->loadFromId();
return false;
}
// Denied by AuthPlugin? But ignore AuthPlugin itself.
if ( get_class( $wgAuth ) !== 'AuthPlugin' && !$wgAuth->autoCreate() ) {
$logger->debug( __METHOD__ . ': denied by AuthPlugin' );
$user->setId( 0 );
$user->loadFromId();
return false;
}
// Wiki is read-only?
if ( wfReadOnly() ) {
$logger->debug( __METHOD__ . ': denied by wfReadOnly()' );
$user->setId( 0 );
$user->loadFromId();
return false;
}
$userName = $user->getName();
// Check the session, if we tried to create this user already there's
// no point in retrying.
$session = self::getGlobalSession();
$reason = $session->get( 'MWSession::AutoCreateBlacklist' );
if ( $reason ) {
$logger->debug( __METHOD__ . ": blacklisted in session ($reason)" );
$user->setId( 0 );
$user->loadFromId();
return false;
}
// Is the IP user able to create accounts?
$anon = new User;
if ( !$anon->isAllowedAny( 'createaccount', 'autocreateaccount' )
|| $anon->isBlockedFromCreateAccount()
) {
// Blacklist the user to avoid repeated DB queries subsequently
$logger->debug( __METHOD__ . ': user is blocked from this wiki, blacklisting' );
$session->set( 'MWSession::AutoCreateBlacklist', 'blocked', 600 );
$session->persist();
$user->setId( 0 );
$user->loadFromId();
return false;
}
// Check for validity of username
if ( !User::isCreatableName( $userName ) ) {
$logger->debug( __METHOD__ . ': Invalid username, blacklisting' );
$session->set( 'MWSession::AutoCreateBlacklist', 'invalid username', 600 );
$session->persist();
$user->setId( 0 );
$user->loadFromId();
return false;
}
// Give other extensions a chance to stop auto creation.
$user->loadDefaults( $userName );
$abortMessage = '';
if ( !\Hooks::run( 'AbortAutoAccount', array( $user, &$abortMessage ) ) ) {
// In this case we have no way to return the message to the user,
// but we can log it.
$logger->debug( __METHOD__ . ": denied by hook: $abortMessage" );
$session->set( 'MWSession::AutoCreateBlacklist', "hook aborted: $abortMessage", 600 );
$session->persist();
$user->setId( 0 );
$user->loadFromId();
return false;
}
// Make sure the name has not been changed
if ( $user->getName() !== $userName ) {
$user->setId( 0 );
$user->loadFromId();
throw new \UnexpectedValueException(
'AbortAutoAccount hook tried to change the user name'
);
}
// Ignore warnings about master connections/writes...hard to avoid here
\Profiler::instance()->getTransactionProfiler()->resetExpectations();
$cache = \ObjectCache::getLocalClusterInstance();
$backoffKey = wfMemcKey( 'MWSession', 'autocreate-failed', md5( $userName ) );
if ( $cache->get( $backoffKey ) ) {
$logger->debug( __METHOD__ . ': denied by prior creation attempt failures' );
$user->setId( 0 );
$user->loadFromId();
return false;
}
// Checks passed, create the user...
$from = isset( $_SERVER['REQUEST_URI'] ) ? $_SERVER['REQUEST_URI'] : 'CLI';
$logger->info( __METHOD__ . ": creating new user ($userName) - from: $from" );
try {
// Insert the user into the local DB master
$status = $user->addToDatabase();
if ( !$status->isOK() ) {
// @codeCoverageIgnoreStart
$logger->error( __METHOD__ . ': failed with message ' . $status->getWikiText() );
$user->setId( 0 );
$user->loadFromId();
return false;
// @codeCoverageIgnoreEnd
}
} catch ( \Exception $ex ) {
// @codeCoverageIgnoreStart
$logger->error( __METHOD__ . ': failed with exception ' . $ex->getMessage() );
// Do not keep throwing errors for a while
$cache->set( $backoffKey, 1, 600 );
// Bubble up error; which should normally trigger DB rollbacks
throw $ex;
// @codeCoverageIgnoreEnd
}
# Notify hooks (e.g. Newuserlog)
\Hooks::run( 'AuthPluginAutoCreate', array( $user ) );
\Hooks::run( 'LocalUserCreated', array( $user, true ) );
# Update user count
\DeferredUpdates::addUpdate( new \SiteStatsUpdate( 0, 0, 0, 0, 1 ) );
# Watch user's userpage and talk page
$user->addWatch( $user->getUserPage(), \WatchedItem::IGNORE_USER_RIGHTS );
return true;
}
/**
* Prevent future sessions for the user
*
* The intention is that the named account will never again be usable for
* normal login (i.e. there is no way to undo the prevention of access).
*
* @private For use from \\User::newSystemUser only
* @param string $username
*/
public function preventSessionsForUser( $username ) {
$this->preventUsers[$username] = true;
// Reset the user's token to kill existing sessions
$user = User::newFromName( $username );
if ( $user && $user->getToken() ) {
$user->setToken( true );
$user->saveSettings();
}
// Instruct the session providers to kill any other sessions too.
foreach ( $this->getProviders() as $provider ) {
$provider->preventSessionsForUser( $username );
}
}
/**
* Test if a user is prevented
* @private For use from SessionBackend only
* @param string $username
* @return bool
*/
public function isUserSessionPrevented( $username ) {
return !empty( $this->preventUsers[$username] );
}
/**
* Get the available SessionProviders
* @return SessionProvider[]
*/
protected function getProviders() {
if ( $this->sessionProviders === null ) {
$this->sessionProviders = array();
foreach ( $this->config->get( 'SessionProviders' ) as $spec ) {
$provider = \ObjectFactory::getObjectFromSpec( $spec );
$provider->setLogger( $this->logger );
$provider->setConfig( $this->config );
$provider->setManager( $this );
if ( isset( $this->sessionProviders[(string)$provider] ) ) {
throw new \UnexpectedValueException( "Duplicate provider name \"$provider\"" );
}
$this->sessionProviders[(string)$provider] = $provider;
}
}
return $this->sessionProviders;
}
/**
* Get a session provider by name
*
* Generally, this will only be used by internal implementation of some
* special session-providing mechanism. General purpose code, if it needs
* to access a SessionProvider at all, will use Session::getProvider().
*
* @param string $name
* @return SessionProvider|null
*/
public function getProvider( $name ) {
$providers = $this->getProviders();
return isset( $providers[$name] ) ? $providers[$name] : null;
}
/**
* Save all active sessions on shutdown
* @private For internal use with register_shutdown_function()
*/
public function shutdown() {
if ( $this->allSessionBackends ) {
$this->logger->debug( 'Saving all sessions on shutdown' );
if ( session_id() !== '' ) {
// @codeCoverageIgnoreStart
session_write_close();
}
// @codeCoverageIgnoreEnd
foreach ( $this->allSessionBackends as $backend ) {
$backend->save( true );
}
}
}
/**
* Fetch the SessionInfo(s) for a request
* @param WebRequest $request
* @return SessionInfo|null
*/
private function getSessionInfoForRequest( WebRequest $request ) {
// Call all providers to fetch "the" session
$infos = array();
foreach ( $this->getProviders() as $provider ) {
$info = $provider->provideSessionInfo( $request );
if ( !$info ) {
continue;
}
if ( $info->getProvider() !== $provider ) {
throw new \UnexpectedValueException(
"$provider returned session info for a different provider: $info"
);
}
$infos[] = $info;
}
// Sort the SessionInfos. Then find the first one that can be
// successfully loaded, and then all the ones after it with the same
// priority.
usort( $infos, 'MediaWiki\\Session\\SessionInfo::compare' );
$retInfos = array();
while ( $infos ) {
$info = array_pop( $infos );
if ( $this->loadSessionInfoFromStore( $info, $request ) ) {
$retInfos[] = $info;
while ( $infos ) {
$info = array_pop( $infos );
if ( SessionInfo::compare( $retInfos[0], $info ) ) {
// We hit a lower priority, stop checking.
break;
}
if ( $this->loadSessionInfoFromStore( $info, $request ) ) {
// This is going to error out below, but we want to
// provide a complete list.
$retInfos[] = $info;
}
}
}
}
if ( count( $retInfos ) > 1 ) {
$ex = new \OverflowException(
'Multiple sessions for this request tied for top priority: ' . join( ', ', $retInfos )
);
$ex->sessionInfos = $retInfos;
throw $ex;
}
return $retInfos ? $retInfos[0] : null;
}
/**
* Load and verify the session info against the store
*
* @param SessionInfo &$info Will likely be replaced with an updated SessionInfo instance
* @param WebRequest $request
* @return bool Whether the session info matches the stored data (if any)
*/
private function loadSessionInfoFromStore( SessionInfo &$info, WebRequest $request ) {
$blob = $this->store->get( wfMemcKey( 'MWSession', $info->getId() ) );
$newParams = array();
if ( $blob !== false ) {
// Sanity check: blob must be an array, if it's saved at all
if ( !is_array( $blob ) ) {
$this->logger->warning( "Session $info: Bad data" );
return false;
}
// Sanity check: blob has data and metadata arrays
if ( !isset( $blob['data'] ) || !is_array( $blob['data'] ) ||
!isset( $blob['metadata'] ) || !is_array( $blob['metadata'] )
) {
$this->logger->warning( "Session $info: Bad data structure" );
return false;
}
$data = $blob['data'];
$metadata = $blob['metadata'];
// Sanity check: metadata must be an array and must contain certain
// keys, if it's saved at all
if ( !array_key_exists( 'userId', $metadata ) ||
!array_key_exists( 'userName', $metadata ) ||
!array_key_exists( 'userToken', $metadata ) ||
!array_key_exists( 'provider', $metadata )
) {
$this->logger->warning( "Session $info: Bad metadata" );
return false;
}
// First, load the provider from metadata, or validate it against the metadata.
$provider = $info->getProvider();
if ( $provider === null ) {
$newParams['provider'] = $provider = $this->getProvider( $metadata['provider'] );
if ( !$provider ) {
$this->logger->warning( "Session $info: Unknown provider, " . $metadata['provider'] );
return false;
}
} elseif ( $metadata['provider'] !== (string)$provider ) {
$this->logger->warning( "Session $info: Wrong provider, " .
$metadata['provider'] . ' !== ' . $provider );
return false;
}
// Load provider metadata from metadata, or validate it against the metadata
$providerMetadata = $info->getProviderMetadata();
if ( isset( $metadata['providerMetadata'] ) ) {
if ( $providerMetadata === null ) {
$newParams['metadata'] = $metadata['providerMetadata'];
} else {
try {
$newProviderMetadata = $provider->mergeMetadata(
$metadata['providerMetadata'], $providerMetadata
);
if ( $newProviderMetadata !== $providerMetadata ) {
$newParams['metadata'] = $newProviderMetadata;
}
} catch ( \UnexpectedValueException $ex ) {
$this->logger->warning( "Session $info: Metadata merge failed: " . $ex->getMessage() );
return false;
}
}
}
// Next, load the user from metadata, or validate it against the metadata.
$userInfo = $info->getUserInfo();
if ( !$userInfo ) {
// For loading, id is preferred to name.
try {
if ( $metadata['userId'] ) {
$userInfo = UserInfo::newFromId( $metadata['userId'] );
} elseif ( $metadata['userName'] !== null ) { // Shouldn't happen, but just in case
$userInfo = UserInfo::newFromName( $metadata['userName'] );
} else {
$userInfo = UserInfo::newAnonymous();
}
} catch ( \InvalidArgumentException $ex ) {
$this->logger->error( "Session $info: " . $ex->getMessage() );
return false;
}
$newParams['userInfo'] = $userInfo;
} else {
// User validation passes if user ID matches, or if there
// is no saved ID and the names match.
if ( $metadata['userId'] ) {
if ( $metadata['userId'] !== $userInfo->getId() ) {
$this->logger->warning( "Session $info: User ID mismatch, " .
$metadata['userId'] . ' !== ' . $userInfo->getId() );
return false;
}
// If the user was renamed, probably best to fail here.
if ( $metadata['userName'] !== null &&
$userInfo->getName() !== $metadata['userName']
) {
$this->logger->warning( "Session $info: User ID matched but name didn't (rename?), " .
$metadata['userName'] . ' !== ' . $userInfo->getName() );
return false;
}
} elseif ( $metadata['userName'] !== null ) { // Shouldn't happen, but just in case
if ( $metadata['userName'] !== $userInfo->getName() ) {
$this->logger->warning( "Session $info: User name mismatch, " .
$metadata['userName'] . ' !== ' . $userInfo->getName() );
return false;
}
} elseif ( !$userInfo->isAnon() ) {
// Metadata specifies an anonymous user, but the passed-in
// user isn't anonymous.
$this->logger->warning(
"Session $info: Metadata has an anonymous user, " .
'but a non-anon user was provided'
);
return false;
}
}
// And if we have a token in the metadata, it must match the loaded/provided user.
if ( $metadata['userToken'] !== null &&
$userInfo->getToken() !== $metadata['userToken']
) {
$this->logger->warning( "Session $info: User token mismatch" );
return false;
}
if ( !$userInfo->isVerified() ) {
$newParams['userInfo'] = $userInfo->verified();
}
if ( !empty( $metadata['remember'] ) && !$info->wasRemembered() ) {
$newParams['remembered'] = true;
}
if ( !empty( $metadata['forceHTTPS'] ) && !$info->forceHTTPS() ) {
$newParams['forceHTTPS'] = true;
}
if ( !$info->isIdSafe() ) {
$newParams['idIsSafe'] = true;
}
} else {
// No metadata, so we can't load the provider if one wasn't given.
if ( $info->getProvider() === null ) {
$this->logger->warning( "Session $info: Null provider and no metadata" );
return false;
}
// If no user was provided and no metadata, it must be anon.
if ( !$info->getUserInfo() ) {
if ( $info->getProvider()->canChangeUser() ) {
$newParams['userInfo'] = UserInfo::newAnonymous();
} else {
$this->logger->info(
"Session $info: No user provided and provider cannot set user"
);
return false;
}
} elseif ( !$info->getUserInfo()->isVerified() ) {
$this->logger->warning(
"Session $info: Unverified user provided and no metadata to auth it"
);
return false;
}
$data = false;
$metadata = false;
if ( !$info->getProvider()->persistsSessionId() && !$info->isIdSafe() ) {
// The ID doesn't come from the user, so it should be safe
// (and if not, nothing we can do about it anyway)
$newParams['idIsSafe'] = true;
}
}
// Construct the replacement SessionInfo, if necessary
if ( $newParams ) {
$newParams['copyFrom'] = $info;
$info = new SessionInfo( $info->getPriority(), $newParams );
}
// Allow the provider to check the loaded SessionInfo
$providerMetadata = $info->getProviderMetadata();
if ( !$info->getProvider()->refreshSessionInfo( $info, $request, $providerMetadata ) ) {
return false;
}
if ( $providerMetadata !== $info->getProviderMetadata() ) {
$info = new SessionInfo( $info->getPriority(), array(
'metadata' => $providerMetadata,
'copyFrom' => $info,
) );
}
// Give hooks a chance to abort. Combined with the SessionMetadata
// hook, this can allow for tying a session to an IP address or the
// like.
$reason = 'Hook aborted';
if ( !\Hooks::run(
'SessionCheckInfo',
array( &$reason, $info, $request, $metadata, $data )
) ) {
$this->logger->warning( "Session $info: $reason" );
return false;
}
return true;
}
/**
* Create a session corresponding to the passed SessionInfo
* @private For use by a SessionProvider that needs to specially create its
* own session.
* @param SessionInfo $info
* @param WebRequest $request
* @return Session
*/
public function getSessionFromInfo( SessionInfo $info, WebRequest $request ) {
$id = $info->getId();
if ( !isset( $this->allSessionBackends[$id] ) ) {
if ( !isset( $this->allSessionIds[$id] ) ) {
$this->allSessionIds[$id] = new SessionId( $id );
}
$backend = new SessionBackend(
$this->allSessionIds[$id],
$info,
$this->store,
$this->logger,
$this->config->get( 'ObjectCacheSessionExpiry' )
);
$this->allSessionBackends[$id] = $backend;
$delay = $backend->delaySave();
} else {
$backend = $this->allSessionBackends[$id];
$delay = $backend->delaySave();
if ( $info->wasPersisted() ) {
$backend->persist();
}
if ( $info->wasRemembered() ) {
$backend->setRememberUser( true );
}
}
$request->setSessionId( $backend->getSessionId() );
$session = $backend->getSession( $request );
if ( !$info->isIdSafe() ) {
$session->resetId();
}
\ScopedCallback::consume( $delay );
return $session;
}
/**
* Deregister a SessionBackend
* @private For use from \\MediaWiki\\Session\\SessionBackend only
* @param SessionBackend $backend
*/
public function deregisterSessionBackend( SessionBackend $backend ) {
$id = $backend->getId();
if ( !isset( $this->allSessionBackends[$id] ) || !isset( $this->allSessionIds[$id] ) ||
$this->allSessionBackends[$id] !== $backend ||
$this->allSessionIds[$id] !== $backend->getSessionId()
) {
throw new \InvalidArgumentException( 'Backend was not registered with this SessionManager' );
}
unset( $this->allSessionBackends[$id] );
// Explicitly do not unset $this->allSessionIds[$id]
}
/**
* Change a SessionBackend's ID
* @private For use from \\MediaWiki\\Session\\SessionBackend only
* @param SessionBackend $backend
*/
public function changeBackendId( SessionBackend $backend ) {
$sessionId = $backend->getSessionId();
$oldId = (string)$sessionId;
if ( !isset( $this->allSessionBackends[$oldId] ) || !isset( $this->allSessionIds[$oldId] ) ||
$this->allSessionBackends[$oldId] !== $backend ||
$this->allSessionIds[$oldId] !== $sessionId
) {
throw new \InvalidArgumentException( 'Backend was not registered with this SessionManager' );
}
$newId = $this->generateSessionId();
unset( $this->allSessionBackends[$oldId], $this->allSessionIds[$oldId] );
$sessionId->setId( $newId );
$this->allSessionBackends[$newId] = $backend;
$this->allSessionIds[$newId] = $sessionId;
}
/**
* Generate a new random session ID
* @return string
*/
public function generateSessionId() {
do {
$id = wfBaseConvert( \MWCryptRand::generateHex( 40 ), 16, 32, 32 );
$key = wfMemcKey( 'MWSession', $id );
} while ( isset( $this->allSessionIds[$id] ) || is_array( $this->store->get( $key ) ) );
return $id;
}
/**
* Call setters on a PHPSessionHandler
* @private Use PhpSessionHandler::install()
* @param PHPSessionHandler $handler
*/
public function setupPHPSessionHandler( PHPSessionHandler $handler ) {
$handler->setManager( $this, $this->store, $this->logger );
}
/**
* Reset the internal caching for unit testing
*/
public static function resetCache() {
if ( !defined( 'MW_PHPUNIT_TEST' ) ) {
// @codeCoverageIgnoreStart
throw new MWException( __METHOD__ . ' may only be called from unit tests!' );
// @codeCoverageIgnoreEnd
}
self::$globalSession = null;
self::$globalSessionRequest = null;
}
/**@}*/
}

View file

@ -0,0 +1,109 @@
<?php
/**
* MediaWiki\Session entry point interface
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along
* with this program; if not, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
* http://www.gnu.org/copyleft/gpl.html
*
* @file
* @ingroup Session
*/
namespace MediaWiki\Session;
use Psr\Log\LoggerAwareInterface;
use WebRequest;
/**
* This exists to make IDEs happy, so they don't see the
* internal-but-required-to-be-public methods on SessionManager.
*
* @ingroup Session
* @since 1.27
*/
interface SessionManagerInterface extends LoggerAwareInterface {
/**
* Fetch the persisted session ID in a request.
*
* Note this is not the same thing as whether the session associated with
* the request is currently persistent, as the session might have been
* first made persistent during this request.
*
* @param WebRequest $request
* @return string|null
* @throws \\OverflowException if there are multiple sessions tied for top
* priority in the request. Exception has a property "sessionInfos"
* holding the SessionInfo objects for the sessions involved.
*/
public function getPersistedSessionId( WebRequest $request );
/**
* Fetch the session for a request
*
* @note You probably want to use $request->getSession() instead. It's more
* efficient and doesn't break FauxRequests or sessions that were changed
* by $this->getSessionById() or $this->getEmptySession().
* @param WebRequest $request Any existing associated session will be reset
* to the session corresponding to the data in the request itself.
* @return Session
* @throws \\OverflowException if there are multiple sessions tied for top
* priority in the request. Exception has a property "sessionInfos"
* holding the SessionInfo objects for the sessions involved.
*/
public function getSessionForRequest( WebRequest $request );
/**
* Fetch a session by ID
* @param string $id
* @param bool $noEmpty Don't return an empty session
* @param WebRequest|null $request Corresponding request. Any existing
* session associated with this WebRequest object will be overwritten.
* @return Session|null
*/
public function getSessionById( $id, $noEmpty = false, WebRequest $request = null );
/**
* Fetch a new, empty session
*
* The first provider configured that is able to provide an empty session
* will be used.
*
* @param WebRequest|null $request Corresponding request. Any existing
* session associated with this WebRequest object will be overwritten.
* @return Session
*/
public function getEmptySession( WebRequest $request = null );
/**
* Return the HTTP headers that need varying on.
*
* The return value is such that someone could theoretically do this:
* @code
* foreach ( $provider->getVaryHeaders() as $header => $options ) {
* $outputPage->addVaryHeader( $header, $options );
* }
* @endcode
*
* @return array
*/
public function getVaryHeaders();
/**
* Return the list of cookies that need varying on.
* @return string[]
*/
public function getVaryCookies();
}

View file

@ -0,0 +1,473 @@
<?php
/**
* MediaWiki session provider base class
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along
* with this program; if not, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
* http://www.gnu.org/copyleft/gpl.html
*
* @file
* @ingroup Session
*/
namespace MediaWiki\Session;
use Psr\Log\LoggerAwareInterface;
use Psr\Log\LoggerInterface;
use Config;
use Language;
use WebRequest;
/**
* A SessionProvider provides SessionInfo and support for Session
*
* A SessionProvider is responsible for taking a WebRequest and determining
* the authenticated session that it's a part of. It does this by returning an
* SessionInfo object with basic information about the session it thinks is
* associated with the request, namely the session ID and possibly the
* authenticated user the session belongs to.
*
* The SessionProvider also provides for updating the WebResponse with
* information necessary to provide the client with data that the client will
* send with later requests, and for populating the Vary and Key headers with
* the data necessary to correctly vary the cache on these client requests.
*
* An important part of the latter is indicating whether it even *can* tell the
* client to include such data in future requests, via the persistsSessionId()
* and canChangeUser() methods. The cases are (in order of decreasing
* commonness):
* - Cannot persist ID, no changing User: The request identifies and
* authenticates a particular local user, and the client cannot be
* instructed to include an arbitrary session ID with future requests. For
* example, OAuth or SSL certificate auth.
* - Can persist ID and can change User: The client can be instructed to
* return at least one piece of arbitrary data, that being the session ID.
* The user identity might also be given to the client, otherwise it's saved
* in the session data. For example, cookie-based sessions.
* - Can persist ID but no changing User: The request uniquely identifies and
* authenticates a local user, and the client can be instructed to return an
* arbitrary session ID with future requests. For example, HTTP Digest
* authentication might somehow use the 'opaque' field as a session ID
* (although getting MediaWiki to return 401 responses without breaking
* other stuff might be a challenge).
* - Cannot persist ID but can change User: I can't think of a way this
* would make sense.
*
* Note that many methods that are technically "cannot persist ID" could be
* turned into "can persist ID but not changing User" using a session cookie,
* as implemented by ImmutableSessionProviderWithCookie. If doing so, different
* session cookie names should be used for different providers to avoid
* collisions.
*
* @ingroup Session
* @since 1.27
*/
abstract class SessionProvider implements SessionProviderInterface, LoggerAwareInterface {
/** @var LoggerInterface */
protected $logger;
/** @var Config */
protected $config;
/** @var SessionManager */
protected $manager;
/** @var int Session priority. Used for the default newSessionInfo(), but
* could be used by subclasses too.
*/
protected $priority;
/**
* @note To fully initialize a SessionProvider, the setLogger(),
* setConfig(), and setManager() methods must be called (and should be
* called in that order). Failure to do so is liable to cause things to
* fail unexpectedly.
*/
public function __construct() {
$this->priority = SessionInfo::MIN_PRIORITY + 10;
}
public function setLogger( LoggerInterface $logger ) {
$this->logger = $logger;
}
/**
* Set configuration
* @param Config $config
*/
public function setConfig( Config $config ) {
$this->config = $config;
}
/**
* Set the session manager
* @param SessionManager $manager
*/
public function setManager( SessionManager $manager ) {
$this->manager = $manager;
}
/**
* Get the session manager
* @return SessionManager
*/
public function getManager() {
return $this->manager;
}
/**
* Provide session info for a request
*
* If no session exists for the request, return null. Otherwise return an
* SessionInfo object identifying the session.
*
* If multiple SessionProviders provide sessions, the one with highest
* priority wins. In case of a tie, an exception is thrown.
* SessionProviders are encouraged to make priorities user-configurable
* unless only max-priority makes sense.
*
* @warning This will be called early in the MediaWiki setup process,
* before $wgUser, $wgLang, $wgOut, $wgParser, $wgTitle, and corresponding
* pieces of the main RequestContext are set up! If you try to use these,
* things *will* break.
* @note The SessionProvider must not attempt to auto-create users.
* MediaWiki will do this later (when it's safe) if the chosen session has
* a user with a valid name but no ID.
* @protected For use by \\MediaWiki\\Session\\SessionManager only
* @param WebRequest $request
* @return SessionInfo|null
*/
abstract public function provideSessionInfo( WebRequest $request );
/**
* Provide session info for a new, empty session
*
* Return null if such a session cannot be created. This base
* implementation assumes that it only makes sense if a session ID can be
* persisted and changing users is allowed.
*
* @protected For use by \\MediaWiki\\Session\\SessionManager only
* @param string|null $id ID to force for the new session
* @return SessionInfo|null
* If non-null, must return true for $info->isIdSafe(); pass true for
* $data['idIsSafe'] to ensure this.
*/
public function newSessionInfo( $id = null ) {
if ( $this->canChangeUser() && $this->persistsSessionId() ) {
return new SessionInfo( $this->priority, array(
'id' => $id,
'provider' => $this,
'persisted' => false,
'idIsSafe' => true,
) );
}
return null;
}
/**
* Merge saved session provider metadata
*
* The default implementation checks that anything in both arrays is
* identical, then returns $providedMetadata.
*
* @protected For use by \\MediaWiki\\Session\\SessionManager only
* @param array $savedMetadata Saved provider metadata
* @param array $providedMetadata Provided provider metadata
* @return array Resulting metadata
* @throws \UnexpectedValueException If the metadata cannot be merged
*/
public function mergeMetadata( array $savedMetadata, array $providedMetadata ) {
foreach ( $providedMetadata as $k => $v ) {
if ( array_key_exists( $k, $savedMetadata ) && $savedMetadata[$k] !== $v ) {
throw new \UnexpectedValueException( "Key \"$k\" changed" );
}
}
return $providedMetadata;
}
/**
* Validate a loaded SessionInfo and refresh provider metadata
*
* This is similar in purpose to the 'SessionCheckInfo' hook, and also
* allows for updating the provider metadata. On failure, the provider is
* expected to write an appropriate message to its logger.
*
* @protected For use by \\MediaWiki\\Session\\SessionManager only
* @param SessionInfo $info
* @param WebRequest $request
* @param array|null &$metadata Provider metadata, may be altered.
* @return bool Return false to reject the SessionInfo after all.
*/
public function refreshSessionInfo( SessionInfo $info, WebRequest $request, &$metadata ) {
return true;
}
/**
* Indicate whether self::persistSession() can save arbitrary session IDs
*
* If false, any session passed to self::persistSession() will have an ID
* that was originally provided by self::provideSessionInfo().
*
* If true, the provider may be passed sessions with arbitrary session IDs,
* and will be expected to manipulate the request in such a way that future
* requests will cause self::provideSessionInfo() to provide a SessionInfo
* with that ID.
*
* For example, a session provider for OAuth would function by matching the
* OAuth headers to a particular user, and then would use self::hashToSessionId()
* to turn the user and OAuth client ID (and maybe also the user token and
* client secret) into a session ID, and therefore can't easily assign that
* user+client a different ID. Similarly, a session provider for SSL client
* certificates would function by matching the certificate to a particular
* user, and then would use self::hashToSessionId() to turn the user and
* certificate fingerprint into a session ID, and therefore can't easily
* assign a different ID either. On the other hand, a provider that saves
* the session ID into a cookie can easily just set the cookie to a
* different value.
*
* @protected For use by \\MediaWiki\\Session\\SessionBackend only
* @return bool
*/
abstract public function persistsSessionId();
/**
* Indicate whether the user associated with the request can be changed
*
* If false, any session passed to self::persistSession() will have a user
* that was originally provided by self::provideSessionInfo(). Further,
* self::provideSessionInfo() may only provide sessions that have a user
* already set.
*
* If true, the provider may be passed sessions with arbitrary users, and
* will be expected to manipulate the request in such a way that future
* requests will cause self::provideSessionInfo() to provide a SessionInfo
* with that ID. This can be as simple as not passing any 'userInfo' into
* SessionInfo's constructor, in which case SessionInfo will load the user
* from the saved session's metadata.
*
* For example, a session provider for OAuth or SSL client certificates
* would function by matching the OAuth headers or certificate to a
* particular user, and thus would return false here since it can't
* arbitrarily assign those OAuth credentials or that certificate to a
* different user. A session provider that shoves information into cookies,
* on the other hand, could easily do so.
*
* @protected For use by \\MediaWiki\\Session\\SessionBackend only
* @return bool
*/
abstract public function canChangeUser();
/**
* Notification that the session ID was reset
*
* No need to persist here, persistSession() will be called if appropriate.
*
* @protected For use by \\MediaWiki\\Session\\SessionBackend only
* @param SessionBackend $session Session to persist
* @param string $oldId Old session ID
* @codeCoverageIgnore
*/
public function sessionIdWasReset( SessionBackend $session, $oldId ) {
}
/**
* Persist a session into a request/response
*
* For example, you might set cookies for the session's ID, user ID, user
* name, and user token on the passed request.
*
* To correctly persist a user independently of the session ID, the
* provider should persist both the user ID (or name, but preferably the
* ID) and the user token. When reading the data from the request, it
* should construct a User object from the ID/name and then verify that the
* User object's token matches the token included in the request. Should
* the tokens not match, an anonymous user *must* be passed to
* SessionInfo::__construct().
*
* When persisting a user independently of the session ID,
* $session->shouldRememberUser() should be checked first. If this returns
* false, the user token *must not* be saved to cookies. The user name
* and/or ID may be persisted, and should be used to construct an
* unverified UserInfo to pass to SessionInfo::__construct().
*
* A backend that cannot persist sesison ID or user info should implement
* this as a no-op.
*
* @protected For use by \\MediaWiki\\Session\\SessionBackend only
* @param SessionBackend $session Session to persist
* @param WebRequest $request Request into which to persist the session
*/
abstract public function persistSession( SessionBackend $session, WebRequest $request );
/**
* Remove any persisted session from a request/response
*
* For example, blank and expire any cookies set by self::persistSession().
*
* A backend that cannot persist sesison ID or user info should implement
* this as a no-op.
*
* @protected For use by \\MediaWiki\\Session\\SessionManager only
* @param WebRequest $request Request from which to remove any session data
*/
abstract public function unpersistSession( WebRequest $request );
/**
* Prevent future sessions for the user
*
* If the provider is capable of returning a SessionInfo with a verified
* UserInfo for the named user in some manner other than by validating
* against $user->getToken(), steps must be taken to prevent that from
* occurring in the future. This might add the username to a blacklist, or
* it might just delete whatever authentication credentials would allow
* such a session in the first place (e.g. remove all OAuth grants or
* delete record of the SSL client certificate).
*
* The intention is that the named account will never again be usable for
* normal login (i.e. there is no way to undo the prevention of access).
*
* Note that the passed user name might not exist locally (i.e.
* User::idFromName( $username ) === 0); the name should still be
* prevented, if applicable.
*
* @protected For use by \\MediaWiki\\Session\\SessionManager only
* @param string $username
*/
public function preventSessionsForUser( $username ) {
if ( !$this->canChangeUser() ) {
throw new \BadMethodCallException(
__METHOD__ . ' must be implmented when canChangeUser() is false'
);
}
}
/**
* Return the HTTP headers that need varying on.
*
* The return value is such that someone could theoretically do this:
* @code
* foreach ( $provider->getVaryHeaders() as $header => $options ) {
* $outputPage->addVaryHeader( $header, $options );
* }
* @endcode
*
* @protected For use by \\MediaWiki\\Session\\SessionManager only
* @return array
*/
public function getVaryHeaders() {
return array();
}
/**
* Return the list of cookies that need varying on.
* @protected For use by \\MediaWiki\\Session\\SessionManager only
* @return string[]
*/
public function getVaryCookies() {
return array();
}
/**
* Get a suggested username for the login form
* @protected For use by \\MediaWiki\\Session\\SessionBackend only
* @param WebRequest $request
* @return string|null
*/
public function suggestLoginUsername( WebRequest $request ) {
return null;
}
/**
* @note Only override this if it makes sense to instantiate multiple
* instances of the provider. Value returned must be unique across
* configured providers. If you override this, you'll likely need to
* override self::describeMessage() as well.
* @return string
*/
public function __toString() {
return get_class( $this );
}
/**
* Return a Message identifying this session type
*
* This default implementation takes the class name, lowercases it,
* replaces backslashes with dashes, and prefixes 'sessionprovider-' to
* determine the message key. For example, MediaWiki\\Session\\CookieSessionProvider
* produces 'sessionprovider-mediawiki-session-cookiesessionprovider'.
*
* @note If self::__toString() is overridden, this will likely need to be
* overridden as well.
* @warning This will be called early during MediaWiki startup. Do not
* use $wgUser, $wgLang, $wgOut, $wgParser, or their equivalents via
* RequestContext from this method!
* @return Message
*/
protected function describeMessage() {
return wfMessage(
'sessionprovider-' . str_replace( '\\', '-', strtolower( get_class( $this ) ) )
);
}
public function describe( Language $lang ) {
$msg = $this->describeMessage();
$msg->inLanguage( $lang );
if ( $msg->isDisabled() ) {
$msg = wfMessage( 'sessionprovider-generic', (string)$this )->inLanguage( $lang );
}
return $msg->plain();
}
public function whyNoSession() {
return null;
}
/**
* Hash data as a session ID
*
* Generally this will only be used when self::persistsSessionId() is false and
* the provider has to base the session ID on the verified user's identity
* or other static data.
*
* @param string $data
* @param string|null $key Defaults to $this->config->get( 'SecretKey' )
* @return string
*/
final protected function hashToSessionId( $data, $key = null ) {
if ( !is_string( $data ) ) {
throw new \InvalidArgumentException(
'$data must be a string, ' . gettype( $data ) . ' was passed'
);
}
if ( $key !== null && !is_string( $key ) ) {
throw new \InvalidArgumentException(
'$key must be a string or null, ' . gettype( $key ) . ' was passed'
);
}
$hash = \MWCryptHash::hmac( "$this\n$data", $key ?: $this->config->get( 'SecretKey' ), false );
if ( strlen( $hash ) < 32 ) {
// Should never happen, even md5 is 128 bits
// @codeCoverageIgnoreStart
throw new \UnexpectedValueException( 'Hash fuction returned less than 128 bits' );
// @codeCoverageIgnoreEnd
}
if ( strlen( $hash ) >= 40 ) {
$hash = wfBaseConvert( $hash, 16, 32, 32 );
}
return substr( $hash, -32 );
}
}

View file

@ -0,0 +1,54 @@
<?php
/**
* MediaWiki\Session\Provider interface
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along
* with this program; if not, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
* http://www.gnu.org/copyleft/gpl.html
*
* @file
* @ingroup Session
*/
namespace MediaWiki\Session;
use Language;
/**
* This exists to make IDEs happy, so they don't see the
* internal-but-required-to-be-public methods on SessionProvider.
*
* @ingroup Session
* @since 1.27
*/
interface SessionProviderInterface {
/**
* Return an identifier for this session type
*
* @param Language $lang Language to use.
* @return string
*/
public function describe( Language $lang );
/**
* Return a Message for why sessions might not be being persisted.
*
* For example, "check whether you're blocking our cookies".
*
* @return Message|null
*/
public function whyNoSession();
}

View file

@ -0,0 +1,187 @@
<?php
/**
* MediaWiki session user info
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along
* with this program; if not, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
* http://www.gnu.org/copyleft/gpl.html
*
* @file
* @ingroup Session
*/
namespace MediaWiki\Session;
use User;
/**
* Object holding data about a session's user
*
* In general, this class exists for two purposes:
* - User doesn't distinguish between "anonymous user" and "non-anonymous user
* that doesn't exist locally", while we do need to.
* - We also need the "verified" property described below; tracking it via
* another data item to SessionInfo's constructor makes things much more
* confusing.
*
* A UserInfo may be "verified". This indicates that the creator knows that the
* request really comes from that user, whether that's by validating OAuth
* credentials, SSL client certificates, or by having both the user ID and
* token available from cookies.
*
* An "unverified" UserInfo should be used when it's not possible to
* authenticate the user, e.g. the user ID cookie is set but the user Token
* cookie isn't. If the Token is available but doesn't match, don't return a
* UserInfo at all.
*
* @ingroup Session
* @since 1.27
*/
final class UserInfo {
private $verified = false;
/** @var User|null */
private $user = null;
private function __construct( User $user = null, $verified ) {
if ( $user && $user->isAnon() && !User::isUsableName( $user->getName() ) ) {
$this->verified = true;
$this->user = null;
} else {
$this->verified = $verified;
$this->user = $user;
}
}
/**
* Create an instance for an anonymous (i.e. not logged in) user
*
* Logged-out users are always "verified".
*
* @return UserInfo
*/
public static function newAnonymous() {
return new self( null, true );
}
/**
* Create an instance for a logged-in user by ID
* @param int $id User ID
* @param bool $verified True if the user is verified
* @return UserInfo
*/
public static function newFromId( $id, $verified = false ) {
$user = User::newFromId( $id );
// Ensure the ID actually exists
$user->load();
if ( $user->isAnon() ) {
throw new \InvalidArgumentException( 'Invalid ID' );
}
return new self( $user, $verified );
}
/**
* Create an instance for a logged-in user by name
* @param string $name User name (need not exist locally)
* @param bool $verified True if the user is verified
* @return UserInfo
*/
public static function newFromName( $name, $verified = false ) {
$user = User::newFromName( $name, 'usable' );
if ( !$user ) {
throw new \InvalidArgumentException( 'Invalid user name' );
}
return new self( $user, $verified );
}
/**
* Create an instance from an existing User object
* @param User $user (need not exist locally)
* @param bool $verified True if the user is verified
* @return UserInfo
*/
public static function newFromUser( User $user, $verified = false ) {
return new self( $user, $verified );
}
/**
* Return whether this is an anonymous user
* @return bool
*/
public function isAnon() {
return $this->user === null;
}
/**
* Return whether this represents a verified user
* @return bool
*/
public function isVerified() {
return $this->verified;
}
/**
* Return the user ID
* @note Do not use this to test for anonymous users!
* @return int
*/
public function getId() {
return $this->user === null ? 0 : $this->user->getId();
}
/**
* Return the user name
* @return string|null
*/
public function getName() {
return $this->user === null ? null : $this->user->getName();
}
/**
* Return the user token
* @return string|null
*/
public function getToken() {
return $this->user === null || $this->user->getId() === 0 ? null : $this->user->getToken( true );
}
/**
* Return a User object
* @return User
*/
public function getUser() {
return $this->user === null ? new User : $this->user;
}
/**
* Return a verified version of this object
* @return UserInfo
*/
public function verified() {
return $this->verified ? $this : new self( $this->user, true );
}
public function __toString() {
if ( $this->user === null ) {
return '<anon>';
}
return '<' .
( $this->verified ? '+' : '-' ) . ':' .
$this->getId() . ':' . $this->getName() .
'>';
}
}

View file

@ -21,6 +21,7 @@
* @ingroup SpecialPage
*/
use MediaWiki\Logger\LoggerFactory;
use MediaWiki\Session\SessionManager;
/**
* Implements Special:UserLogin
@ -263,9 +264,9 @@ class LoginForm extends SpecialPage {
* @param string|null $subPage
*/
public function execute( $subPage ) {
if ( session_id() == '' ) {
wfSetupSession();
}
// Make sure session is persisted
$session = MediaWiki\Session\SessionManager::getGlobalSession();
$session->persist();
$this->load();
@ -276,6 +277,17 @@ class LoginForm extends SpecialPage {
}
$this->setHeaders();
// Make sure it's possible to log in
if ( $this->mType !== 'signup' && !$session->canSetUser() ) {
throw new ErrorPageError(
'cannotloginnow-title',
'cannotloginnow-text',
array(
$session->getProvider()->describe( RequestContext::getMain()->getLanguage() )
)
);
}
/**
* In the case where the user is already logged in, and was redirected to
* the login form from a page that requires login, do not show the login
@ -1376,7 +1388,7 @@ class LoginForm extends SpecialPage {
if ( $user->isLoggedIn() ) {
$this->mUsername = $user->getName();
} else {
$this->mUsername = $this->getRequest()->getCookie( 'UserName' );
$this->mUsername = $this->getRequest()->getSession()->suggestLoginUsername();
}
}
@ -1552,7 +1564,8 @@ class LoginForm extends SpecialPage {
function hasSessionCookie() {
global $wgDisableCookieCheck;
return $wgDisableCookieCheck ? true : $this->getRequest()->checkSessionCookie();
return $wgDisableCookieCheck ||
SessionManager::singleton()->getPersistedSessionId( $this->getRequest() ) !== null;
}
/**
@ -1571,7 +1584,7 @@ class LoginForm extends SpecialPage {
public static function setLoginToken() {
global $wgRequest;
// Generate a token directly instead of using $user->getEditToken()
// because the latter reuses $_SESSION['wsEditToken']
// because the latter reuses wsEditToken in the session
$wgRequest->setSessionData( 'wsLoginToken', MWCryptRand::generateHex( 32 ) );
}
@ -1617,7 +1630,7 @@ class LoginForm extends SpecialPage {
$wgCookieSecure = false;
}
wfResetSessionID();
MediaWiki\Session\SessionManager::getGlobalSession()->resetId();
}
/**

View file

@ -44,6 +44,18 @@ class SpecialUserlogout extends UnlistedSpecialPage {
$this->setHeaders();
$this->outputHeader();
// Make sure it's possible to log out
$session = MediaWiki\Session\SessionManager::getGlobalSession();
if ( !$session->canSetUser() ) {
throw new ErrorPageError(
'cannotlogoutnow-title',
'cannotlogoutnow-text',
array(
$session->getProvider()->describe( RequestContext::getMain()->getLanguage() )
)
);
}
$user = $this->getUser();
$oldName = $user->getName();
$user->logout();

View file

@ -390,7 +390,7 @@ class UploadFromUrl extends UploadBase {
'userName' => $user->getName(),
'leaveMessage' => $this->mAsync == 'async-leavemessage',
'ignoreWarnings' => $this->mIgnoreWarnings,
'sessionId' => session_id(),
'sessionId' => MediaWiki\Session\SessionManager::getGlobalSession()->getId(),
'sessionKey' => $sessionKey,
) );
$job->initializeSessionData();

View file

@ -20,6 +20,8 @@
* @file
*/
use MediaWiki\Session\SessionManager;
/**
* String Some punctuation to prevent editing from broken text-mangling proxies.
* @ingroup Constants
@ -99,6 +101,7 @@ class User implements IDBAccessObject {
'apihighlimits',
'applychangetags',
'autoconfirmed',
'autocreateaccount',
'autopatrol',
'bigdelete',
'block',
@ -227,7 +230,7 @@ class User implements IDBAccessObject {
* - 'defaults' anonymous user initialised from class defaults
* - 'name' initialise from mName
* - 'id' initialise from mId
* - 'session' log in from cookies or session if possible
* - 'session' log in from session if possible
*
* Use the User::newFrom*() family of functions to set this.
*/
@ -311,14 +314,26 @@ class User implements IDBAccessObject {
* @param integer $flags User::READ_* constant bitfield
*/
public function load( $flags = self::READ_NORMAL ) {
global $wgFullyInitialised;
if ( $this->mLoadedItems === true ) {
return;
}
// Set it now to avoid infinite recursion in accessors
$oldLoadedItems = $this->mLoadedItems;
$this->mLoadedItems = true;
$this->queryFlagsUsed = $flags;
// If this is called too early, things are likely to break.
if ( $this->mFrom === 'session' && empty( $wgFullyInitialised ) ) {
\MediaWiki\Logger\LoggerFactory::getInstance( 'session' )
->warning( 'User::loadFromSession called before the end of Setup.php' );
$this->loadDefaults();
$this->mLoadedItems = $oldLoadedItems;
return;
}
switch ( $this->mFrom ) {
case 'defaults':
$this->loadDefaults();
@ -541,8 +556,8 @@ class User implements IDBAccessObject {
}
/**
* Create a new user object using data from session or cookies. If the
* login credentials are invalid, the result is an anonymous user.
* Create a new user object using data from session. If the login
* credentials are invalid, the result is an anonymous user.
*
* @param WebRequest|null $request Object to use; $wgRequest will be used if omitted.
* @return User
@ -662,6 +677,8 @@ class User implements IDBAccessObject {
$user->saveSettings();
}
SessionManager::singleton()->preventSessionsForUser( $user->getName() );
return $user;
}
@ -1069,8 +1086,9 @@ class User implements IDBAccessObject {
$this->mOptionOverrides = null;
$this->mOptionsLoaded = false;
$loggedOut = $this->getRequest()->getCookie( 'LoggedOut' );
if ( $loggedOut !== null ) {
$request = $this->getRequest();
$loggedOut = $request ? $request->getSession()->getLoggedOutTimestamp() : 0;
if ( $loggedOut !== 0 ) {
$this->mTouched = wfTimestamp( TS_MW, $loggedOut );
} else {
$this->mTouched = '1'; # Allow any pages to be cached
@ -1115,84 +1133,32 @@ class User implements IDBAccessObject {
}
/**
* Load user data from the session or login cookie.
* Load user data from the session.
*
* @return bool True if the user is logged in, false otherwise.
*/
private function loadFromSession() {
// Deprecated hook
$result = null;
Hooks::run( 'UserLoadFromSession', array( $this, &$result ) );
Hooks::run( 'UserLoadFromSession', array( $this, &$result ), '1.27' );
if ( $result !== null ) {
return $result;
}
$request = $this->getRequest();
$cookieId = $request->getCookie( 'UserID' );
$sessId = $request->getSessionData( 'wsUserID' );
if ( $cookieId !== null ) {
$sId = intval( $cookieId );
if ( $sessId !== null && $cookieId != $sessId ) {
wfDebugLog( 'loginSessions', "Session user ID ($sessId) and
cookie user ID ($sId) don't match!" );
return false;
}
$request->setSessionData( 'wsUserID', $sId );
} elseif ( $sessId !== null && $sessId != 0 ) {
$sId = $sessId;
} else {
return false;
}
if ( $request->getSessionData( 'wsUserName' ) !== null ) {
$sName = $request->getSessionData( 'wsUserName' );
} elseif ( $request->getCookie( 'UserName' ) !== null ) {
$sName = $request->getCookie( 'UserName' );
$request->setSessionData( 'wsUserName', $sName );
} else {
return false;
}
$proposedUser = User::newFromId( $sId );
if ( !$proposedUser->isLoggedIn() ) {
// Not a valid ID
return false;
}
global $wgBlockDisablesLogin;
if ( $wgBlockDisablesLogin && $proposedUser->isBlocked() ) {
// User blocked and we've disabled blocked user logins
return false;
}
if ( $request->getSessionData( 'wsToken' ) ) {
$passwordCorrect =
( $proposedUser->getToken( false ) === $request->getSessionData( 'wsToken' ) );
$from = 'session';
} elseif ( $request->getCookie( 'Token' ) ) {
# Get the token from DB/cache and clean it up to remove garbage padding.
# This deals with historical problems with bugs and the default column value.
$token = rtrim( $proposedUser->getToken( false ) ); // correct token
// Make comparison in constant time (bug 61346)
$passwordCorrect = strlen( $token )
&& hash_equals( $token, $request->getCookie( 'Token' ) );
$from = 'cookie';
} else {
// No session or persistent login cookie
return false;
}
if ( ( $sName === $proposedUser->getName() ) && $passwordCorrect ) {
$this->loadFromUserObject( $proposedUser );
$request->setSessionData( 'wsToken', $this->mToken );
wfDebug( "User: logged in from $from\n" );
// MediaWiki\Session\Session already did the necessary authentication of the user
// returned here, so just use it if applicable.
$session = $this->getRequest()->getSession();
$user = $session->getUser();
if ( $user->isLoggedIn() ) {
$this->loadFromUserObject( $user );
// Other code expects these to be set in the session, so set them.
$session->set( 'wsUserID', $this->getId() );
$session->set( 'wsUserName', $this->getName() );
$session->set( 'wsToken', $this->mToken );
return true;
} else {
// Invalid credentials
wfDebug( "User: can't log in from $from, invalid credentials\n" );
return false;
}
return false;
}
/**
@ -3502,6 +3468,7 @@ class User implements IDBAccessObject {
/**
* Set a cookie on the user's client. Wrapper for
* WebResponse::setCookie
* @deprecated since 1.27
* @param string $name Name of the cookie to set
* @param string $value Value to set
* @param int $exp Expiration time, as a UNIX time value;
@ -3517,6 +3484,7 @@ class User implements IDBAccessObject {
protected function setCookie(
$name, $value, $exp = 0, $secure = null, $params = array(), $request = null
) {
wfDeprecated( __METHOD__, '1.27' );
if ( $request === null ) {
$request = $this->getRequest();
}
@ -3526,6 +3494,7 @@ class User implements IDBAccessObject {
/**
* Clear a cookie on the user's client
* @deprecated since 1.27
* @param string $name Name of the cookie to clear
* @param bool $secure
* true: Force setting the secure attribute when setting the cookie
@ -3534,6 +3503,7 @@ class User implements IDBAccessObject {
* @param array $params Array of options sent passed to WebResponse::setcookie()
*/
protected function clearCookie( $name, $secure = null, $params = array() ) {
wfDeprecated( __METHOD__, '1.27' );
$this->setCookie( $name, '', time() - 86400, $secure, $params );
}
@ -3544,6 +3514,7 @@ class User implements IDBAccessObject {
*
* @see User::setCookie
*
* @deprecated since 1.27
* @param string $name Name of the cookie to set
* @param string $value Value to set
* @param bool $secure
@ -3554,6 +3525,8 @@ class User implements IDBAccessObject {
protected function setExtendedLoginCookie( $name, $value, $secure ) {
global $wgExtendedLoginCookieExpiration, $wgCookieExpiration;
wfDeprecated( __METHOD__, '1.27' );
$exp = time();
$exp += $wgExtendedLoginCookieExpiration !== null
? $wgExtendedLoginCookieExpiration
@ -3563,7 +3536,7 @@ class User implements IDBAccessObject {
}
/**
* Set the default cookies for this session on the user's client.
* Persist this user's session (e.g. set cookies)
*
* @param WebRequest|null $request WebRequest object to use; $wgRequest will be used if null
* is passed.
@ -3571,72 +3544,36 @@ class User implements IDBAccessObject {
* @param bool $rememberMe Whether to add a Token cookie for elongated sessions
*/
public function setCookies( $request = null, $secure = null, $rememberMe = false ) {
global $wgExtendedLoginCookies;
if ( $request === null ) {
$request = $this->getRequest();
}
$this->load();
if ( 0 == $this->mId ) {
return;
}
if ( !$this->mToken ) {
// When token is empty or NULL generate a new one and then save it to the database
// This allows a wiki to re-secure itself after a leak of it's user table or $wgSecretKey
// Simply by setting every cell in the user_token column to NULL and letting them be
// regenerated as users log back into the wiki.
$this->setToken();
if ( !wfReadOnly() ) {
$this->saveSettings();
$session = $this->getRequest()->getSession();
if ( $request && $session->getRequest() !== $request ) {
$session = $session->sessionWithRequest( $request );
}
$delay = $session->delaySave();
if ( !$session->getUser()->equals( $this ) ) {
if ( !$session->canSetUser() ) {
\MediaWiki\Logger\LoggerFactory::getInstance( 'session' )
->warning( __METHOD__ .
": Cannot save user \"$this\" to a user \"{$session->getUser()}\"'s immutable session"
);
return;
}
}
$session = array(
'wsUserID' => $this->mId,
'wsToken' => $this->mToken,
'wsUserName' => $this->getName()
);
$cookies = array(
'UserID' => $this->mId,
'UserName' => $this->getName(),
);
if ( $rememberMe ) {
$cookies['Token'] = $this->mToken;
} else {
$cookies['Token'] = false;
$session->setUser( $this );
}
Hooks::run( 'UserSetCookies', array( $this, &$session, &$cookies ) );
foreach ( $session as $name => $value ) {
$request->setSessionData( $name, $value );
}
foreach ( $cookies as $name => $value ) {
if ( $value === false ) {
$this->clearCookie( $name );
} elseif ( $rememberMe && in_array( $name, $wgExtendedLoginCookies ) ) {
$this->setExtendedLoginCookie( $name, $value, $secure );
} else {
$this->setCookie( $name, $value, 0, $secure, array(), $request );
}
$session->setRememberUser( $rememberMe );
if ( $secure !== null ) {
$session->setForceHTTPS( $secure );
}
/**
* If wpStickHTTPS was selected, also set an insecure cookie that
* will cause the site to redirect the user to HTTPS, if they access
* it over HTTP. Bug 29898. Use an un-prefixed cookie, so it's the same
* as the one set by centralauth (bug 53538). Also set it to session, or
* standard time setting, based on if rememberme was set.
*/
if ( $request->getCheck( 'wpStickHTTPS' ) || $this->requiresHTTPS() ) {
$this->setCookie(
'forceHTTPS',
'true',
$rememberMe ? 0 : null,
false,
array( 'prefix' => '' ) // no prefix
);
}
$session->persist();
ScopedCallback::consume( $delay );
}
/**
@ -3649,20 +3586,29 @@ class User implements IDBAccessObject {
}
/**
* Clear the user's cookies and session, and reset the instance cache.
* Clear the user's session, and reset the instance cache.
* @see logout()
*/
public function doLogout() {
$this->clearInstanceCache( 'defaults' );
$this->getRequest()->setSessionData( 'wsUserID', 0 );
$this->clearCookie( 'UserID' );
$this->clearCookie( 'Token' );
$this->clearCookie( 'forceHTTPS', false, array( 'prefix' => '' ) );
// Remember when user logged out, to prevent seeing cached pages
$this->setCookie( 'LoggedOut', time(), time() + 86400 );
$session = $this->getRequest()->getSession();
if ( !$session->canSetUser() ) {
\MediaWiki\Logger\LoggerFactory::getInstance( 'session' )
->warning( __METHOD__ . ": Cannot log out of an immutable session" );
} elseif ( !$session->getUser()->equals( $this ) ) {
\MediaWiki\Logger\LoggerFactory::getInstance( 'session' )
->warning( __METHOD__ .
": Cannot log user \"$this\" out of a user \"{$session->getUser()}\"'s session"
);
// But we still may as well make this user object anon
$this->clearInstanceCache( 'defaults' );
} else {
$this->clearInstanceCache( 'defaults' );
$delay = $session->delaySave();
$session->setLoggedOutTimestamp( time() );
$session->setUser( new User );
$session->set( 'wsUserID', 0 ); // Other code expects this
ScopedCallback::consume( $delay );
}
}
/**

View file

@ -389,6 +389,8 @@
"virus-scanfailed": "scan failed (code $1)",
"virus-unknownscanner": "unknown antivirus:",
"logouttext": "<strong>You are now logged out.</strong>\n\nNote that some pages may continue to be displayed as if you were still logged in, until you clear your browser cache.",
"cannotlogoutnow-title": "Cannot log out now",
"cannotlogoutnow-text": "Logging out is not possible when using $1.",
"welcomeuser": "Welcome, $1!",
"welcomecreation-msg": "Your account has been created.\nYou can change your {{SITENAME}} [[Special:Preferences|preferences]] if you wish.",
"yourname": "Username:",
@ -406,6 +408,8 @@
"remembermypassword": "Remember my login on this browser (for a maximum of $1 {{PLURAL:$1|day|days}})",
"userlogin-remembermypassword": "Keep me logged in",
"userlogin-signwithsecure": "Use secure connection",
"cannotloginnow-title": "Cannot log in now",
"cannotloginnow-text": "Logging in is not possible when using $1.",
"yourdomainname": "Your domain:",
"password-change-forbidden": "You cannot change passwords on this wiki.",
"externaldberror": "There was either an authentication database error or you are not allowed to update your external account.",
@ -1112,6 +1116,7 @@
"right-createpage": "Create pages (which are not discussion pages)",
"right-createtalk": "Create discussion pages",
"right-createaccount": "Create new user accounts",
"right-autocreateaccount": "Automatically log in with an external user account",
"right-minoredit": "Mark edits as minor",
"right-move": "Move pages",
"right-move-subpages": "Move pages with their subpages",
@ -1190,6 +1195,7 @@
"action-createpage": "create pages",
"action-createtalk": "create discussion pages",
"action-createaccount": "create this user account",
"action-autocreateaccount": "automatically create this external user account",
"action-history": "view the history of this page",
"action-minoredit": "mark this edit as minor",
"action-move": "move this page",
@ -3924,5 +3930,9 @@
"mw-widgets-dateinput-placeholder-month": "YYYY-MM",
"mw-widgets-titleinput-description-new-page": "page does not exist yet",
"mw-widgets-titleinput-description-redirect": "redirect to $1",
"api-error-blacklisted": "Please choose a different, descriptive title."
"api-error-blacklisted": "Please choose a different, descriptive title.",
"sessionmanager-tie": "Cannot combine multiple request authentication types: $1.",
"sessionprovider-generic": "$1 sessions",
"sessionprovider-mediawiki-session-cookiesessionprovider": "cookie-based sessions",
"sessionprovider-nocookies": "Cookies may be disabled. Ensure you have cookies enabled and start again."
}

View file

@ -564,6 +564,8 @@
"virus-scanfailed": "Used as error message. \"scan\" stands for \"virus scan\". Parameters:\n* $1 - exit code of virus scanner",
"virus-unknownscanner": "Used as error message. This message is followed by the virus scanner name.",
"logouttext": "Log out message. Parameters:\n* $1 - (Unused) an URL to [[Special:Userlogin]] containing <code>returnto</code> and <code>returntoquery</code> parameters",
"cannotlogoutnow-title": "Error page title shown when logging out is not possible.",
"cannotlogoutnow-text": "Error page text shown when logging out is not possible. Parameters:\n* $1 - Session type in use that makes it not possible to log out, from a message like {{msg-mw|sessionprovider-mediawiki-session-cookiesessionprovider}}.",
"welcomeuser": "Text for a welcome heading that users see after registering a user account.\n\nParameters:\n* $1 - the username of the new user. See [[phab:T44215]]",
"welcomecreation-msg": "A welcome message users see after registering a user account, following a welcomeuser heading.\n\nParameters:\n* $1 - (Unused) the username of the new user.\n\nReplaces [[MediaWiki:welcomecreation|welcomecreation]] in 1.21wmf5, see [[phab:T44215]]",
"yourname": "Since 1.22 no longer used in core, but used by some extensions.\n{{Identical|Username}}",
@ -581,6 +583,8 @@
"remembermypassword": "Used as checkbox label on [[Special:ChangePassword]]. Parameters:\n* $1 - number of days\n{{Identical|Remember my login on this computer}}",
"userlogin-remembermypassword": "The text for a check box in [[Special:UserLogin]].",
"userlogin-signwithsecure": "Text of link to HTTPS login form.\n\nSee example: [[Special:UserLogin]]",
"cannotloginnow-title": "Error page title shown when logging in is not possible.",
"cannotloginnow-text": "Error page text shown when logging in is not possible. Parameters:\n* $1 - Session type in use that makes it not possible to log in, from a message like {{msg-mw|sessionprovider-mediawiki-session-cookiesessionprovider}}.",
"yourdomainname": "Used as label for listbox.",
"password-change-forbidden": "Error message shown when an external authentication source does not allow the password to be changed.",
"externaldberror": "This message is thrown when a valid attempt to change the wiki password for a user fails because of a database error or an error from an external system.",
@ -1287,6 +1291,7 @@
"right-createpage": "{{doc-right|createpage}}\nBasic right to create pages. The right to edit discussion/talk pages is {{msg-mw|right-createtalk}}.",
"right-createtalk": "{{doc-right|createtalk}}\nBasic right to create discussion/talk pages. The right to edit other pages is {{msg-mw|right-createpage}}.",
"right-createaccount": "{{doc-right|createaccount}}\nThe right to [[Special:CreateAccount|create a user account]].",
"right-autocreateaccount": "{{doc-right|autocreateaccount}}\nThe right to automatically create an account from an external source (e.g. CentralAuth).",
"right-minoredit": "{{doc-right|minoredit}}\nThe right to use the \"This is a minor edit\" checkbox. See {{msg-mw|minoredit}} for the message used for that checkbox.",
"right-move": "{{doc-right|move}}\nThe right to move any page that is not protected from moving.\n{{Identical|Move page}}",
"right-move-subpages": "{{doc-right|move-subpages}}",
@ -1365,6 +1370,7 @@
"action-createpage": "{{Doc-action|createpage}}\n{{Identical|Create page}}",
"action-createtalk": "{{Doc-action|createtalk}}",
"action-createaccount": "{{Doc-action|createaccount}}",
"action-autocreateaccount": "{{Doc-action|autocreateaccount}}",
"action-history": "{{Doc-action|history}}",
"action-minoredit": "{{Doc-action|minoredit}}",
"action-move": "{{Doc-action|move}}",
@ -4099,5 +4105,9 @@
"mw-widgets-dateinput-placeholder-month": "Placeholder displayed in a date input field when it's empty, representing a date format with 4 digits for year and 2 digits for month, separated with hyphens (without a day). This should be uppercase, if possible, and must not include any additional explanations. If there is no good way to translate it, make this message blank.",
"mw-widgets-titleinput-description-new-page": "Description label for a new page in the title input widget.",
"mw-widgets-titleinput-description-redirect": "Description label for a redirect in the title input widget.",
"api-error-blacklisted": "Used as error message.\n\nFollowed by the link {{msg-mw|Mwe-upwiz-feedback-blacklist-info-prompt}}."
"api-error-blacklisted": "Used as error message.\n\nFollowed by the link {{msg-mw|Mwe-upwiz-feedback-blacklist-info-prompt}}.",
"sessionmanager-tie": "Used as an error message when multiple session sources are tied in priority.\n\nParameters:\n* $1 - List of dession type descriptions, from messages like {{msg-mw|sessionprovider-mediawiki-session-cookiesessionprovider}}.",
"sessionprovider-generic": "Used to create a generic session type description when one isn't provided via the proper message. Should be phrased to make sense when added to a message such as {{msg-mw|cannotloginnow-text}}.\n\nParameters:\n* $1 - PHP classname.",
"sessionprovider-mediawiki-session-cookiesessionprovider": "Description of the sessions provided by the CookieSessionProvider class, which use HTTP cookies. Should be phrased to make sense when added to a message such as {{msg-mw|cannotloginnow-text}}.",
"sessionprovider-nocookies": "Used to inform the user that sessions may be missing due to lack of cookies."
}

View file

@ -49,6 +49,7 @@ $wgAutoloadClasses += array(
# tests/phpunit/includes
'TestingAccessWrapper' => "$testDir/phpunit/includes/TestingAccessWrapper.php",
'TestLogger' => "$testDir/phpunit/includes/TestLogger.php",
# tests/phpunit/includes/api
'ApiFormatTestBase' => "$testDir/phpunit/includes/api/format/ApiFormatTestBase.php",
@ -94,6 +95,10 @@ $wgAutoloadClasses += array(
'ResourceLoaderImageModuleTestable' =>
"$testDir/phpunit/includes/resourceloader/ResourceLoaderImageModuleTest.php",
# tests/phpunit/includes/session
'MediaWiki\\Session\\TestBagOStuff' => "$testDir/phpunit/includes/session/TestBagOStuff.php",
'MediaWiki\\Session\\TestUtils' => "$testDir/phpunit/includes/session/TestUtils.php",
# tests/phpunit/includes/specials
'SpecialPageTestBase' => "$testDir/phpunit/includes/specials/SpecialPageTestBase.php",
@ -118,6 +123,9 @@ $wgAutoloadClasses += array(
'MockSvgHandler' => "$testDir/phpunit/mocks/media/MockSvgHandler.php",
'MockDjVuHandler' => "$testDir/phpunit/mocks/media/MockDjVuHandler.php",
'MockWebRequest' => "$testDir/phpunit/mocks/MockWebRequest.php",
'MediaWiki\\Session\\DummySessionBackend'
=> "$testDir/phpunit/mocks/session/DummySessionBackend.php",
'DummySessionProvider' => "$testDir/phpunit/mocks/session/DummySessionProvider.php",
# tests/parser
'NewParserTest' => "$testDir/phpunit/includes/parser/NewParserTest.php",

View file

@ -221,6 +221,8 @@ abstract class MediaWikiTestCase extends PHPUnit_Framework_TestCase {
}
protected function tearDown() {
global $wgRequest;
$status = ob_get_status();
if ( isset( $status['name'] ) &&
$status['name'] === 'MediaWikiTestCase::wfResetOutputBuffersBarrier'
@ -252,6 +254,12 @@ abstract class MediaWikiTestCase extends PHPUnit_Framework_TestCase {
$this->mwGlobals = array();
RequestContext::resetMain();
MediaHandler::resetCache();
if ( session_id() !== '' ) {
session_write_close();
session_id( '' );
}
$wgRequest = new FauxRequest();
MediaWiki\Session\SessionManager::resetCache();
$phpErrorLevel = intval( ini_get( 'error_reporting' ) );
@ -509,6 +517,13 @@ abstract class MediaWikiTestCase extends PHPUnit_Framework_TestCase {
false,
$user
);
// doEditContent() probably started the session via
// User::loadFromSession(). Close it now.
if ( session_id() !== '' ) {
session_write_close();
session_id( '' );
}
}
}

View file

@ -0,0 +1,105 @@
<?php
/**
* Testing logger
*
* Copyright (C) 2015 Brad Jorsch <bjorsch@wikimedia.org>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along
* with this program; if not, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
* http://www.gnu.org/copyleft/gpl.html
*
* @file
* @author Brad Jorsch <bjorsch@wikimedia.org>
*/
use Psr\Log\LogLevel;
/**
* A logger that may be configured to either buffer logs or to print them to
* the output where PHPUnit will complain about them.
*
* @since 1.27
*/
class TestLogger extends \Psr\Log\AbstractLogger {
private $collect = false;
private $buffer = array();
private $filter = null;
/**
* @param bool $collect Whether to collect logs
* @param callable $filter Filter logs before collecting/printing. Signature is
* string|null function ( string $message, string $level );
*/
public function __construct( $collect = false, $filter = null ) {
$this->collect = $collect;
$this->filter = $filter;
}
/**
* Set the "collect" flag
* @param bool $collect
*/
public function setCollect( $collect ) {
$this->collect = $collect;
}
/**
* Return the collected logs
* @return array Array of array( string $level, string $message )
*/
public function getBuffer() {
return $this->buffer;
}
/**
* Clear the collected log buffer
*/
public function clearBuffer() {
$this->buffer = array();
}
public function log( $level, $message, array $context = array() ) {
$message = trim( $message );
if ( $this->filter ) {
$message = call_user_func( $this->filter, $message, $level );
if ( $message === null ) {
return;
}
}
if ( $this->collect ) {
$this->buffer[] = array( $level, $message );
} else {
switch ( $level ) {
case LogLevel::DEBUG:
case LogLevel::INFO:
case LogLevel::NOTICE:
trigger_error( "LOG[$level]: $message", E_USER_NOTICE );
break;
case LogLevel::WARNING:
trigger_error( "LOG[$level]: $message", E_USER_WARNING );
break;
case LogLevel::ERROR:
case LogLevel::CRITICAL:
case LogLevel::ALERT:
case LogLevel::EMERGENCY:
trigger_error( "LOG[$level]: $message", E_USER_ERROR );
break;
}
}
}
}

View file

@ -97,7 +97,8 @@ class ApiMainTest extends ApiTestCase {
$request->setHeaders( $headers );
$request->response()->statusHeader( 200 ); // Why doesn't it default?
$api = new ApiMain( $request );
$context = $this->apiContext->newTestContext( $request, null );
$api = new ApiMain( $context );
$priv = TestingAccessWrapper::newFromObject( $api );
$priv->mInternalMode = false;

View file

@ -47,11 +47,7 @@ abstract class ApiTestCase extends MediaWikiLangTestCase {
protected function tearDown() {
// Avoid leaking session over tests
if ( session_id() != '' ) {
global $wgUser;
$wgUser->logout();
session_destroy();
}
MediaWiki\Session\SessionManager::getGlobalSession()->clear();
parent::tearDown();
}

View file

@ -15,8 +15,6 @@ abstract class ApiTestCaseUpload extends ApiTestCase {
'wgEnableAPI' => true,
) );
wfSetupSession();
$this->clearFakeUploads();
}

View file

@ -37,6 +37,14 @@ class RequestContextTest extends MediaWikiTestCase {
* @covers RequestContext::importScopedSession
*/
public function testImportScopedSession() {
// Make sure session handling is started
if ( !MediaWiki\Session\PHPSessionHandler::isInstalled() ) {
MediaWiki\Session\PHPSessionHandler::install(
MediaWiki\Session\SessionManager::singleton()
);
}
$oldSessionId = session_id();
$context = RequestContext::getMain();
$oInfo = $context->exportSession();
@ -76,7 +84,16 @@ class RequestContextTest extends MediaWikiTestCase {
$context->getRequest()->getAllHeaders(),
"Correct context headers."
);
$this->assertEquals( $sinfo['sessionId'], session_id(), "Correct context session ID." );
$this->assertEquals(
$sinfo['sessionId'],
MediaWiki\Session\SessionManager::getGlobalSession()->getId(),
"Correct context session ID."
);
if ( \MediaWiki\Session\PhpSessionHandler::isEnabled() ) {
$this->assertEquals( $sinfo['sessionId'], session_id(), "Correct context session ID." );
} else {
$this->assertEquals( $oldSessionId, session_id(), "Unchanged PHP session ID." );
}
$this->assertEquals( true, $context->getUser()->isLoggedIn(), "Correct context user." );
$this->assertEquals( $sinfo['userId'], $context->getUser()->getId(), "Correct context user ID." );
$this->assertEquals(

View file

@ -0,0 +1,726 @@
<?php
namespace MediaWiki\Session;
use MediaWikiTestCase;
use User;
/**
* @group Session
* @group Database
* @covers MediaWiki\Session\CookieSessionProvider
*/
class CookieSessionProviderTest extends MediaWikiTestCase {
private function getConfig() {
global $wgCookieExpiration;
return new \HashConfig( array(
'CookiePrefix' => 'CookiePrefix',
'CookiePath' => 'CookiePath',
'CookieDomain' => 'CookieDomain',
'CookieSecure' => true,
'CookieHttpOnly' => true,
'SessionName' => false,
'ExtendedLoginCookies' => array( 'UserID', 'Token' ),
'ExtendedLoginCookieExpiration' => $wgCookieExpiration * 2,
) );
}
public function testConstructor() {
try {
new CookieSessionProvider();
$this->fail( 'Expected exception not thrown' );
} catch ( \InvalidArgumentException $ex ) {
$this->assertSame(
'MediaWiki\\Session\\CookieSessionProvider::__construct: priority must be specified',
$ex->getMessage()
);
}
try {
new CookieSessionProvider( array( 'priority' => 'foo' ) );
$this->fail( 'Expected exception not thrown' );
} catch ( \InvalidArgumentException $ex ) {
$this->assertSame(
'MediaWiki\\Session\\CookieSessionProvider::__construct: Invalid priority',
$ex->getMessage()
);
}
try {
new CookieSessionProvider( array( 'priority' => SessionInfo::MIN_PRIORITY - 1 ) );
$this->fail( 'Expected exception not thrown' );
} catch ( \InvalidArgumentException $ex ) {
$this->assertSame(
'MediaWiki\\Session\\CookieSessionProvider::__construct: Invalid priority',
$ex->getMessage()
);
}
try {
new CookieSessionProvider( array( 'priority' => SessionInfo::MAX_PRIORITY + 1 ) );
$this->fail( 'Expected exception not thrown' );
} catch ( \InvalidArgumentException $ex ) {
$this->assertSame(
'MediaWiki\\Session\\CookieSessionProvider::__construct: Invalid priority',
$ex->getMessage()
);
}
try {
new CookieSessionProvider( array( 'priority' => 1, 'cookieOptions' => null ) );
$this->fail( 'Expected exception not thrown' );
} catch ( \InvalidArgumentException $ex ) {
$this->assertSame(
'MediaWiki\\Session\\CookieSessionProvider::__construct: cookieOptions must be an array',
$ex->getMessage()
);
}
$config = $this->getConfig();
$p = \TestingAccessWrapper::newFromObject(
new CookieSessionProvider( array( 'priority' => 1 ) )
);
$p->setLogger( new \TestLogger() );
$p->setConfig( $config );
$this->assertEquals( 1, $p->priority );
$this->assertEquals( array(
'callUserSetCookiesHook' => false,
'sessionName' => 'CookiePrefix_session',
), $p->params );
$this->assertEquals( array(
'prefix' => 'CookiePrefix',
'path' => 'CookiePath',
'domain' => 'CookieDomain',
'secure' => true,
'httpOnly' => true,
), $p->cookieOptions );
$config->set( 'SessionName', 'SessionName' );
$p = \TestingAccessWrapper::newFromObject(
new CookieSessionProvider( array( 'priority' => 3 ) )
);
$p->setLogger( new \TestLogger() );
$p->setConfig( $config );
$this->assertEquals( 3, $p->priority );
$this->assertEquals( array(
'callUserSetCookiesHook' => false,
'sessionName' => 'SessionName',
), $p->params );
$this->assertEquals( array(
'prefix' => 'CookiePrefix',
'path' => 'CookiePath',
'domain' => 'CookieDomain',
'secure' => true,
'httpOnly' => true,
), $p->cookieOptions );
$p = \TestingAccessWrapper::newFromObject( new CookieSessionProvider( array(
'priority' => 10,
'callUserSetCookiesHook' => true,
'cookieOptions' => array(
'prefix' => 'XPrefix',
'path' => 'XPath',
'domain' => 'XDomain',
'secure' => 'XSecure',
'httpOnly' => 'XHttpOnly',
),
'sessionName' => 'XSession',
) ) );
$p->setLogger( new \TestLogger() );
$p->setConfig( $config );
$this->assertEquals( 10, $p->priority );
$this->assertEquals( array(
'callUserSetCookiesHook' => true,
'sessionName' => 'XSession',
), $p->params );
$this->assertEquals( array(
'prefix' => 'XPrefix',
'path' => 'XPath',
'domain' => 'XDomain',
'secure' => 'XSecure',
'httpOnly' => 'XHttpOnly',
), $p->cookieOptions );
}
public function testBasics() {
$provider = new CookieSessionProvider( array( 'priority' => 10 ) );
$this->assertTrue( $provider->persistsSessionID() );
$this->assertTrue( $provider->canChangeUser() );
$msg = $provider->whyNoSession();
$this->assertInstanceOf( 'Message', $msg );
$this->assertSame( 'sessionprovider-nocookies', $msg->getKey() );
}
public function testProvideSessionInfo() {
$params = array(
'priority' => 20,
'sessionName' => 'session',
'cookieOptions' => array( 'prefix' => 'x' ),
);
$provider = new CookieSessionProvider( $params );
$provider->setLogger( new \TestLogger() );
$provider->setConfig( $this->getConfig() );
$provider->setManager( new SessionManager() );
$user = User::newFromName( 'UTSysop' );
$id = $user->getId();
$name = $user->getName();
$token = $user->getToken( true );
$sessionId = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa';
// No data
$request = new \FauxRequest();
$info = $provider->provideSessionInfo( $request );
$this->assertNull( $info );
// Session key only
$request = new \FauxRequest();
$request->setCookies( array(
'session' => $sessionId,
), '' );
$info = $provider->provideSessionInfo( $request );
$this->assertNotNull( $info );
$this->assertSame( $params['priority'], $info->getPriority() );
$this->assertSame( $sessionId, $info->getId() );
$this->assertNull( $info->getUserInfo() );
$this->assertFalse( $info->forceHTTPS() );
// User, no session key
$request = new \FauxRequest();
$request->setCookies( array(
'xUserID' => $id,
'xToken' => $token,
), '' );
$info = $provider->provideSessionInfo( $request );
$this->assertNotNull( $info );
$this->assertSame( $params['priority'], $info->getPriority() );
$this->assertNotSame( $sessionId, $info->getId() );
$this->assertNotNull( $info->getUserInfo() );
$this->assertSame( $id, $info->getUserInfo()->getId() );
$this->assertSame( $name, $info->getUserInfo()->getName() );
$this->assertFalse( $info->forceHTTPS() );
// User and session key
$request = new \FauxRequest();
$request->setCookies( array(
'session' => $sessionId,
'xUserID' => $id,
'xToken' => $token,
), '' );
$info = $provider->provideSessionInfo( $request );
$this->assertNotNull( $info );
$this->assertSame( $params['priority'], $info->getPriority() );
$this->assertSame( $sessionId, $info->getId() );
$this->assertNotNull( $info->getUserInfo() );
$this->assertSame( $id, $info->getUserInfo()->getId() );
$this->assertSame( $name, $info->getUserInfo()->getName() );
$this->assertFalse( $info->forceHTTPS() );
// User with bad token
$request = new \FauxRequest();
$request->setCookies( array(
'session' => $sessionId,
'xUserID' => $id,
'xToken' => 'BADTOKEN',
), '' );
$info = $provider->provideSessionInfo( $request );
$this->assertNull( $info );
// User id with no token
$request = new \FauxRequest();
$request->setCookies( array(
'session' => $sessionId,
'xUserID' => $id,
), '' );
$info = $provider->provideSessionInfo( $request );
$this->assertNotNull( $info );
$this->assertSame( $params['priority'], $info->getPriority() );
$this->assertSame( $sessionId, $info->getId() );
$this->assertNotNull( $info->getUserInfo() );
$this->assertFalse( $info->getUserInfo()->isVerified() );
$this->assertSame( $id, $info->getUserInfo()->getId() );
$this->assertSame( $name, $info->getUserInfo()->getName() );
$this->assertFalse( $info->forceHTTPS() );
$request = new \FauxRequest();
$request->setCookies( array(
'xUserID' => $id,
), '' );
$info = $provider->provideSessionInfo( $request );
$this->assertNull( $info );
// User and session key, with forceHTTPS flag
$request = new \FauxRequest();
$request->setCookies( array(
'session' => $sessionId,
'xUserID' => $id,
'xToken' => $token,
'forceHTTPS' => true,
), '' );
$info = $provider->provideSessionInfo( $request );
$this->assertNotNull( $info );
$this->assertSame( $params['priority'], $info->getPriority() );
$this->assertSame( $sessionId, $info->getId() );
$this->assertNotNull( $info->getUserInfo() );
$this->assertSame( $id, $info->getUserInfo()->getId() );
$this->assertSame( $name, $info->getUserInfo()->getName() );
$this->assertTrue( $info->forceHTTPS() );
// Invalid user id
$request = new \FauxRequest();
$request->setCookies( array(
'session' => $sessionId,
'xUserID' => '-1',
), '' );
$info = $provider->provideSessionInfo( $request );
$this->assertNull( $info );
// User id with matching name
$request = new \FauxRequest();
$request->setCookies( array(
'session' => $sessionId,
'xUserID' => $id,
'xUserName' => $name,
), '' );
$info = $provider->provideSessionInfo( $request );
$this->assertNotNull( $info );
$this->assertSame( $params['priority'], $info->getPriority() );
$this->assertSame( $sessionId, $info->getId() );
$this->assertNotNull( $info->getUserInfo() );
$this->assertFalse( $info->getUserInfo()->isVerified() );
$this->assertSame( $id, $info->getUserInfo()->getId() );
$this->assertSame( $name, $info->getUserInfo()->getName() );
$this->assertFalse( $info->forceHTTPS() );
// User id with wrong name
$request = new \FauxRequest();
$request->setCookies( array(
'session' => $sessionId,
'xUserID' => $id,
'xUserName' => 'Wrong',
), '' );
$info = $provider->provideSessionInfo( $request );
$this->assertNull( $info );
}
public function testGetVaryCookies() {
$provider = new CookieSessionProvider( array(
'priority' => 1,
'sessionName' => 'MySessionName',
'cookieOptions' => array( 'prefix' => 'MyCookiePrefix' ),
) );
$this->assertArrayEquals( array(
'MyCookiePrefixToken',
'MyCookiePrefixLoggedOut',
'MySessionName',
'forceHTTPS',
), $provider->getVaryCookies() );
}
public function testSuggestLoginUsername() {
$provider = new CookieSessionProvider( array(
'priority' => 1,
'sessionName' => 'MySessionName',
'cookieOptions' => array( 'prefix' => 'x' ),
) );
$request = new \FauxRequest();
$this->assertEquals( null, $provider->suggestLoginUsername( $request ) );
$request->setCookies( array(
'xUserName' => 'Example',
), '' );
$this->assertEquals( 'Example', $provider->suggestLoginUsername( $request ) );
}
public function testPersistSession() {
$this->setMwGlobals( array( 'wgCookieExpiration' => 100 ) );
$provider = new CookieSessionProvider( array(
'priority' => 1,
'sessionName' => 'MySessionName',
'callUserSetCookiesHook' => false,
'cookieOptions' => array( 'prefix' => 'x' ),
) );
$config = $this->getConfig();
$provider->setLogger( new \TestLogger() );
$provider->setConfig( $config );
$provider->setManager( SessionManager::singleton() );
$sessionId = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa';
$store = new \HashBagOStuff();
$user = User::newFromName( 'UTSysop' );
$anon = new User;
$backend = new SessionBackend(
new SessionId( $sessionId ),
new SessionInfo( SessionInfo::MIN_PRIORITY, array(
'provider' => $provider,
'id' => $sessionId,
'persisted' => true,
'idIsSafe' => true,
) ),
$store,
new \Psr\Log\NullLogger(),
10
);
\TestingAccessWrapper::newFromObject( $backend )->usePhpSessionHandling = false;
$mock = $this->getMock( 'stdClass', array( 'onUserSetCookies' ) );
$mock->expects( $this->never() )->method( 'onUserSetCookies' );
$this->mergeMwGlobalArrayValue( 'wgHooks', array( 'UserSetCookies' => array( $mock ) ) );
// Anonymous user
$backend->setUser( $anon );
$backend->setRememberUser( true );
$backend->setForceHTTPS( false );
$request = new \FauxRequest();
$provider->persistSession( $backend, $request );
$this->assertSame( $sessionId, $request->response()->getCookie( 'MySessionName' ) );
$this->assertSame( '', $request->response()->getCookie( 'xUserID' ) );
$this->assertSame( null, $request->response()->getCookie( 'xUserName' ) );
$this->assertSame( '', $request->response()->getCookie( 'xToken' ) );
$this->assertSame( null, $request->response()->getCookie( 'forceHTTPS' ) );
$this->assertSame( array(), $backend->getData() );
// Logged-in user, no remember
$backend->setUser( $user );
$backend->setRememberUser( false );
$backend->setForceHTTPS( false );
$request = new \FauxRequest();
$provider->persistSession( $backend, $request );
$this->assertSame( $sessionId, $request->response()->getCookie( 'MySessionName' ) );
$this->assertSame( (string)$user->getId(), $request->response()->getCookie( 'xUserID' ) );
$this->assertSame( $user->getName(), $request->response()->getCookie( 'xUserName' ) );
$this->assertSame( '', $request->response()->getCookie( 'xToken' ) );
$this->assertSame( null, $request->response()->getCookie( 'forceHTTPS' ) );
$this->assertSame( array(), $backend->getData() );
// Logged-in user, remember
$backend->setUser( $user );
$backend->setRememberUser( true );
$backend->setForceHTTPS( true );
$request = new \FauxRequest();
$time = time();
$provider->persistSession( $backend, $request );
$this->assertSame( $sessionId, $request->response()->getCookie( 'MySessionName' ) );
$this->assertSame( (string)$user->getId(), $request->response()->getCookie( 'xUserID' ) );
$this->assertSame( $user->getName(), $request->response()->getCookie( 'xUserName' ) );
$this->assertSame( $user->getToken(), $request->response()->getCookie( 'xToken' ) );
$this->assertSame( 'true', $request->response()->getCookie( 'forceHTTPS' ) );
$this->assertSame( array(), $backend->getData() );
}
/**
* @dataProvider provideCookieData
* @param bool $secure
* @param bool $remember
*/
public function testCookieData( $secure, $remember ) {
$this->setMwGlobals( array(
'wgCookieExpiration' => 100,
'wgSecureLogin' => false,
) );
$provider = new CookieSessionProvider( array(
'priority' => 1,
'sessionName' => 'MySessionName',
'callUserSetCookiesHook' => false,
'cookieOptions' => array( 'prefix' => 'x' ),
) );
$config = $this->getConfig();
$config->set( 'CookieSecure', false );
$provider->setLogger( new \TestLogger() );
$provider->setConfig( $config );
$provider->setManager( SessionManager::singleton() );
$sessionId = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa';
$user = User::newFromName( 'UTSysop' );
$this->assertFalse( $user->requiresHTTPS(), 'sanity check' );
$backend = new SessionBackend(
new SessionId( $sessionId ),
new SessionInfo( SessionInfo::MIN_PRIORITY, array(
'provider' => $provider,
'id' => $sessionId,
'persisted' => true,
'idIsSafe' => true,
) ),
new \EmptyBagOStuff(),
new \Psr\Log\NullLogger(),
10
);
\TestingAccessWrapper::newFromObject( $backend )->usePhpSessionHandling = false;
$backend->setUser( $user );
$backend->setRememberUser( $remember );
$backend->setForceHTTPS( $secure );
$request = new \FauxRequest();
$time = time();
$provider->persistSession( $backend, $request );
$defaults = array(
'expire' => (int)100,
'path' => $config->get( 'CookiePath' ),
'domain' => $config->get( 'CookieDomain' ),
'secure' => $secure,
'httpOnly' => $config->get( 'CookieHttpOnly' ),
'raw' => false,
);
$extendedExpiry = $config->get( 'ExtendedLoginCookieExpiration' );
$extendedExpiry = (int)( $extendedExpiry === null ? 0 : $extendedExpiry );
$this->assertEquals( array( 'UserID', 'Token' ), $config->get( 'ExtendedLoginCookies' ),
'sanity check' );
$expect = array(
'MySessionName' => array(
'value' => (string)$sessionId,
'expire' => 0,
) + $defaults,
'xUserID' => array(
'value' => (string)$user->getId(),
'expire' => $extendedExpiry,
) + $defaults,
'xUserName' => array(
'value' => $user->getName(),
) + $defaults,
'xToken' => array(
'value' => $remember ? $user->getToken() : '',
'expire' => $remember ? $extendedExpiry : -31536000,
) + $defaults,
'forceHTTPS' => !$secure ? null : array(
'value' => 'true',
'secure' => false,
'expire' => $remember ? $defaults['expire'] : null,
) + $defaults,
);
foreach ( $expect as $key => $value ) {
$actual = $request->response()->getCookieData( $key );
if ( $actual && $actual['expire'] > 0 ) {
// Round expiry so we don't randomly fail if the seconds ticked during the test.
$actual['expire'] = round( $actual['expire'] - $time, -2 );
}
$this->assertEquals( $value, $actual, "Cookie $key" );
}
}
public static function provideCookieData() {
return array(
array( false, false ),
array( false, true ),
array( true, false ),
array( true, true ),
);
}
protected function getSentRequest() {
$sentResponse = $this->getMock( 'FauxResponse', array( 'headersSent', 'setCookie', 'header' ) );
$sentResponse->expects( $this->any() )->method( 'headersSent' )
->will( $this->returnValue( true ) );
$sentResponse->expects( $this->never() )->method( 'setCookie' );
$sentResponse->expects( $this->never() )->method( 'header' );
$sentRequest = $this->getMock( 'FauxRequest', array( 'response' ) );
$sentRequest->expects( $this->any() )->method( 'response' )
->will( $this->returnValue( $sentResponse ) );
return $sentRequest;
}
public function testPersistSessionWithHook() {
$that = $this;
$provider = new CookieSessionProvider( array(
'priority' => 1,
'sessionName' => 'MySessionName',
'callUserSetCookiesHook' => true,
'cookieOptions' => array( 'prefix' => 'x' ),
) );
$provider->setLogger( new \Psr\Log\NullLogger() );
$provider->setConfig( $this->getConfig() );
$provider->setManager( SessionManager::singleton() );
$sessionId = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa';
$store = new \HashBagOStuff();
$user = User::newFromName( 'UTSysop' );
$anon = new User;
$backend = new SessionBackend(
new SessionId( $sessionId ),
new SessionInfo( SessionInfo::MIN_PRIORITY, array(
'provider' => $provider,
'id' => $sessionId,
'persisted' => true,
'idIsSafe' => true,
) ),
$store,
new \Psr\Log\NullLogger(),
10
);
\TestingAccessWrapper::newFromObject( $backend )->usePhpSessionHandling = false;
// Anonymous user
$mock = $this->getMock( 'stdClass', array( 'onUserSetCookies' ) );
$mock->expects( $this->never() )->method( 'onUserSetCookies' );
$this->mergeMwGlobalArrayValue( 'wgHooks', array( 'UserSetCookies' => array( $mock ) ) );
$backend->setUser( $anon );
$backend->setRememberUser( true );
$backend->setForceHTTPS( false );
$request = new \FauxRequest();
$provider->persistSession( $backend, $request );
$this->assertSame( $sessionId, $request->response()->getCookie( 'MySessionName' ) );
$this->assertSame( '', $request->response()->getCookie( 'xUserID' ) );
$this->assertSame( null, $request->response()->getCookie( 'xUserName' ) );
$this->assertSame( '', $request->response()->getCookie( 'xToken' ) );
$this->assertSame( null, $request->response()->getCookie( 'forceHTTPS' ) );
$this->assertSame( array(), $backend->getData() );
$provider->persistSession( $backend, $this->getSentRequest() );
// Logged-in user, no remember
$mock = $this->getMock( __CLASS__, array( 'onUserSetCookies' ) );
$mock->expects( $this->once() )->method( 'onUserSetCookies' )
->will( $this->returnCallback( function ( $u, &$sessionData, &$cookies ) use ( $that, $user ) {
$that->assertSame( $user, $u );
$that->assertEquals( array(
'wsUserID' => $user->getId(),
'wsUserName' => $user->getName(),
'wsToken' => $user->getToken(),
), $sessionData );
$that->assertEquals( array(
'UserID' => $user->getId(),
'UserName' => $user->getName(),
'Token' => false,
), $cookies );
$sessionData['foo'] = 'foo!';
$cookies['bar'] = 'bar!';
return true;
} ) );
$this->mergeMwGlobalArrayValue( 'wgHooks', array( 'UserSetCookies' => array( $mock ) ) );
$backend->setUser( $user );
$backend->setRememberUser( false );
$backend->setForceHTTPS( false );
$backend->setLoggedOutTimestamp( $loggedOut = time() );
$request = new \FauxRequest();
$provider->persistSession( $backend, $request );
$this->assertSame( $sessionId, $request->response()->getCookie( 'MySessionName' ) );
$this->assertSame( (string)$user->getId(), $request->response()->getCookie( 'xUserID' ) );
$this->assertSame( $user->getName(), $request->response()->getCookie( 'xUserName' ) );
$this->assertSame( '', $request->response()->getCookie( 'xToken' ) );
$this->assertSame( null, $request->response()->getCookie( 'forceHTTPS' ) );
$this->assertSame( 'bar!', $request->response()->getCookie( 'xbar' ) );
$this->assertSame( (string)$loggedOut, $request->response()->getCookie( 'xLoggedOut' ) );
$this->assertEquals( array(
'wsUserID' => $user->getId(),
'wsUserName' => $user->getName(),
'wsToken' => $user->getToken(),
'foo' => 'foo!',
), $backend->getData() );
$provider->persistSession( $backend, $this->getSentRequest() );
// Logged-in user, remember
$mock = $this->getMock( __CLASS__, array( 'onUserSetCookies' ) );
$mock->expects( $this->once() )->method( 'onUserSetCookies' )
->will( $this->returnCallback( function ( $u, &$sessionData, &$cookies ) use ( $that, $user ) {
$that->assertSame( $user, $u );
$that->assertEquals( array(
'wsUserID' => $user->getId(),
'wsUserName' => $user->getName(),
'wsToken' => $user->getToken(),
), $sessionData );
$that->assertEquals( array(
'UserID' => $user->getId(),
'UserName' => $user->getName(),
'Token' => $user->getToken(),
), $cookies );
$sessionData['foo'] = 'foo 2!';
$cookies['bar'] = 'bar 2!';
return true;
} ) );
$this->mergeMwGlobalArrayValue( 'wgHooks', array( 'UserSetCookies' => array( $mock ) ) );
$backend->setUser( $user );
$backend->setRememberUser( true );
$backend->setForceHTTPS( true );
$backend->setLoggedOutTimestamp( 0 );
$request = new \FauxRequest();
$provider->persistSession( $backend, $request );
$this->assertSame( $sessionId, $request->response()->getCookie( 'MySessionName' ) );
$this->assertSame( (string)$user->getId(), $request->response()->getCookie( 'xUserID' ) );
$this->assertSame( $user->getName(), $request->response()->getCookie( 'xUserName' ) );
$this->assertSame( $user->getToken(), $request->response()->getCookie( 'xToken' ) );
$this->assertSame( 'true', $request->response()->getCookie( 'forceHTTPS' ) );
$this->assertSame( 'bar 2!', $request->response()->getCookie( 'xbar' ) );
$this->assertSame( null, $request->response()->getCookie( 'xLoggedOut' ) );
$this->assertEquals( array(
'wsUserID' => $user->getId(),
'wsUserName' => $user->getName(),
'wsToken' => $user->getToken(),
'foo' => 'foo 2!',
), $backend->getData() );
$provider->persistSession( $backend, $this->getSentRequest() );
}
public function testUnpersistSession() {
$provider = new CookieSessionProvider( array(
'priority' => 1,
'sessionName' => 'MySessionName',
'cookieOptions' => array( 'prefix' => 'x' ),
) );
$provider->setLogger( new \Psr\Log\NullLogger() );
$provider->setConfig( $this->getConfig() );
$provider->setManager( SessionManager::singleton() );
$request = new \FauxRequest();
$provider->unpersistSession( $request );
$this->assertSame( '', $request->response()->getCookie( 'MySessionName' ) );
$this->assertSame( '', $request->response()->getCookie( 'xUserID' ) );
$this->assertSame( null, $request->response()->getCookie( 'xUserName' ) );
$this->assertSame( '', $request->response()->getCookie( 'xToken' ) );
$this->assertSame( '', $request->response()->getCookie( 'forceHTTPS' ) );
$provider->unpersistSession( $this->getSentRequest() );
}
public function testSetLoggedOutCookie() {
$provider = \TestingAccessWrapper::newFromObject( new CookieSessionProvider( array(
'priority' => 1,
'sessionName' => 'MySessionName',
'cookieOptions' => array( 'prefix' => 'x' ),
) ) );
$provider->setLogger( new \Psr\Log\NullLogger() );
$provider->setConfig( $this->getConfig() );
$provider->setManager( SessionManager::singleton() );
$t1 = time();
$t2 = time() - 86400 * 2;
// Set it
$request = new \FauxRequest();
$provider->setLoggedOutCookie( $t1, $request );
$this->assertSame( (string)$t1, $request->response()->getCookie( 'xLoggedOut' ) );
// Too old
$request = new \FauxRequest();
$provider->setLoggedOutCookie( $t2, $request );
$this->assertSame( null, $request->response()->getCookie( 'xLoggedOut' ) );
// Don't reset if it's already set
$request = new \FauxRequest();
$request->setCookies( array(
'xLoggedOut' => $t1,
), '' );
$provider->setLoggedOutCookie( $t1, $request );
$this->assertSame( null, $request->response()->getCookie( 'xLoggedOut' ) );
}
/**
* To be mocked for hooks, since PHPUnit can't otherwise mock methods that
* take references.
*/
public function onUserSetCookies( $user, &$sessionData, &$cookies ) {
}
}

View file

@ -0,0 +1,301 @@
<?php
namespace MediaWiki\Session;
use MediaWikiTestCase;
use User;
/**
* @group Session
* @group Database
* @covers MediaWiki\Session\ImmutableSessionProviderWithCookie
*/
class ImmutableSessionProviderWithCookieTest extends MediaWikiTestCase {
private function getProvider( $name, $prefix = null ) {
$config = new \HashConfig();
$config->set( 'CookiePrefix', 'wgCookiePrefix' );
$params = array(
'sessionCookieName' => $name,
'sessionCookieOptions' => array(),
);
if ( $prefix !== null ) {
$params['sessionCookieOptions']['prefix'] = $prefix;
}
$provider = $this->getMockBuilder( 'MediaWiki\\Session\\ImmutableSessionProviderWithCookie' )
->setConstructorArgs( array( $params ) )
->getMockForAbstractClass();
$provider->setLogger( new \TestLogger() );
$provider->setConfig( $config );
$provider->setManager( new SessionManager() );
return $provider;
}
public function testConstructor() {
$provider = $this->getMockBuilder( 'MediaWiki\\Session\\ImmutableSessionProviderWithCookie' )
->getMockForAbstractClass();
$priv = \TestingAccessWrapper::newFromObject( $provider );
$this->assertNull( $priv->sessionCookieName );
$this->assertSame( array(), $priv->sessionCookieOptions );
$provider = $this->getMockBuilder( 'MediaWiki\\Session\\ImmutableSessionProviderWithCookie' )
->setConstructorArgs( array( array(
'sessionCookieName' => 'Foo',
'sessionCookieOptions' => array( 'Bar' ),
) ) )
->getMockForAbstractClass();
$priv = \TestingAccessWrapper::newFromObject( $provider );
$this->assertSame( 'Foo', $priv->sessionCookieName );
$this->assertSame( array( 'Bar' ), $priv->sessionCookieOptions );
try {
$provider = $this->getMockBuilder( 'MediaWiki\\Session\\ImmutableSessionProviderWithCookie' )
->setConstructorArgs( array( array(
'sessionCookieName' => false,
) ) )
->getMockForAbstractClass();
$this->fail( 'Expected exception not thrown' );
} catch ( \InvalidArgumentException $ex ) {
$this->assertSame(
'sessionCookieName must be a string',
$ex->getMessage()
);
}
try {
$provider = $this->getMockBuilder( 'MediaWiki\\Session\\ImmutableSessionProviderWithCookie' )
->setConstructorArgs( array( array(
'sessionCookieOptions' => 'x',
) ) )
->getMockForAbstractClass();
$this->fail( 'Expected exception not thrown' );
} catch ( \InvalidArgumentException $ex ) {
$this->assertSame(
'sessionCookieOptions must be an array',
$ex->getMessage()
);
}
}
public function testBasics() {
$provider = $this->getProvider( null );
$this->assertFalse( $provider->persistsSessionID() );
$this->assertFalse( $provider->canChangeUser() );
$provider = $this->getProvider( 'Foo' );
$this->assertTrue( $provider->persistsSessionID() );
$this->assertFalse( $provider->canChangeUser() );
$msg = $provider->whyNoSession();
$this->assertInstanceOf( 'Message', $msg );
$this->assertSame( 'sessionprovider-nocookies', $msg->getKey() );
}
public function testGetVaryCookies() {
$provider = $this->getProvider( null );
$this->assertSame( array(), $provider->getVaryCookies() );
$provider = $this->getProvider( 'Foo' );
$this->assertSame( array( 'wgCookiePrefixFoo' ), $provider->getVaryCookies() );
$provider = $this->getProvider( 'Foo', 'Bar' );
$this->assertSame( array( 'BarFoo' ), $provider->getVaryCookies() );
$provider = $this->getProvider( 'Foo', '' );
$this->assertSame( array( 'Foo' ), $provider->getVaryCookies() );
}
public function testGetSessionIdFromCookie() {
$this->setMwGlobals( 'wgCookiePrefix', 'wgCookiePrefix' );
$request = new \FauxRequest();
$request->setCookies( array(
'' => 'empty---------------------------',
'Foo' => 'foo-----------------------------',
'wgCookiePrefixFoo' => 'wgfoo---------------------------',
'BarFoo' => 'foobar--------------------------',
'bad' => 'bad',
), '' );
$provider = \TestingAccessWrapper::newFromObject( $this->getProvider( null ) );
try {
$provider->getSessionIdFromCookie( $request );
$this->fail( 'Expected exception not thrown' );
} catch ( \BadMethodCallException $ex ) {
$this->assertSame(
'MediaWiki\\Session\\ImmutableSessionProviderWithCookie::getSessionIdFromCookie ' .
'may not be called when $this->sessionCookieName === null',
$ex->getMessage()
);
}
$provider = \TestingAccessWrapper::newFromObject( $this->getProvider( 'Foo' ) );
$this->assertSame(
'wgfoo---------------------------',
$provider->getSessionIdFromCookie( $request )
);
$provider = \TestingAccessWrapper::newFromObject( $this->getProvider( 'Foo', 'Bar' ) );
$this->assertSame(
'foobar--------------------------',
$provider->getSessionIdFromCookie( $request )
);
$provider = \TestingAccessWrapper::newFromObject( $this->getProvider( 'Foo', '' ) );
$this->assertSame(
'foo-----------------------------',
$provider->getSessionIdFromCookie( $request )
);
$provider = \TestingAccessWrapper::newFromObject( $this->getProvider( 'bad', '' ) );
$this->assertSame( null, $provider->getSessionIdFromCookie( $request ) );
$provider = \TestingAccessWrapper::newFromObject( $this->getProvider( 'none', '' ) );
$this->assertSame( null, $provider->getSessionIdFromCookie( $request ) );
}
protected function getSentRequest() {
$sentResponse = $this->getMock( 'FauxResponse', array( 'headersSent', 'setCookie', 'header' ) );
$sentResponse->expects( $this->any() )->method( 'headersSent' )
->will( $this->returnValue( true ) );
$sentResponse->expects( $this->never() )->method( 'setCookie' );
$sentResponse->expects( $this->never() )->method( 'header' );
$sentRequest = $this->getMock( 'FauxRequest', array( 'response' ) );
$sentRequest->expects( $this->any() )->method( 'response' )
->will( $this->returnValue( $sentResponse ) );
return $sentRequest;
}
/**
* @dataProvider providePersistSession
* @param bool $secure
* @param bool $remember
*/
public function testPersistSession( $secure, $remember ) {
$this->setMwGlobals( array(
'wgCookieExpiration' => 100,
'wgSecureLogin' => false,
) );
$provider = $this->getProvider( 'session' );
$provider->setLogger( new \Psr\Log\NullLogger() );
$priv = \TestingAccessWrapper::newFromObject( $provider );
$priv->sessionCookieOptions = array(
'prefix' => 'x',
'path' => 'CookiePath',
'domain' => 'CookieDomain',
'secure' => false,
'httpOnly' => true,
);
$sessionId = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa';
$user = User::newFromName( 'UTSysop' );
$this->assertFalse( $user->requiresHTTPS(), 'sanity check' );
$backend = new SessionBackend(
new SessionId( $sessionId ),
new SessionInfo( SessionInfo::MIN_PRIORITY, array(
'provider' => $provider,
'id' => $sessionId,
'persisted' => true,
'userInfo' => UserInfo::newFromUser( $user, true ),
'idIsSafe' => true,
) ),
new \EmptyBagOStuff(),
new \Psr\Log\NullLogger(),
10
);
\TestingAccessWrapper::newFromObject( $backend )->usePhpSessionHandling = false;
$backend->setRememberUser( $remember );
$backend->setForceHTTPS( $secure );
// No cookie
$priv->sessionCookieName = null;
$request = new \FauxRequest();
$provider->persistSession( $backend, $request );
$this->assertSame( array(), $request->response()->getCookies() );
// Cookie
$priv->sessionCookieName = 'session';
$request = new \FauxRequest();
$time = time();
$provider->persistSession( $backend, $request );
$cookie = $request->response()->getCookieData( 'xsession' );
$this->assertInternalType( 'array', $cookie );
if ( isset( $cookie['expire'] ) && $cookie['expire'] > 0 ) {
// Round expiry so we don't randomly fail if the seconds ticked during the test.
$cookie['expire'] = round( $cookie['expire'] - $time, -2 );
}
$this->assertEquals( array(
'value' => $sessionId,
'expire' => null,
'path' => 'CookiePath',
'domain' => 'CookieDomain',
'secure' => $secure,
'httpOnly' => true,
'raw' => false,
), $cookie );
$cookie = $request->response()->getCookieData( 'forceHTTPS' );
if ( $secure ) {
$this->assertInternalType( 'array', $cookie );
if ( isset( $cookie['expire'] ) && $cookie['expire'] > 0 ) {
// Round expiry so we don't randomly fail if the seconds ticked during the test.
$cookie['expire'] = round( $cookie['expire'] - $time, -2 );
}
$this->assertEquals( array(
'value' => 'true',
'expire' => $remember ? 100 : null,
'path' => 'CookiePath',
'domain' => 'CookieDomain',
'secure' => false,
'httpOnly' => true,
'raw' => false,
), $cookie );
} else {
$this->assertNull( $cookie );
}
// Headers sent
$request = $this->getSentRequest();
$provider->persistSession( $backend, $request );
$this->assertSame( array(), $request->response()->getCookies() );
}
public static function providePersistSession() {
return array(
array( false, false ),
array( false, true ),
array( true, false ),
array( true, true ),
);
}
public function testUnpersistSession() {
$provider = $this->getProvider( 'session', '' );
$provider->setLogger( new \Psr\Log\NullLogger() );
$priv = \TestingAccessWrapper::newFromObject( $provider );
// No cookie
$priv->sessionCookieName = null;
$request = new \FauxRequest();
$provider->unpersistSession( $request );
$this->assertSame( null, $request->response()->getCookie( 'session', '' ) );
// Cookie
$priv->sessionCookieName = 'session';
$request = new \FauxRequest();
$provider->unpersistSession( $request );
$this->assertSame( '', $request->response()->getCookie( 'session', '' ) );
// Headers sent
$request = $this->getSentRequest();
$provider->unpersistSession( $request );
$this->assertSame( null, $request->response()->getCookie( 'session', '' ) );
}
}

View file

@ -0,0 +1,353 @@
<?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 = array();
// 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( 'MediaWiki\\Session\\PHPSessionHandler', 'instance' );
$rProp->setAccessible( true );
if ( $rProp->getValue() ) {
$old = \TestingAccessWrapper::newFromObject( $rProp->getValue() );
$oldManager = $old->manager;
$oldStore = $old->store;
$oldLogger = $old->logger;
$reset[] = new \ScopedCallback(
array( 'MediaWiki\\Session\\PHPSessionHandler', 'install' ),
array( $oldManager, $oldStore, $oldLogger )
);
}
return $reset;
}
public function testEnableFlags() {
$handler = \TestingAccessWrapper::newFromObject(
$this->getMockBuilder( 'MediaWiki\\Session\\PHPSessionHandler' )
->setMethods( null )
->disableOriginalConstructor()
->getMock()
);
$rProp = new \ReflectionProperty( 'MediaWiki\\Session\\PHPSessionHandler', 'instance' );
$rProp->setAccessible( true );
$reset = new \ScopedCallback( array( $rProp, 'setValue' ), array( $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 \HashBagOStuff();
$logger = new \TestLogger();
$manager = new SessionManager( array(
'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( array(
'wgSessionProviders' => array( array( 'class' => 'DummySessionProvider' ) ),
'wgObjectCacheSessionExpiry' => 2,
) );
$store = new \HashBagOStuff();
$logger = new \TestLogger( true, function ( $m ) {
return preg_match( '/^SessionBackend a{32} /', $m ) ? null : $m;
} );
$manager = new SessionManager( array(
'store' => $store,
'logger' => $logger,
) );
PHPSessionHandler::install( $manager );
$wrap = \TestingAccessWrapper::newFromObject( $rProp->getValue() );
$reset[] = new \ScopedCallback(
array( $wrap, 'setEnableFlags' ),
array( $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( array(), $_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 = array( 'AuthenticationSessionTest' => $rand );
session_write_close();
$this->assertSame( array(
array( 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 );
}
// Test expiry
session_write_close();
ini_set( 'session.gc_divisor', 1 );
ini_set( 'session.gc_probability', 1 );
sleep( 3 );
session_start();
$this->assertSame( array(), $_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( array(), $_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( array(), $_SESSION );
$_SESSION['id'] = 'C';
session_write_close();
session_id( $sessionB );
session_start();
$this->assertSame( array( 'id' => 'B' ), $_SESSION );
session_write_close();
session_id( $sessionC );
session_start();
$this->assertSame( array( 'id' => 'C' ), $_SESSION );
session_destroy();
session_id( $sessionB );
session_start();
$this->assertSame( array( 'id' => 'B' ), $_SESSION );
// Test merging between Session and $_SESSION
session_write_close();
$session = $manager->getEmptySession();
$session->set( 'Unchanged', 'setup' );
$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( array(
'Added in Session' => 'Session',
'Added in $_SESSION' => '$_SESSION',
'Added in both' => 'Session',
'Unchanged' => 'setup',
'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( array(
42 => 'forty-two',
'forty-two' => 42,
), iterator_to_array( $session ) );
}
public static function provideHandlers() {
return array(
array( 'php' ),
array( 'php_binary' ),
array( 'php_serialize' ),
);
}
/**
* @dataProvider provideDisabled
* @expectedException BadMethodCallException
* @expectedExceptionMessage Attempt to use PHP session management
*/
public function testDisabled( $method, $args ) {
$rProp = new \ReflectionProperty( 'MediaWiki\\Session\\PHPSessionHandler', 'instance' );
$rProp->setAccessible( true );
$handler = $this->getMockBuilder( 'MediaWiki\\Session\\PHPSessionHandler' )
->setMethods( null )
->disableOriginalConstructor()
->getMock();
\TestingAccessWrapper::newFromObject( $handler )->setEnableFlags( 'disable' );
$oldValue = $rProp->getValue();
$rProp->setValue( $handler );
$reset = new \ScopedCallback( array( $rProp, 'setValue' ), array( $oldValue ) );
call_user_func_array( array( $handler, $method ), $args );
}
public static function provideDisabled() {
return array(
array( 'open', array( '', '' ) ),
array( 'read', array( '' ) ),
array( 'write', array( '', '' ) ),
array( 'destroy', array( '' ) ),
);
}
/**
* @dataProvider provideWrongInstance
* @expectedException UnexpectedValueException
* @expectedExceptionMessageRegExp /: Wrong instance called!$/
*/
public function testWrongInstance( $method, $args ) {
$handler = $this->getMockBuilder( 'MediaWiki\\Session\\PHPSessionHandler' )
->setMethods( null )
->disableOriginalConstructor()
->getMock();
\TestingAccessWrapper::newFromObject( $handler )->setEnableFlags( 'enable' );
call_user_func_array( array( $handler, $method ), $args );
}
public static function provideWrongInstance() {
return array(
array( 'open', array( '', '' ) ),
array( 'close', array() ),
array( 'read', array( '' ) ),
array( 'write', array( '', '' ) ),
array( 'destroy', array( '' ) ),
array( 'gc', array( 0 ) ),
);
}
}

View file

@ -0,0 +1,746 @@
<?php
namespace MediaWiki\Session;
use MediaWikiTestCase;
use User;
/**
* @group Session
* @group Database
* @covers MediaWiki\Session\SessionBackend
*/
class SessionBackendTest extends MediaWikiTestCase {
const SESSIONID = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa';
protected $manager;
protected $config;
protected $provider;
protected $store;
protected $onSessionMetadataCalled = false;
/**
* Returns a non-persistent backend that thinks it has at least one session active
* @param User|null $user
*/
protected function getBackend( User $user = null ) {
if ( !$this->config ) {
$this->config = new \HashConfig();
$this->manager = null;
}
if ( !$this->store ) {
$this->store = new TestBagOStuff();
$this->manager = null;
}
$logger = new \Psr\Log\NullLogger();
if ( !$this->manager ) {
$this->manager = new SessionManager( array(
'store' => $this->store,
'logger' => $logger,
'config' => $this->config,
) );
}
if ( !$this->provider ) {
$this->provider = new \DummySessionProvider();
}
$this->provider->setLogger( $logger );
$this->provider->setConfig( $this->config );
$this->provider->setManager( $this->manager );
$info = new SessionInfo( SessionInfo::MIN_PRIORITY, array(
'provider' => $this->provider,
'id' => self::SESSIONID,
'persisted' => true,
'userInfo' => UserInfo::newFromUser( $user ?: new User, true ),
'idIsSafe' => true,
) );
$id = new SessionId( $info->getId() );
$backend = new SessionBackend( $id, $info, $this->store, $logger, 10 );
$priv = \TestingAccessWrapper::newFromObject( $backend );
$priv->persist = false;
$priv->requests = array( 100 => new \FauxRequest() );
$priv->usePhpSessionHandling = false;
$manager = \TestingAccessWrapper::newFromObject( $this->manager );
$manager->allSessionBackends = array( $backend->getId() => $backend );
$manager->allSessionIds = array( $backend->getId() => $id );
$manager->sessionProviders = array( (string)$this->provider => $this->provider );
return $backend;
}
public function testConstructor() {
// Set variables
$this->getBackend();
$info = new SessionInfo( SessionInfo::MIN_PRIORITY, array(
'provider' => $this->provider,
'id' => self::SESSIONID,
'persisted' => true,
'userInfo' => UserInfo::newFromName( 'UTSysop', false ),
'idIsSafe' => true,
) );
$id = new SessionId( $info->getId() );
$logger = new \Psr\Log\NullLogger();
try {
new SessionBackend( $id, $info, $this->store, $logger, 10 );
$this->fail( 'Expected exception not thrown' );
} catch ( \InvalidArgumentException $ex ) {
$this->assertSame(
"Refusing to create session for unverified user {$info->getUserInfo()}",
$ex->getMessage()
);
}
$info = new SessionInfo( SessionInfo::MIN_PRIORITY, array(
'id' => self::SESSIONID,
'userInfo' => UserInfo::newFromName( 'UTSysop', true ),
'idIsSafe' => true,
) );
$id = new SessionId( $info->getId() );
try {
new SessionBackend( $id, $info, $this->store, $logger, 10 );
$this->fail( 'Expected exception not thrown' );
} catch ( \InvalidArgumentException $ex ) {
$this->assertSame( 'Cannot create session without a provider', $ex->getMessage() );
}
$info = new SessionInfo( SessionInfo::MIN_PRIORITY, array(
'provider' => $this->provider,
'id' => self::SESSIONID,
'persisted' => true,
'userInfo' => UserInfo::newFromName( 'UTSysop', true ),
'idIsSafe' => true,
) );
$id = new SessionId( '!' . $info->getId() );
try {
new SessionBackend( $id, $info, $this->store, $logger, 10 );
$this->fail( 'Expected exception not thrown' );
} catch ( \InvalidArgumentException $ex ) {
$this->assertSame(
'SessionId and SessionInfo don\'t match',
$ex->getMessage()
);
}
$info = new SessionInfo( SessionInfo::MIN_PRIORITY, array(
'provider' => $this->provider,
'id' => self::SESSIONID,
'persisted' => true,
'userInfo' => UserInfo::newFromName( 'UTSysop', true ),
'idIsSafe' => true,
) );
$id = new SessionId( $info->getId() );
$backend = new SessionBackend( $id, $info, $this->store, $logger, 10 );
$this->assertSame( self::SESSIONID, $backend->getId() );
$this->assertSame( $id, $backend->getSessionId() );
$this->assertSame( $this->provider, $backend->getProvider() );
$this->assertInstanceOf( 'User', $backend->getUser() );
$this->assertSame( 'UTSysop', $backend->getUser()->getName() );
$this->assertSame( $info->wasPersisted(), $backend->isPersistent() );
$this->assertSame( $info->wasRemembered(), $backend->shouldRememberUser() );
$this->assertSame( $info->forceHTTPS(), $backend->shouldForceHTTPS() );
$expire = time() + 100;
$this->store->setSessionMeta( self::SESSIONID, array( 'expires' => $expire ), 2 );
$info = new SessionInfo( SessionInfo::MIN_PRIORITY, array(
'provider' => $this->provider,
'id' => self::SESSIONID,
'persisted' => true,
'forceHTTPS' => true,
'metadata' => array( 'foo' ),
'idIsSafe' => true,
) );
$id = new SessionId( $info->getId() );
$backend = new SessionBackend( $id, $info, $this->store, $logger, 10 );
$this->assertSame( self::SESSIONID, $backend->getId() );
$this->assertSame( $id, $backend->getSessionId() );
$this->assertSame( $this->provider, $backend->getProvider() );
$this->assertInstanceOf( 'User', $backend->getUser() );
$this->assertTrue( $backend->getUser()->isAnon() );
$this->assertSame( $info->wasPersisted(), $backend->isPersistent() );
$this->assertSame( $info->wasRemembered(), $backend->shouldRememberUser() );
$this->assertSame( $info->forceHTTPS(), $backend->shouldForceHTTPS() );
$this->assertSame( $expire, \TestingAccessWrapper::newFromObject( $backend )->expires );
$this->assertSame( array( 'foo' ), $backend->getProviderMetadata() );
}
public function testSessionStuff() {
$backend = $this->getBackend();
$priv = \TestingAccessWrapper::newFromObject( $backend );
$priv->requests = array(); // Remove dummy session
$manager = \TestingAccessWrapper::newFromObject( $this->manager );
$request1 = new \FauxRequest();
$session1 = $backend->getSession( $request1 );
$request2 = new \FauxRequest();
$session2 = $backend->getSession( $request2 );
$this->assertInstanceOf( 'MediaWiki\\Session\\Session', $session1 );
$this->assertInstanceOf( 'MediaWiki\\Session\\Session', $session2 );
$this->assertSame( 2, count( $priv->requests ) );
$index = \TestingAccessWrapper::newFromObject( $session1 )->index;
$this->assertSame( $request1, $backend->getRequest( $index ) );
$this->assertSame( null, $backend->suggestLoginUsername( $index ) );
$request1->setCookie( 'UserName', 'Example' );
$this->assertSame( 'Example', $backend->suggestLoginUsername( $index ) );
$session1 = null;
$this->assertSame( 1, count( $priv->requests ) );
$this->assertArrayHasKey( $backend->getId(), $manager->allSessionBackends );
$this->assertSame( $backend, $manager->allSessionBackends[$backend->getId()] );
try {
$backend->getRequest( $index );
$this->fail( 'Expected exception not thrown' );
} catch ( \InvalidArgumentException $ex ) {
$this->assertSame( 'Invalid session index', $ex->getMessage() );
}
try {
$backend->suggestLoginUsername( $index );
$this->fail( 'Expected exception not thrown' );
} catch ( \InvalidArgumentException $ex ) {
$this->assertSame( 'Invalid session index', $ex->getMessage() );
}
$session2 = null;
$this->assertSame( 0, count( $priv->requests ) );
$this->assertArrayNotHasKey( $backend->getId(), $manager->allSessionBackends );
$this->assertArrayHasKey( $backend->getId(), $manager->allSessionIds );
}
public function testResetId() {
$id = session_id();
$builder = $this->getMockBuilder( 'DummySessionProvider' )
->setMethods( array( 'persistsSessionId', 'sessionIdWasReset' ) );
$this->provider = $builder->getMock();
$this->provider->expects( $this->any() )->method( 'persistsSessionId' )
->will( $this->returnValue( false ) );
$this->provider->expects( $this->never() )->method( 'sessionIdWasReset' );
$backend = $this->getBackend( User::newFromName( 'UTSysop' ) );
$manager = \TestingAccessWrapper::newFromObject( $this->manager );
$sessionId = $backend->getSessionId();
$backend->resetId();
$this->assertSame( self::SESSIONID, $backend->getId() );
$this->assertSame( $backend->getId(), $sessionId->getId() );
$this->assertSame( $id, session_id() );
$this->assertSame( $backend, $manager->allSessionBackends[self::SESSIONID] );
$this->provider = $builder->getMock();
$this->provider->expects( $this->any() )->method( 'persistsSessionId' )
->will( $this->returnValue( true ) );
$backend = $this->getBackend();
$this->provider->expects( $this->once() )->method( 'sessionIdWasReset' )
->with( $this->identicalTo( $backend ), $this->identicalTo( self::SESSIONID ) );
$manager = \TestingAccessWrapper::newFromObject( $this->manager );
$sessionId = $backend->getSessionId();
$backend->resetId();
$this->assertNotEquals( self::SESSIONID, $backend->getId() );
$this->assertSame( $backend->getId(), $sessionId->getId() );
$this->assertInternalType( 'array', $this->store->getSession( $backend->getId() ) );
$this->assertFalse( $this->store->getSession( self::SESSIONID ) );
$this->assertSame( $id, session_id() );
$this->assertArrayNotHasKey( self::SESSIONID, $manager->allSessionBackends );
$this->assertArrayHasKey( $backend->getId(), $manager->allSessionBackends );
$this->assertSame( $backend, $manager->allSessionBackends[$backend->getId()] );
}
public function testPersist() {
$this->provider = $this->getMock( 'DummySessionProvider', array( 'persistSession' ) );
$this->provider->expects( $this->once() )->method( 'persistSession' );
$backend = $this->getBackend();
$this->assertFalse( $backend->isPersistent(), 'sanity check' );
$backend->save(); // This one shouldn't call $provider->persistSession()
$backend->persist();
$this->assertTrue( $backend->isPersistent(), 'sanity check' );
$this->provider = null;
$backend = $this->getBackend();
$wrap = \TestingAccessWrapper::newFromObject( $backend );
$wrap->persist = true;
$wrap->expires = 0;
$backend->persist();
$this->assertNotEquals( 0, $wrap->expires );
}
public function testRememberUser() {
$backend = $this->getBackend();
$remembered = $backend->shouldRememberUser();
$backend->setRememberUser( !$remembered );
$this->assertNotEquals( $remembered, $backend->shouldRememberUser() );
$backend->setRememberUser( $remembered );
$this->assertEquals( $remembered, $backend->shouldRememberUser() );
}
public function testForceHTTPS() {
$backend = $this->getBackend();
$force = $backend->shouldForceHTTPS();
$backend->setForceHTTPS( !$force );
$this->assertNotEquals( $force, $backend->shouldForceHTTPS() );
$backend->setForceHTTPS( $force );
$this->assertEquals( $force, $backend->shouldForceHTTPS() );
}
public function testLoggedOutTimestamp() {
$backend = $this->getBackend();
$backend->setLoggedOutTimestamp( 42 );
$this->assertSame( 42, $backend->getLoggedOutTimestamp() );
$backend->setLoggedOutTimestamp( '123' );
$this->assertSame( 123, $backend->getLoggedOutTimestamp() );
}
public function testSetUser() {
$user = User::newFromName( 'UTSysop' );
$this->provider = $this->getMock( 'DummySessionProvider', array( 'canChangeUser' ) );
$this->provider->expects( $this->any() )->method( 'canChangeUser' )
->will( $this->returnValue( false ) );
$backend = $this->getBackend();
$this->assertFalse( $backend->canSetUser() );
try {
$backend->setUser( $user );
$this->fail( 'Expected exception not thrown' );
} catch ( \BadMethodCallException $ex ) {
$this->assertSame(
'Cannot set user on this session; check $session->canSetUser() first',
$ex->getMessage()
);
}
$this->assertNotSame( $user, $backend->getUser() );
$this->provider = null;
$backend = $this->getBackend();
$this->assertTrue( $backend->canSetUser() );
$this->assertNotSame( $user, $backend->getUser(), 'sanity check' );
$backend->setUser( $user );
$this->assertSame( $user, $backend->getUser() );
}
public function testDirty() {
$backend = $this->getBackend();
$priv = \TestingAccessWrapper::newFromObject( $backend );
$priv->dataDirty = false;
$backend->dirty();
$this->assertTrue( $priv->dataDirty );
}
public function testGetData() {
$backend = $this->getBackend();
$data = $backend->getData();
$this->assertSame( array(), $data );
$this->assertTrue( \TestingAccessWrapper::newFromObject( $backend )->dataDirty );
$data['???'] = '!!!';
$this->assertSame( array( '???' => '!!!' ), $data );
$testData = array( 'foo' => 'foo!', 'bar', array( 'baz', null ) );
$this->store->setSessionData( self::SESSIONID, $testData );
$backend = $this->getBackend();
$this->assertSame( $testData, $backend->getData() );
$this->assertFalse( \TestingAccessWrapper::newFromObject( $backend )->dataDirty );
}
public function testAddData() {
$backend = $this->getBackend();
$priv = \TestingAccessWrapper::newFromObject( $backend );
$priv->data = array( 'foo' => 1 );
$priv->dataDirty = false;
$backend->addData( array( 'foo' => 1 ) );
$this->assertSame( array( 'foo' => 1 ), $priv->data );
$this->assertFalse( $priv->dataDirty );
$priv->data = array( 'foo' => 1 );
$priv->dataDirty = false;
$backend->addData( array( 'foo' => '1' ) );
$this->assertSame( array( 'foo' => '1' ), $priv->data );
$this->assertTrue( $priv->dataDirty );
$priv->data = array( 'foo' => 1 );
$priv->dataDirty = false;
$backend->addData( array( 'bar' => 2 ) );
$this->assertSame( array( 'foo' => 1, 'bar' => 2 ), $priv->data );
$this->assertTrue( $priv->dataDirty );
}
public function testDelaySave() {
$this->mergeMwGlobalArrayValue( 'wgHooks', array( 'SessionMetadata' => array( $this ) ) );
$backend = $this->getBackend();
$priv = \TestingAccessWrapper::newFromObject( $backend );
$priv->persist = true;
// Saves happen normally when no delay is in effect
$this->onSessionMetadataCalled = false;
$priv->metaDirty = true;
$backend->save();
$this->assertTrue( $this->onSessionMetadataCalled, 'sanity check' );
$this->onSessionMetadataCalled = false;
$priv->metaDirty = true;
$priv->autosave();
$this->assertTrue( $this->onSessionMetadataCalled, 'sanity check' );
$delay = $backend->delaySave();
// Autosave doesn't happen when no delay is in effect
$this->onSessionMetadataCalled = false;
$priv->metaDirty = true;
$priv->autosave();
$this->assertFalse( $this->onSessionMetadataCalled );
// Save still does happen when no delay is in effect
$priv->save();
$this->assertTrue( $this->onSessionMetadataCalled );
// Save happens when delay is consumed
$this->onSessionMetadataCalled = false;
$priv->metaDirty = true;
\ScopedCallback::consume( $delay );
$this->assertTrue( $this->onSessionMetadataCalled );
// Test multiple delays
$delay1 = $backend->delaySave();
$delay2 = $backend->delaySave();
$delay3 = $backend->delaySave();
$this->onSessionMetadataCalled = false;
$priv->metaDirty = true;
$priv->autosave();
$this->assertFalse( $this->onSessionMetadataCalled );
\ScopedCallback::consume( $delay3 );
$this->assertFalse( $this->onSessionMetadataCalled );
\ScopedCallback::consume( $delay1 );
$this->assertFalse( $this->onSessionMetadataCalled );
\ScopedCallback::consume( $delay2 );
$this->assertTrue( $this->onSessionMetadataCalled );
}
public function testSave() {
$user = User::newFromName( 'UTSysop' );
$this->store = new TestBagOStuff();
$testData = array( 'foo' => 'foo!', 'bar', array( 'baz', null ) );
$neverHook = $this->getMock( __CLASS__, array( 'onSessionMetadata' ) );
$neverHook->expects( $this->never() )->method( 'onSessionMetadata' );
$neverProvider = $this->getMock( 'DummySessionProvider', array( 'persistSession' ) );
$neverProvider->expects( $this->never() )->method( 'persistSession' );
// Not persistent or dirty
$this->provider = $neverProvider;
$this->mergeMwGlobalArrayValue( 'wgHooks', array( 'SessionMetadata' => array( $neverHook ) ) );
$this->store->setSessionData( self::SESSIONID, $testData );
$backend = $this->getBackend( $user );
$this->store->deleteSession( self::SESSIONID );
$this->assertFalse( $backend->isPersistent(), 'sanity check' );
\TestingAccessWrapper::newFromObject( $backend )->metaDirty = false;
\TestingAccessWrapper::newFromObject( $backend )->dataDirty = false;
$backend->save();
$this->assertFalse( $this->store->getSession( self::SESSIONID ), 'making sure it didn\'t save' );
// Not persistent, but dirty
$this->provider = $neverProvider;
$this->onSessionMetadataCalled = false;
$this->mergeMwGlobalArrayValue( 'wgHooks', array( 'SessionMetadata' => array( $this ) ) );
$this->store->setSessionData( self::SESSIONID, $testData );
$backend = $this->getBackend( $user );
$this->store->deleteSession( self::SESSIONID );
$this->assertFalse( $backend->isPersistent(), 'sanity check' );
\TestingAccessWrapper::newFromObject( $backend )->metaDirty = false;
\TestingAccessWrapper::newFromObject( $backend )->dataDirty = true;
$backend->save();
$this->assertTrue( $this->onSessionMetadataCalled );
$blob = $this->store->getSession( self::SESSIONID );
$this->assertInternalType( 'array', $blob );
$this->assertArrayHasKey( 'metadata', $blob );
$metadata = $blob['metadata'];
$this->assertInternalType( 'array', $metadata );
$this->assertArrayHasKey( '???', $metadata );
$this->assertSame( '!!!', $metadata['???'] );
// Persistent, not dirty
$this->provider = $neverProvider;
$this->mergeMwGlobalArrayValue( 'wgHooks', array( 'SessionMetadata' => array( $neverHook ) ) );
$this->store->setSessionData( self::SESSIONID, $testData );
$backend = $this->getBackend( $user );
$this->store->deleteSession( self::SESSIONID );
\TestingAccessWrapper::newFromObject( $backend )->persist = true;
$this->assertTrue( $backend->isPersistent(), 'sanity check' );
\TestingAccessWrapper::newFromObject( $backend )->metaDirty = false;
\TestingAccessWrapper::newFromObject( $backend )->dataDirty = false;
$backend->save();
$this->assertFalse( $this->store->getSession( self::SESSIONID ), 'making sure it didn\'t save' );
$this->provider = $this->getMock( 'DummySessionProvider', array( 'persistSession' ) );
$this->provider->expects( $this->atLeastOnce() )->method( 'persistSession' );
$this->mergeMwGlobalArrayValue( 'wgHooks', array( 'SessionMetadata' => array( $neverHook ) ) );
$this->store->setSessionData( self::SESSIONID, $testData );
$backend = $this->getBackend( $user );
$this->store->deleteSession( self::SESSIONID );
\TestingAccessWrapper::newFromObject( $backend )->persist = true;
\TestingAccessWrapper::newFromObject( $backend )->forcePersist = true;
$this->assertTrue( $backend->isPersistent(), 'sanity check' );
\TestingAccessWrapper::newFromObject( $backend )->metaDirty = false;
\TestingAccessWrapper::newFromObject( $backend )->dataDirty = false;
$backend->save();
$this->assertFalse( $this->store->getSession( self::SESSIONID ), 'making sure it didn\'t save' );
// Persistent and dirty
$this->provider = $neverProvider;
$this->onSessionMetadataCalled = false;
$this->mergeMwGlobalArrayValue( 'wgHooks', array( 'SessionMetadata' => array( $this ) ) );
$this->store->setSessionData( self::SESSIONID, $testData );
$backend = $this->getBackend( $user );
$this->store->deleteSession( self::SESSIONID );
\TestingAccessWrapper::newFromObject( $backend )->persist = true;
$this->assertTrue( $backend->isPersistent(), 'sanity check' );
\TestingAccessWrapper::newFromObject( $backend )->metaDirty = false;
\TestingAccessWrapper::newFromObject( $backend )->dataDirty = true;
$backend->save();
$this->assertTrue( $this->onSessionMetadataCalled );
$blob = $this->store->getSession( self::SESSIONID );
$this->assertInternalType( 'array', $blob );
$this->assertArrayHasKey( 'metadata', $blob );
$metadata = $blob['metadata'];
$this->assertInternalType( 'array', $metadata );
$this->assertArrayHasKey( '???', $metadata );
$this->assertSame( '!!!', $metadata['???'] );
$this->provider = $this->getMock( 'DummySessionProvider', array( 'persistSession' ) );
$this->provider->expects( $this->atLeastOnce() )->method( 'persistSession' );
$this->onSessionMetadataCalled = false;
$this->mergeMwGlobalArrayValue( 'wgHooks', array( 'SessionMetadata' => array( $this ) ) );
$this->store->setSessionData( self::SESSIONID, $testData );
$backend = $this->getBackend( $user );
$this->store->deleteSession( self::SESSIONID );
\TestingAccessWrapper::newFromObject( $backend )->persist = true;
\TestingAccessWrapper::newFromObject( $backend )->forcePersist = true;
$this->assertTrue( $backend->isPersistent(), 'sanity check' );
\TestingAccessWrapper::newFromObject( $backend )->metaDirty = false;
\TestingAccessWrapper::newFromObject( $backend )->dataDirty = true;
$backend->save();
$this->assertTrue( $this->onSessionMetadataCalled );
$blob = $this->store->getSession( self::SESSIONID );
$this->assertInternalType( 'array', $blob );
$this->assertArrayHasKey( 'metadata', $blob );
$metadata = $blob['metadata'];
$this->assertInternalType( 'array', $metadata );
$this->assertArrayHasKey( '???', $metadata );
$this->assertSame( '!!!', $metadata['???'] );
$this->provider = $this->getMock( 'DummySessionProvider', array( 'persistSession' ) );
$this->provider->expects( $this->atLeastOnce() )->method( 'persistSession' );
$this->onSessionMetadataCalled = false;
$this->mergeMwGlobalArrayValue( 'wgHooks', array( 'SessionMetadata' => array( $this ) ) );
$this->store->setSessionData( self::SESSIONID, $testData );
$backend = $this->getBackend( $user );
$this->store->deleteSession( self::SESSIONID );
\TestingAccessWrapper::newFromObject( $backend )->persist = true;
$this->assertTrue( $backend->isPersistent(), 'sanity check' );
\TestingAccessWrapper::newFromObject( $backend )->metaDirty = true;
\TestingAccessWrapper::newFromObject( $backend )->dataDirty = false;
$backend->save();
$this->assertTrue( $this->onSessionMetadataCalled );
$blob = $this->store->getSession( self::SESSIONID );
$this->assertInternalType( 'array', $blob );
$this->assertArrayHasKey( 'metadata', $blob );
$metadata = $blob['metadata'];
$this->assertInternalType( 'array', $metadata );
$this->assertArrayHasKey( '???', $metadata );
$this->assertSame( '!!!', $metadata['???'] );
// Not marked dirty, but dirty data
$this->provider = $neverProvider;
$this->onSessionMetadataCalled = false;
$this->mergeMwGlobalArrayValue( 'wgHooks', array( 'SessionMetadata' => array( $this ) ) );
$this->store->setSessionData( self::SESSIONID, $testData );
$backend = $this->getBackend( $user );
$this->store->deleteSession( self::SESSIONID );
\TestingAccessWrapper::newFromObject( $backend )->persist = true;
$this->assertTrue( $backend->isPersistent(), 'sanity check' );
\TestingAccessWrapper::newFromObject( $backend )->metaDirty = false;
\TestingAccessWrapper::newFromObject( $backend )->dataDirty = false;
\TestingAccessWrapper::newFromObject( $backend )->dataHash = 'Doesn\'t match';
$backend->save();
$this->assertTrue( $this->onSessionMetadataCalled );
$blob = $this->store->getSession( self::SESSIONID );
$this->assertInternalType( 'array', $blob );
$this->assertArrayHasKey( 'metadata', $blob );
$metadata = $blob['metadata'];
$this->assertInternalType( 'array', $metadata );
$this->assertArrayHasKey( '???', $metadata );
$this->assertSame( '!!!', $metadata['???'] );
// Bad hook
$this->provider = null;
$mockHook = $this->getMock( __CLASS__, array( 'onSessionMetadata' ) );
$mockHook->expects( $this->any() )->method( 'onSessionMetadata' )
->will( $this->returnCallback(
function ( SessionBackend $backend, array &$metadata, array $requests ) {
$metadata['userId']++;
}
) );
$this->mergeMwGlobalArrayValue( 'wgHooks', array( 'SessionMetadata' => array( $mockHook ) ) );
$this->store->setSessionData( self::SESSIONID, $testData );
$backend = $this->getBackend( $user );
$backend->dirty();
try {
$backend->save();
$this->fail( 'Expected exception not thrown' );
} catch ( \UnexpectedValueException $ex ) {
$this->assertSame(
'SessionMetadata hook changed metadata key "userId"',
$ex->getMessage()
);
}
// SessionManager::preventSessionsForUser
\TestingAccessWrapper::newFromObject( $this->manager )->preventUsers = array(
$user->getName() => true,
);
$this->provider = $neverProvider;
$this->mergeMwGlobalArrayValue( 'wgHooks', array( 'SessionMetadata' => array( $neverHook ) ) );
$this->store->setSessionData( self::SESSIONID, $testData );
$backend = $this->getBackend( $user );
$this->store->deleteSession( self::SESSIONID );
\TestingAccessWrapper::newFromObject( $backend )->persist = true;
$this->assertTrue( $backend->isPersistent(), 'sanity check' );
\TestingAccessWrapper::newFromObject( $backend )->metaDirty = true;
\TestingAccessWrapper::newFromObject( $backend )->dataDirty = true;
$backend->save();
$this->assertFalse( $this->store->getSession( self::SESSIONID ), 'making sure it didn\'t save' );
}
public function testRenew() {
$user = User::newFromName( 'UTSysop' );
$this->store = new TestBagOStuff();
$testData = array( 'foo' => 'foo!', 'bar', array( 'baz', null ) );
// Not persistent
$this->provider = $this->getMock( 'DummySessionProvider', array( 'persistSession' ) );
$this->provider->expects( $this->never() )->method( 'persistSession' );
$this->onSessionMetadataCalled = false;
$this->mergeMwGlobalArrayValue( 'wgHooks', array( 'SessionMetadata' => array( $this ) ) );
$this->store->setSessionData( self::SESSIONID, $testData );
$backend = $this->getBackend( $user );
$this->store->deleteSession( self::SESSIONID );
$wrap = \TestingAccessWrapper::newFromObject( $backend );
$this->assertFalse( $backend->isPersistent(), 'sanity check' );
$wrap->metaDirty = false;
$wrap->dataDirty = false;
$wrap->forcePersist = false;
$wrap->expires = 0;
$backend->renew();
$this->assertTrue( $this->onSessionMetadataCalled );
$blob = $this->store->getSession( self::SESSIONID );
$this->assertInternalType( 'array', $blob );
$this->assertArrayHasKey( 'metadata', $blob );
$metadata = $blob['metadata'];
$this->assertInternalType( 'array', $metadata );
$this->assertArrayHasKey( '???', $metadata );
$this->assertSame( '!!!', $metadata['???'] );
$this->assertNotEquals( 0, $wrap->expires );
// Persistent
$this->provider = $this->getMock( 'DummySessionProvider', array( 'persistSession' ) );
$this->provider->expects( $this->atLeastOnce() )->method( 'persistSession' );
$this->onSessionMetadataCalled = false;
$this->mergeMwGlobalArrayValue( 'wgHooks', array( 'SessionMetadata' => array( $this ) ) );
$this->store->setSessionData( self::SESSIONID, $testData );
$backend = $this->getBackend( $user );
$this->store->deleteSession( self::SESSIONID );
$wrap = \TestingAccessWrapper::newFromObject( $backend );
$wrap->persist = true;
$this->assertTrue( $backend->isPersistent(), 'sanity check' );
$wrap->metaDirty = false;
$wrap->dataDirty = false;
$wrap->forcePersist = false;
$wrap->expires = 0;
$backend->renew();
$this->assertTrue( $this->onSessionMetadataCalled );
$blob = $this->store->getSession( self::SESSIONID );
$this->assertInternalType( 'array', $blob );
$this->assertArrayHasKey( 'metadata', $blob );
$metadata = $blob['metadata'];
$this->assertInternalType( 'array', $metadata );
$this->assertArrayHasKey( '???', $metadata );
$this->assertSame( '!!!', $metadata['???'] );
$this->assertNotEquals( 0, $wrap->expires );
// Not persistent, not expiring
$this->provider = $this->getMock( 'DummySessionProvider', array( 'persistSession' ) );
$this->provider->expects( $this->never() )->method( 'persistSession' );
$this->onSessionMetadataCalled = false;
$this->mergeMwGlobalArrayValue( 'wgHooks', array( 'SessionMetadata' => array( $this ) ) );
$this->store->setSessionData( self::SESSIONID, $testData );
$backend = $this->getBackend( $user );
$this->store->deleteSession( self::SESSIONID );
$wrap = \TestingAccessWrapper::newFromObject( $backend );
$this->assertFalse( $backend->isPersistent(), 'sanity check' );
$wrap->metaDirty = false;
$wrap->dataDirty = false;
$wrap->forcePersist = false;
$expires = time() + $wrap->lifetime + 100;
$wrap->expires = $expires;
$backend->renew();
$this->assertFalse( $this->onSessionMetadataCalled );
$this->assertFalse( $this->store->getSession( self::SESSIONID ), 'making sure it didn\'t save' );
$this->assertEquals( $expires, $wrap->expires );
}
public function onSessionMetadata( SessionBackend $backend, array &$metadata, array $requests ) {
$this->onSessionMetadataCalled = true;
$metadata['???'] = '!!!';
}
public function testResetIdOfGlobalSession() {
if ( !PHPSessionHandler::isInstalled() ) {
PHPSessionHandler::install( SessionManager::singleton() );
}
if ( !PHPSessionHandler::isEnabled() ) {
$rProp = new \ReflectionProperty( 'MediaWiki\\Session\\PHPSessionHandler', 'instance' );
$rProp->setAccessible( true );
$handler = \TestingAccessWrapper::newFromObject( $rProp->getValue() );
$resetHandler = new \ScopedCallback( function () use ( $handler ) {
session_write_close();
$handler->enable = false;
} );
$handler->enable = true;
}
$backend = $this->getBackend( User::newFromName( 'UTSysop' ) );
\TestingAccessWrapper::newFromObject( $backend )->usePhpSessionHandling = true;
TestUtils::setSessionManagerSingleton( $this->manager );
$manager = \TestingAccessWrapper::newFromObject( $this->manager );
$request = \RequestContext::getMain()->getRequest();
$manager->globalSession = $backend->getSession( $request );
$manager->globalSessionRequest = $request;
session_id( self::SESSIONID );
\MediaWiki\quietCall( 'session_start' );
$backend->resetId();
$this->assertNotEquals( self::SESSIONID, $backend->getId() );
$this->assertSame( $backend->getId(), session_id() );
session_write_close();
session_id( '' );
$this->assertNotSame( $backend->getId(), session_id(), 'sanity check' );
$backend->persist();
$this->assertSame( $backend->getId(), session_id() );
session_write_close();
}
}

View file

@ -0,0 +1,22 @@
<?php
namespace MediaWiki\Session;
use MediaWikiTestCase;
/**
* @group Session
* @covers MediaWiki\Session\SessionId
*/
class SessionIdTest extends MediaWikiTestCase {
public function testEverything() {
$id = new SessionId( 'foo' );
$this->assertSame( 'foo', $id->getId() );
$this->assertSame( 'foo', (string)$id );
$id->setId( 'bar' );
$this->assertSame( 'bar', $id->getId() );
$this->assertSame( 'bar', (string)$id );
}
}

View file

@ -0,0 +1,328 @@
<?php
namespace MediaWiki\Session;
use Psr\Log\LogLevel;
use MediaWikiTestCase;
/**
* @group Session
* @group Database
* @covers MediaWiki\Session\SessionInfo
*/
class SessionInfoTest extends MediaWikiTestCase {
public function testBasics() {
$anonInfo = UserInfo::newAnonymous();
$userInfo = UserInfo::newFromName( 'UTSysop', true );
$unverifiedUserInfo = UserInfo::newFromName( 'UTSysop', false );
try {
new SessionInfo( SessionInfo::MIN_PRIORITY - 1, array() );
$this->fail( 'Expected exception not thrown', 'priority < min' );
} catch ( \InvalidArgumentException $ex ) {
$this->assertSame( 'Invalid priority', $ex->getMessage(), 'priority < min' );
}
try {
new SessionInfo( SessionInfo::MAX_PRIORITY + 1, array() );
$this->fail( 'Expected exception not thrown', 'priority > max' );
} catch ( \InvalidArgumentException $ex ) {
$this->assertSame( 'Invalid priority', $ex->getMessage(), 'priority > max' );
}
try {
new SessionInfo( SessionInfo::MIN_PRIORITY, array( 'id' => 'ABC?' ) );
$this->fail( 'Expected exception not thrown', 'bad session ID' );
} catch ( \InvalidArgumentException $ex ) {
$this->assertSame( 'Invalid session ID', $ex->getMessage(), 'bad session ID' );
}
try {
new SessionInfo( SessionInfo::MIN_PRIORITY, array( 'userInfo' => new \stdClass ) );
$this->fail( 'Expected exception not thrown', 'bad userInfo' );
} catch ( \InvalidArgumentException $ex ) {
$this->assertSame( 'Invalid userInfo', $ex->getMessage(), 'bad userInfo' );
}
try {
new SessionInfo( SessionInfo::MIN_PRIORITY, array() );
$this->fail( 'Expected exception not thrown', 'no provider, no id' );
} catch ( \InvalidArgumentException $ex ) {
$this->assertSame( 'Must supply an ID when no provider is given', $ex->getMessage(),
'no provider, no id' );
}
try {
new SessionInfo( SessionInfo::MIN_PRIORITY, array( 'copyFrom' => new \stdClass ) );
$this->fail( 'Expected exception not thrown', 'bad copyFrom' );
} catch ( \InvalidArgumentException $ex ) {
$this->assertSame( 'Invalid copyFrom', $ex->getMessage(),
'bad copyFrom' );
}
$manager = new SessionManager();
$provider = $this->getMockBuilder( 'MediaWiki\\Session\\SessionProvider' )
->setMethods( array( 'persistsSessionId', 'canChangeUser', '__toString' ) )
->getMockForAbstractClass();
$provider->setManager( $manager );
$provider->expects( $this->any() )->method( 'persistsSessionId' )
->will( $this->returnValue( true ) );
$provider->expects( $this->any() )->method( 'canChangeUser' )
->will( $this->returnValue( true ) );
$provider->expects( $this->any() )->method( '__toString' )
->will( $this->returnValue( 'Mock' ) );
$provider2 = $this->getMockBuilder( 'MediaWiki\\Session\\SessionProvider' )
->setMethods( array( 'persistsSessionId', 'canChangeUser', '__toString' ) )
->getMockForAbstractClass();
$provider2->setManager( $manager );
$provider2->expects( $this->any() )->method( 'persistsSessionId' )
->will( $this->returnValue( true ) );
$provider2->expects( $this->any() )->method( 'canChangeUser' )
->will( $this->returnValue( true ) );
$provider2->expects( $this->any() )->method( '__toString' )
->will( $this->returnValue( 'Mock2' ) );
try {
new SessionInfo( SessionInfo::MIN_PRIORITY, array(
'provider' => $provider,
'userInfo' => $anonInfo,
'metadata' => 'foo',
) );
$this->fail( 'Expected exception not thrown', 'bad metadata' );
} catch ( \InvalidArgumentException $ex ) {
$this->assertSame( 'Invalid metadata', $ex->getMessage(), 'bad metadata' );
}
$info = new SessionInfo( SessionInfo::MIN_PRIORITY + 5, array(
'provider' => $provider,
'userInfo' => $anonInfo
) );
$this->assertSame( $provider, $info->getProvider() );
$this->assertNotNull( $info->getId() );
$this->assertSame( SessionInfo::MIN_PRIORITY + 5, $info->getPriority() );
$this->assertSame( $anonInfo, $info->getUserInfo() );
$this->assertTrue( $info->isIdSafe() );
$this->assertFalse( $info->wasPersisted() );
$this->assertFalse( $info->wasRemembered() );
$this->assertFalse( $info->forceHTTPS() );
$this->assertNull( $info->getProviderMetadata() );
$info = new SessionInfo( SessionInfo::MIN_PRIORITY + 5, array(
'provider' => $provider,
'userInfo' => $unverifiedUserInfo,
'metadata' => array( 'Foo' ),
) );
$this->assertSame( $provider, $info->getProvider() );
$this->assertNotNull( $info->getId() );
$this->assertSame( SessionInfo::MIN_PRIORITY + 5, $info->getPriority() );
$this->assertSame( $unverifiedUserInfo, $info->getUserInfo() );
$this->assertTrue( $info->isIdSafe() );
$this->assertFalse( $info->wasPersisted() );
$this->assertFalse( $info->wasRemembered() );
$this->assertFalse( $info->forceHTTPS() );
$this->assertSame( array( 'Foo' ), $info->getProviderMetadata() );
$info = new SessionInfo( SessionInfo::MIN_PRIORITY + 5, array(
'provider' => $provider,
'userInfo' => $userInfo
) );
$this->assertSame( $provider, $info->getProvider() );
$this->assertNotNull( $info->getId() );
$this->assertSame( SessionInfo::MIN_PRIORITY + 5, $info->getPriority() );
$this->assertSame( $userInfo, $info->getUserInfo() );
$this->assertTrue( $info->isIdSafe() );
$this->assertFalse( $info->wasPersisted() );
$this->assertTrue( $info->wasRemembered() );
$this->assertFalse( $info->forceHTTPS() );
$this->assertNull( $info->getProviderMetadata() );
$id = $manager->generateSessionId();
$info = new SessionInfo( SessionInfo::MIN_PRIORITY + 5, array(
'provider' => $provider,
'id' => $id,
'persisted' => true,
'userInfo' => $anonInfo
) );
$this->assertSame( $provider, $info->getProvider() );
$this->assertSame( $id, $info->getId() );
$this->assertSame( SessionInfo::MIN_PRIORITY + 5, $info->getPriority() );
$this->assertSame( $anonInfo, $info->getUserInfo() );
$this->assertFalse( $info->isIdSafe() );
$this->assertTrue( $info->wasPersisted() );
$this->assertFalse( $info->wasRemembered() );
$this->assertFalse( $info->forceHTTPS() );
$this->assertNull( $info->getProviderMetadata() );
$info = new SessionInfo( SessionInfo::MIN_PRIORITY + 5, array(
'provider' => $provider,
'id' => $id,
'userInfo' => $userInfo
) );
$this->assertSame( $provider, $info->getProvider() );
$this->assertSame( $id, $info->getId() );
$this->assertSame( SessionInfo::MIN_PRIORITY + 5, $info->getPriority() );
$this->assertSame( $userInfo, $info->getUserInfo() );
$this->assertFalse( $info->isIdSafe() );
$this->assertFalse( $info->wasPersisted() );
$this->assertTrue( $info->wasRemembered() );
$this->assertFalse( $info->forceHTTPS() );
$this->assertNull( $info->getProviderMetadata() );
$info = new SessionInfo( SessionInfo::MIN_PRIORITY + 5, array(
'id' => $id,
'persisted' => true,
'userInfo' => $userInfo,
'metadata' => array( 'Foo' ),
) );
$this->assertSame( $id, $info->getId() );
$this->assertSame( SessionInfo::MIN_PRIORITY + 5, $info->getPriority() );
$this->assertSame( $userInfo, $info->getUserInfo() );
$this->assertFalse( $info->isIdSafe() );
$this->assertTrue( $info->wasPersisted() );
$this->assertFalse( $info->wasRemembered() );
$this->assertFalse( $info->forceHTTPS() );
$this->assertNull( $info->getProviderMetadata() );
$info = new SessionInfo( SessionInfo::MIN_PRIORITY + 5, array(
'id' => $id,
'remembered' => true,
'userInfo' => $userInfo,
) );
$this->assertFalse( $info->wasRemembered(), 'no provider' );
$info = new SessionInfo( SessionInfo::MIN_PRIORITY + 5, array(
'provider' => $provider,
'id' => $id,
'remembered' => true,
) );
$this->assertFalse( $info->wasRemembered(), 'no user' );
$info = new SessionInfo( SessionInfo::MIN_PRIORITY + 5, array(
'provider' => $provider,
'id' => $id,
'remembered' => true,
'userInfo' => $anonInfo,
) );
$this->assertFalse( $info->wasRemembered(), 'anonymous user' );
$info = new SessionInfo( SessionInfo::MIN_PRIORITY + 5, array(
'provider' => $provider,
'id' => $id,
'remembered' => true,
'userInfo' => $unverifiedUserInfo,
) );
$this->assertFalse( $info->wasRemembered(), 'unverified user' );
$info = new SessionInfo( SessionInfo::MIN_PRIORITY + 5, array(
'provider' => $provider,
'id' => $id,
'remembered' => false,
'userInfo' => $userInfo,
) );
$this->assertFalse( $info->wasRemembered(), 'specific override' );
$info = new SessionInfo( SessionInfo::MIN_PRIORITY + 5, array(
'id' => $id,
'idIsSafe' => true,
) );
$this->assertSame( $id, $info->getId() );
$this->assertSame( SessionInfo::MIN_PRIORITY + 5, $info->getPriority() );
$this->assertTrue( $info->isIdSafe() );
$info = new SessionInfo( SessionInfo::MIN_PRIORITY, array(
'id' => $id,
'forceHTTPS' => 1,
) );
$this->assertTrue( $info->forceHTTPS() );
$fromInfo = new SessionInfo( SessionInfo::MIN_PRIORITY, array(
'id' => $id . 'A',
'provider' => $provider,
'userInfo' => $userInfo,
'idIsSafe' => true,
'persisted' => true,
'remembered' => true,
'forceHTTPS' => true,
'metadata' => array( 'foo!' ),
) );
$info = new SessionInfo( SessionInfo::MIN_PRIORITY + 4, array(
'copyFrom' => $fromInfo,
) );
$this->assertSame( $id . 'A', $info->getId() );
$this->assertSame( SessionInfo::MIN_PRIORITY + 4, $info->getPriority() );
$this->assertSame( $provider, $info->getProvider() );
$this->assertSame( $userInfo, $info->getUserInfo() );
$this->assertTrue( $info->isIdSafe() );
$this->assertTrue( $info->wasPersisted() );
$this->assertTrue( $info->wasRemembered() );
$this->assertTrue( $info->forceHTTPS() );
$this->assertSame( array( 'foo!' ), $info->getProviderMetadata() );
$info = new SessionInfo( SessionInfo::MIN_PRIORITY + 4, array(
'id' => $id . 'X',
'provider' => $provider2,
'userInfo' => $unverifiedUserInfo,
'idIsSafe' => false,
'persisted' => false,
'remembered' => false,
'forceHTTPS' => false,
'metadata' => null,
'copyFrom' => $fromInfo,
) );
$this->assertSame( $id . 'X', $info->getId() );
$this->assertSame( SessionInfo::MIN_PRIORITY + 4, $info->getPriority() );
$this->assertSame( $provider2, $info->getProvider() );
$this->assertSame( $unverifiedUserInfo, $info->getUserInfo() );
$this->assertFalse( $info->isIdSafe() );
$this->assertFalse( $info->wasPersisted() );
$this->assertFalse( $info->wasRemembered() );
$this->assertFalse( $info->forceHTTPS() );
$this->assertNull( $info->getProviderMetadata() );
$info = new SessionInfo( SessionInfo::MIN_PRIORITY, array(
'id' => $id,
) );
$this->assertSame(
'[' . SessionInfo::MIN_PRIORITY . "]null<null>$id",
(string)$info,
'toString'
);
$info = new SessionInfo( SessionInfo::MIN_PRIORITY, array(
'provider' => $provider,
'id' => $id,
'persisted' => true,
'userInfo' => $userInfo
) );
$this->assertSame(
'[' . SessionInfo::MIN_PRIORITY . "]Mock<+:{$userInfo->getId()}:UTSysop>$id",
(string)$info,
'toString'
);
$info = new SessionInfo( SessionInfo::MIN_PRIORITY, array(
'provider' => $provider,
'id' => $id,
'persisted' => true,
'userInfo' => $unverifiedUserInfo
) );
$this->assertSame(
'[' . SessionInfo::MIN_PRIORITY . "]Mock<-:{$userInfo->getId()}:UTSysop>$id",
(string)$info,
'toString'
);
}
public function testCompare() {
$id = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa';
$info1 = new SessionInfo( SessionInfo::MIN_PRIORITY + 1, array( 'id' => $id ) );
$info2 = new SessionInfo( SessionInfo::MIN_PRIORITY + 2, array( 'id' => $id ) );
$this->assertTrue( SessionInfo::compare( $info1, $info2 ) < 0, '<' );
$this->assertTrue( SessionInfo::compare( $info2, $info1 ) > 0, '>' );
$this->assertTrue( SessionInfo::compare( $info1, $info1 ) === 0, '==' );
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,177 @@
<?php
namespace MediaWiki\Session;
use MediaWikiTestCase;
/**
* @group Session
* @group Database
* @covers MediaWiki\Session\SessionProvider
*/
class SessionProviderTest extends MediaWikiTestCase {
public function testBasics() {
$manager = new SessionManager();
$logger = new \TestLogger();
$config = new \HashConfig();
$provider = $this->getMockForAbstractClass( 'MediaWiki\\Session\\SessionProvider' );
$priv = \TestingAccessWrapper::newFromObject( $provider );
$provider->setConfig( $config );
$this->assertSame( $config, $priv->config );
$provider->setLogger( $logger );
$this->assertSame( $logger, $priv->logger );
$provider->setManager( $manager );
$this->assertSame( $manager, $priv->manager );
$this->assertSame( $manager, $provider->getManager() );
$this->assertSame( array(), $provider->getVaryHeaders() );
$this->assertSame( array(), $provider->getVaryCookies() );
$this->assertSame( null, $provider->suggestLoginUsername( new \FauxRequest ) );
$this->assertSame( get_class( $provider ), (string)$provider );
$this->assertNull( $provider->whyNoSession() );
$info = new SessionInfo( SessionInfo::MIN_PRIORITY, array(
'id' => 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',
'provider' => $provider,
) );
$metadata = array( 'foo' );
$this->assertTrue( $provider->refreshSessionInfo( $info, new \FauxRequest, $metadata ) );
$this->assertSame( array( 'foo' ), $metadata );
}
/**
* @dataProvider provideNewSessionInfo
* @param bool $persistId Return value for ->persistsSessionId()
* @param bool $persistUser Return value for ->persistsSessionUser()
* @param bool $ok Whether a SessionInfo is provided
*/
public function testNewSessionInfo( $persistId, $persistUser, $ok ) {
$manager = new SessionManager();
$provider = $this->getMockBuilder( 'MediaWiki\\Session\\SessionProvider' )
->setMethods( array( 'canChangeUser', 'persistsSessionId' ) )
->getMockForAbstractClass();
$provider->expects( $this->any() )->method( 'persistsSessionId' )
->will( $this->returnValue( $persistId ) );
$provider->expects( $this->any() )->method( 'canChangeUser' )
->will( $this->returnValue( $persistUser ) );
$provider->setManager( $manager );
if ( $ok ) {
$info = $provider->newSessionInfo();
$this->assertNotNull( $info );
$this->assertFalse( $info->wasPersisted() );
$this->assertTrue( $info->isIdSafe() );
$id = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa';
$info = $provider->newSessionInfo( $id );
$this->assertNotNull( $info );
$this->assertSame( $id, $info->getId() );
$this->assertFalse( $info->wasPersisted() );
$this->assertTrue( $info->isIdSafe() );
} else {
$this->assertNull( $provider->newSessionInfo() );
}
}
public function testMergeMetadata() {
$provider = $this->getMockBuilder( 'MediaWiki\\Session\\SessionProvider' )
->getMockForAbstractClass();
try {
$provider->mergeMetadata(
array( 'foo' => 1, 'baz' => 3 ),
array( 'bar' => 2, 'baz' => '3' )
);
$this->fail( 'Expected exception not thrown' );
} catch ( \UnexpectedValueException $ex ) {
$this->assertSame( 'Key "baz" changed', $ex->getMessage() );
}
$res = $provider->mergeMetadata(
array( 'foo' => 1, 'baz' => 3 ),
array( 'bar' => 2, 'baz' => 3 )
);
$this->assertSame( array( 'bar' => 2, 'baz' => 3 ), $res );
}
public static function provideNewSessionInfo() {
return array(
array( false, false, false ),
array( true, false, false ),
array( false, true, false ),
array( true, true, true ),
);
}
public function testImmutableSessions() {
$provider = $this->getMockBuilder( 'MediaWiki\\Session\\SessionProvider' )
->setMethods( array( 'canChangeUser', 'persistsSessionId' ) )
->getMockForAbstractClass();
$provider->expects( $this->any() )->method( 'canChangeUser' )
->will( $this->returnValue( true ) );
$provider->preventSessionsForUser( 'Foo' );
$provider = $this->getMockBuilder( 'MediaWiki\\Session\\SessionProvider' )
->setMethods( array( 'canChangeUser', 'persistsSessionId' ) )
->getMockForAbstractClass();
$provider->expects( $this->any() )->method( 'canChangeUser' )
->will( $this->returnValue( false ) );
try {
$provider->preventSessionsForUser( 'Foo' );
$this->fail( 'Expected exception not thrown' );
} catch ( \BadMethodCallException $ex ) {
}
}
public function testHashToSessionId() {
$config = new \HashConfig( array(
'SecretKey' => 'Shhh!',
) );
$provider = $this->getMockForAbstractClass( 'MediaWiki\\Session\\SessionProvider',
array(), 'MockSessionProvider' );
$provider->setConfig( $config );
$priv = \TestingAccessWrapper::newFromObject( $provider );
$this->assertSame( 'eoq8cb1mg7j30ui5qolafps4hg29k5bb', $priv->hashToSessionId( 'foobar' ) );
$this->assertSame( '4do8j7tfld1g8tte9jqp3csfgmulaun9',
$priv->hashToSessionId( 'foobar', 'secret' ) );
try {
$priv->hashToSessionId( array() );
$this->fail( 'Expected exception not thrown' );
} catch ( \InvalidArgumentException $ex ) {
$this->assertSame(
'$data must be a string, array was passed',
$ex->getMessage()
);
}
try {
$priv->hashToSessionId( '', false );
$this->fail( 'Expected exception not thrown' );
} catch ( \InvalidArgumentException $ex ) {
$this->assertSame(
'$key must be a string or null, boolean was passed',
$ex->getMessage()
);
}
}
public function testDescribe() {
$provider = $this->getMockForAbstractClass( 'MediaWiki\\Session\\SessionProvider',
array(), 'MockSessionProvider' );
$this->assertSame(
'MockSessionProvider sessions',
$provider->describe( \Language::factory( 'en' ) )
);
}
}

View file

@ -0,0 +1,201 @@
<?php
namespace MediaWiki\Session;
use MediaWikiTestCase;
use User;
/**
* @group Session
* @covers MediaWiki\Session\Session
*/
class SessionTest extends MediaWikiTestCase {
public function testConstructor() {
$backend = TestUtils::getDummySessionBackend();
\TestingAccessWrapper::newFromObject( $backend )->requests = array( -1 => 'dummy' );
\TestingAccessWrapper::newFromObject( $backend )->id = new SessionId( 'abc' );
$session = new Session( $backend, 42 );
$priv = \TestingAccessWrapper::newFromObject( $session );
$this->assertSame( $backend, $priv->backend );
$this->assertSame( 42, $priv->index );
$request = new \FauxRequest();
$priv2 = \TestingAccessWrapper::newFromObject( $session->sessionWithRequest( $request ) );
$this->assertSame( $backend, $priv2->backend );
$this->assertNotSame( $priv->index, $priv2->index );
$this->assertSame( $request, $priv2->getRequest() );
}
/**
* @dataProvider provideMethods
* @param string $m Method to test
* @param array $args Arguments to pass to the method
* @param bool $index Whether the backend method gets passed the index
* @param bool $ret Whether the method returns a value
*/
public function testMethods( $m, $args, $index, $ret ) {
$mock = $this->getMock( 'MediaWiki\\Session\\DummySessionBackend',
array( $m, 'deregisterSession' ) );
$mock->expects( $this->once() )->method( 'deregisterSession' )
->with( $this->identicalTo( 42 ) );
$tmp = $mock->expects( $this->once() )->method( $m );
$expectArgs = array();
if ( $index ) {
$expectArgs[] = $this->identicalTo( 42 );
}
foreach ( $args as $arg ) {
$expectArgs[] = $this->identicalTo( $arg );
}
$tmp = call_user_func_array( array( $tmp, 'with' ), $expectArgs );
$retval = new \stdClass;
$tmp->will( $this->returnValue( $retval ) );
$session = TestUtils::getDummySession( $mock, 42 );
if ( $ret ) {
$this->assertSame( $retval, call_user_func_array( array( $session, $m ), $args ) );
} else {
$this->assertNull( call_user_func_array( array( $session, $m ), $args ) );
}
// Trigger Session destructor
$session = null;
}
public static function provideMethods() {
return array(
array( 'getId', array(), false, true ),
array( 'getSessionId', array(), false, true ),
array( 'resetId', array(), false, true ),
array( 'getProvider', array(), false, true ),
array( 'isPersistent', array(), false, true ),
array( 'persist', array(), false, false ),
array( 'shouldRememberUser', array(), false, true ),
array( 'setRememberUser', array( true ), false, false ),
array( 'getRequest', array(), true, true ),
array( 'getUser', array(), false, true ),
array( 'canSetUser', array(), false, true ),
array( 'setUser', array( new \stdClass ), false, false ),
array( 'suggestLoginUsername', array(), true, true ),
array( 'shouldForceHTTPS', array(), false, true ),
array( 'setForceHTTPS', array( true ), false, false ),
array( 'getLoggedOutTimestamp', array(), false, true ),
array( 'setLoggedOutTimestamp', array( 123 ), false, false ),
array( 'getProviderMetadata', array(), false, true ),
array( 'save', array(), false, false ),
array( 'delaySave', array(), false, true ),
array( 'renew', array(), false, false ),
);
}
public function testDataAccess() {
$session = TestUtils::getDummySession();
$backend = \TestingAccessWrapper::newFromObject( $session )->backend;
$this->assertEquals( 1, $session->get( 'foo' ) );
$this->assertEquals( 'zero', $session->get( 0 ) );
$this->assertFalse( $backend->dirty );
$this->assertEquals( null, $session->get( 'null' ) );
$this->assertEquals( 'default', $session->get( 'null', 'default' ) );
$this->assertFalse( $backend->dirty );
$session->set( 'foo', 55 );
$this->assertEquals( 55, $backend->data['foo'] );
$this->assertTrue( $backend->dirty );
$backend->dirty = false;
$session->set( 1, 'one' );
$this->assertEquals( 'one', $backend->data[1] );
$this->assertTrue( $backend->dirty );
$backend->dirty = false;
$session->set( 1, 'one' );
$this->assertFalse( $backend->dirty );
$this->assertTrue( $session->exists( 'foo' ) );
$this->assertTrue( $session->exists( 1 ) );
$this->assertFalse( $session->exists( 'null' ) );
$this->assertFalse( $session->exists( 100 ) );
$this->assertFalse( $backend->dirty );
$session->remove( 'foo' );
$this->assertArrayNotHasKey( 'foo', $backend->data );
$this->assertTrue( $backend->dirty );
$backend->dirty = false;
$session->remove( 1 );
$this->assertArrayNotHasKey( 1, $backend->data );
$this->assertTrue( $backend->dirty );
$backend->dirty = false;
$session->remove( 101 );
$this->assertFalse( $backend->dirty );
$backend->data = array( 'a', 'b', '?' => 'c' );
$this->assertSame( 3, $session->count() );
$this->assertSame( 3, count( $session ) );
$this->assertFalse( $backend->dirty );
$data = array();
foreach ( $session as $key => $value ) {
$data[$key] = $value;
}
$this->assertEquals( $backend->data, $data );
$this->assertFalse( $backend->dirty );
$this->assertEquals( $backend->data, iterator_to_array( $session ) );
$this->assertFalse( $backend->dirty );
}
public function testClear() {
$session = TestUtils::getDummySession();
$priv = \TestingAccessWrapper::newFromObject( $session );
$backend = $this->getMock(
'MediaWiki\\Session\\DummySessionBackend', array( 'canSetUser', 'setUser', 'save' )
);
$backend->expects( $this->once() )->method( 'canSetUser' )
->will( $this->returnValue( true ) );
$backend->expects( $this->once() )->method( 'setUser' )
->with( $this->callback( function ( $user ) {
return $user instanceof User && $user->isAnon();
} ) );
$backend->expects( $this->once() )->method( 'save' );
$priv->backend = $backend;
$session->clear();
$this->assertSame( array(), $backend->data );
$this->assertTrue( $backend->dirty );
$backend = $this->getMock(
'MediaWiki\\Session\\DummySessionBackend', array( 'canSetUser', 'setUser', 'save' )
);
$backend->data = array();
$backend->expects( $this->once() )->method( 'canSetUser' )
->will( $this->returnValue( true ) );
$backend->expects( $this->once() )->method( 'setUser' )
->with( $this->callback( function ( $user ) {
return $user instanceof User && $user->isAnon();
} ) );
$backend->expects( $this->once() )->method( 'save' );
$priv->backend = $backend;
$session->clear();
$this->assertFalse( $backend->dirty );
$backend = $this->getMock(
'MediaWiki\\Session\\DummySessionBackend', array( 'canSetUser', 'setUser', 'save' )
);
$backend->expects( $this->once() )->method( 'canSetUser' )
->will( $this->returnValue( false ) );
$backend->expects( $this->never() )->method( 'setUser' );
$backend->expects( $this->once() )->method( 'save' );
$priv->backend = $backend;
$session->clear();
$this->assertSame( array(), $backend->data );
$this->assertTrue( $backend->dirty );
}
}

View file

@ -0,0 +1,78 @@
<?php
namespace MediaWiki\Session;
/**
* BagOStuff with utility functions for MediaWiki\\Session\\* testing
*/
class TestBagOStuff extends \HashBagOStuff {
/**
* @param string $id Session ID
* @param array $data Session data
* @param int $expiry Expiry
* @param User $user User for metadata
*/
public function setSessionData( $id, array $data, $expiry = 0, User $user = null ) {
$this->setSession( $id, array( 'data' => $data ), $expiry, $user );
}
/**
* @param string $id Session ID
* @param array $metadata Session metadata
* @param int $expiry Expiry
*/
public function setSessionMeta( $id, array $metadata, $expiry = 0 ) {
$this->setSession( $id, array( 'metadata' => $metadata ), $expiry );
}
/**
* @param string $id Session ID
* @param array $blob Session metadata and data
* @param int $expiry Expiry
* @param User $user User for metadata
*/
public function setSession( $id, array $blob, $expiry = 0, User $user = null ) {
$blob += array(
'data' => array(),
'metadata' => array(),
);
$blob['metadata'] += array(
'userId' => $user ? $user->getId() : 0,
'userName' => $user ? $user->getName() : null,
'userToken' => $user ? $user->getToken( true ) : null,
'provider' => 'DummySessionProvider',
);
$this->setRawSession( $id, $blob, $expiry, $user );
}
/**
* @param string $id Session ID
* @param array|mixed $blob Session metadata and data
* @param int $expiry Expiry
*/
public function setRawSession( $id, $blob, $expiry = 0 ) {
if ( $expiry <= 0 ) {
$expiry = \RequestContext::getMain()->getConfig()->get( 'ObjectCacheSessionExpiry' );
}
$this->set( wfMemcKey( 'MWSession', $id ), $blob, $expiry );
}
/**
* @param string $id Session ID
* @return mixed
*/
public function getSession( $id ) {
return $this->get( wfMemcKey( 'MWSession', $id ) );
}
/**
* @param string $id Session ID
*/
public function deleteSession( $id ) {
$this->delete( wfMemcKey( 'MWSession', $id ) );
}
}

View file

@ -0,0 +1,99 @@
<?php
namespace MediaWiki\Session;
/**
* Utility functions for Session unit tests
*/
class TestUtils {
/**
* Override the singleton for unit testing
* @param SessionManager|null $manager
* @return \\ScopedCallback|null
*/
public static function setSessionManagerSingleton( SessionManager $manager = null ) {
session_write_close();
$rInstance = new \ReflectionProperty(
'MediaWiki\\Session\\SessionManager', 'instance'
);
$rInstance->setAccessible( true );
$rGlobalSession = new \ReflectionProperty(
'MediaWiki\\Session\\SessionManager', 'globalSession'
);
$rGlobalSession->setAccessible( true );
$rGlobalSessionRequest = new \ReflectionProperty(
'MediaWiki\\Session\\SessionManager', 'globalSessionRequest'
);
$rGlobalSessionRequest->setAccessible( true );
$oldInstance = $rInstance->getValue();
$reset = array(
array( $rInstance, $oldInstance ),
array( $rGlobalSession, $rGlobalSession->getValue() ),
array( $rGlobalSessionRequest, $rGlobalSessionRequest->getValue() ),
);
$rInstance->setValue( $manager );
$rGlobalSession->setValue( null );
$rGlobalSessionRequest->setValue( null );
if ( $manager && PHPSessionHandler::isInstalled() ) {
PHPSessionHandler::install( $manager );
}
return new \ScopedCallback( function () use ( &$reset, $oldInstance ) {
foreach ( $reset as &$arr ) {
$arr[0]->setValue( $arr[1] );
}
if ( $oldInstance && PHPSessionHandler::isInstalled() ) {
PHPSessionHandler::install( $oldInstance );
}
} );
}
/**
* If you need a SessionBackend for testing but don't want to create a real
* one, use this.
* @return SessionBackend Unconfigured! Use reflection to set any private
* fields necessary.
*/
public static function getDummySessionBackend() {
$rc = new \ReflectionClass( 'MediaWiki\\Session\\SessionBackend' );
if ( !method_exists( $rc, 'newInstanceWithoutConstructor' ) ) {
\PHPUnit_Framework_Assert::markTestSkipped(
'ReflectionClass::newInstanceWithoutConstructor isn\'t available'
);
}
return $rc->newInstanceWithoutConstructor();
}
/**
* If you need a Session for testing but don't want to create a backend to
* construct one, use this.
* @param object $backend Object to serve as the SessionBackend
* @param int $index Index
* @return Session
*/
public static function getDummySession( $backend = null, $index = -1 ) {
$rc = new \ReflectionClass( 'MediaWiki\\Session\\Session' );
if ( !method_exists( $rc, 'newInstanceWithoutConstructor' ) ) {
\PHPUnit_Framework_Assert::markTestSkipped(
'ReflectionClass::newInstanceWithoutConstructor isn\'t available'
);
}
if ( $backend === null ) {
$backend = new DummySessionBackend;
}
$session = $rc->newInstanceWithoutConstructor();
$priv = \TestingAccessWrapper::newFromObject( $session );
$priv->backend = $backend;
$priv->index = $index;
return $session;
}
}

View file

@ -0,0 +1,186 @@
<?php
namespace MediaWiki\Session;
use MediaWikiTestCase;
use User;
/**
* @group Session
* @group Database
* @covers MediaWiki\Session\UserInfo
*/
class UserInfoTest extends MediaWikiTestCase {
public function testNewAnonymous() {
$userinfo = UserInfo::newAnonymous();
$this->assertTrue( $userinfo->isAnon() );
$this->assertTrue( $userinfo->isVerified() );
$this->assertSame( 0, $userinfo->getId() );
$this->assertSame( null, $userinfo->getName() );
$this->assertSame( null, $userinfo->getToken() );
$this->assertNotNull( $userinfo->getUser() );
$this->assertSame( $userinfo, $userinfo->verified() );
$this->assertSame( '<anon>', (string)$userinfo );
}
public function testNewFromId() {
$id = wfGetDB( DB_MASTER )->selectField( 'user', 'MAX(user_id)' ) + 1;
try {
UserInfo::newFromId( $id );
$this->fail( 'Expected exception not thrown' );
} catch ( \InvalidArgumentException $ex ) {
$this->assertSame( 'Invalid ID', $ex->getMessage() );
}
$user = User::newFromName( 'UTSysop' );
$userinfo = UserInfo::newFromId( $user->getId() );
$this->assertFalse( $userinfo->isAnon() );
$this->assertFalse( $userinfo->isVerified() );
$this->assertSame( $user->getId(), $userinfo->getId() );
$this->assertSame( $user->getName(), $userinfo->getName() );
$this->assertSame( $user->getToken( true ), $userinfo->getToken() );
$this->assertInstanceOf( 'User', $userinfo->getUser() );
$userinfo2 = $userinfo->verified();
$this->assertNotSame( $userinfo2, $userinfo );
$this->assertSame( "<-:{$user->getId()}:{$user->getName()}>", (string)$userinfo );
$this->assertFalse( $userinfo2->isAnon() );
$this->assertTrue( $userinfo2->isVerified() );
$this->assertSame( $user->getId(), $userinfo2->getId() );
$this->assertSame( $user->getName(), $userinfo2->getName() );
$this->assertSame( $user->getToken( true ), $userinfo2->getToken() );
$this->assertInstanceOf( 'User', $userinfo2->getUser() );
$this->assertSame( $userinfo2, $userinfo2->verified() );
$this->assertSame( "<+:{$user->getId()}:{$user->getName()}>", (string)$userinfo2 );
$userinfo = UserInfo::newFromId( $user->getId(), true );
$this->assertTrue( $userinfo->isVerified() );
$this->assertSame( $userinfo, $userinfo->verified() );
}
public function testNewFromName() {
try {
UserInfo::newFromName( '<bad name>' );
$this->fail( 'Expected exception not thrown' );
} catch ( \InvalidArgumentException $ex ) {
$this->assertSame( 'Invalid user name', $ex->getMessage() );
}
// User name that exists
$user = User::newFromName( 'UTSysop' );
$userinfo = UserInfo::newFromName( $user->getName() );
$this->assertFalse( $userinfo->isAnon() );
$this->assertFalse( $userinfo->isVerified() );
$this->assertSame( $user->getId(), $userinfo->getId() );
$this->assertSame( $user->getName(), $userinfo->getName() );
$this->assertSame( $user->getToken( true ), $userinfo->getToken() );
$this->assertInstanceOf( 'User', $userinfo->getUser() );
$userinfo2 = $userinfo->verified();
$this->assertNotSame( $userinfo2, $userinfo );
$this->assertSame( "<-:{$user->getId()}:{$user->getName()}>", (string)$userinfo );
$this->assertFalse( $userinfo2->isAnon() );
$this->assertTrue( $userinfo2->isVerified() );
$this->assertSame( $user->getId(), $userinfo2->getId() );
$this->assertSame( $user->getName(), $userinfo2->getName() );
$this->assertSame( $user->getToken( true ), $userinfo2->getToken() );
$this->assertInstanceOf( 'User', $userinfo2->getUser() );
$this->assertSame( $userinfo2, $userinfo2->verified() );
$this->assertSame( "<+:{$user->getId()}:{$user->getName()}>", (string)$userinfo2 );
$userinfo = UserInfo::newFromName( $user->getName(), true );
$this->assertTrue( $userinfo->isVerified() );
$this->assertSame( $userinfo, $userinfo->verified() );
// User name that does not exist should still be non-anon
$user = User::newFromName( 'DoesNotExist' );
$this->assertSame( 0, $user->getId(), 'sanity check' );
$userinfo = UserInfo::newFromName( $user->getName() );
$this->assertFalse( $userinfo->isAnon() );
$this->assertFalse( $userinfo->isVerified() );
$this->assertSame( $user->getId(), $userinfo->getId() );
$this->assertSame( $user->getName(), $userinfo->getName() );
$this->assertSame( null, $userinfo->getToken() );
$this->assertInstanceOf( 'User', $userinfo->getUser() );
$userinfo2 = $userinfo->verified();
$this->assertNotSame( $userinfo2, $userinfo );
$this->assertSame( "<-:{$user->getId()}:{$user->getName()}>", (string)$userinfo );
$this->assertFalse( $userinfo2->isAnon() );
$this->assertTrue( $userinfo2->isVerified() );
$this->assertSame( $user->getId(), $userinfo2->getId() );
$this->assertSame( $user->getName(), $userinfo2->getName() );
$this->assertSame( null, $userinfo2->getToken() );
$this->assertInstanceOf( 'User', $userinfo2->getUser() );
$this->assertSame( $userinfo2, $userinfo2->verified() );
$this->assertSame( "<+:{$user->getId()}:{$user->getName()}>", (string)$userinfo2 );
$userinfo = UserInfo::newFromName( $user->getName(), true );
$this->assertTrue( $userinfo->isVerified() );
$this->assertSame( $userinfo, $userinfo->verified() );
}
public function testNewFromUser() {
// User that exists
$user = User::newFromName( 'UTSysop' );
$userinfo = UserInfo::newFromUser( $user );
$this->assertFalse( $userinfo->isAnon() );
$this->assertFalse( $userinfo->isVerified() );
$this->assertSame( $user->getId(), $userinfo->getId() );
$this->assertSame( $user->getName(), $userinfo->getName() );
$this->assertSame( $user->getToken( true ), $userinfo->getToken() );
$this->assertSame( $user, $userinfo->getUser() );
$userinfo2 = $userinfo->verified();
$this->assertNotSame( $userinfo2, $userinfo );
$this->assertSame( "<-:{$user->getId()}:{$user->getName()}>", (string)$userinfo );
$this->assertFalse( $userinfo2->isAnon() );
$this->assertTrue( $userinfo2->isVerified() );
$this->assertSame( $user->getId(), $userinfo2->getId() );
$this->assertSame( $user->getName(), $userinfo2->getName() );
$this->assertSame( $user->getToken( true ), $userinfo2->getToken() );
$this->assertSame( $user, $userinfo2->getUser() );
$this->assertSame( $userinfo2, $userinfo2->verified() );
$this->assertSame( "<+:{$user->getId()}:{$user->getName()}>", (string)$userinfo2 );
$userinfo = UserInfo::newFromUser( $user, true );
$this->assertTrue( $userinfo->isVerified() );
$this->assertSame( $userinfo, $userinfo->verified() );
// User name that does not exist should still be non-anon
$user = User::newFromName( 'DoesNotExist' );
$this->assertSame( 0, $user->getId(), 'sanity check' );
$userinfo = UserInfo::newFromUser( $user );
$this->assertFalse( $userinfo->isAnon() );
$this->assertFalse( $userinfo->isVerified() );
$this->assertSame( $user->getId(), $userinfo->getId() );
$this->assertSame( $user->getName(), $userinfo->getName() );
$this->assertSame( null, $userinfo->getToken() );
$this->assertSame( $user, $userinfo->getUser() );
$userinfo2 = $userinfo->verified();
$this->assertNotSame( $userinfo2, $userinfo );
$this->assertSame( "<-:{$user->getId()}:{$user->getName()}>", (string)$userinfo );
$this->assertFalse( $userinfo2->isAnon() );
$this->assertTrue( $userinfo2->isVerified() );
$this->assertSame( $user->getId(), $userinfo2->getId() );
$this->assertSame( $user->getName(), $userinfo2->getName() );
$this->assertSame( null, $userinfo2->getToken() );
$this->assertSame( $user, $userinfo2->getUser() );
$this->assertSame( $userinfo2, $userinfo2->verified() );
$this->assertSame( "<+:{$user->getId()}:{$user->getName()}>", (string)$userinfo2 );
$userinfo = UserInfo::newFromUser( $user, true );
$this->assertTrue( $userinfo->isVerified() );
$this->assertSame( $userinfo, $userinfo->verified() );
// Anonymous user gives anon
$userinfo = UserInfo::newFromUser( new User, false );
$this->assertTrue( $userinfo->isVerified() );
$this->assertSame( 0, $userinfo->getId() );
$this->assertSame( null, $userinfo->getName() );
}
}

View file

@ -16,7 +16,6 @@ class UploadFromUrlTest extends ApiTestCase {
'wgAllowCopyUploads' => true,
'wgAllowAsyncCopyUploads' => true,
) );
wfSetupSession();
if ( wfLocalFile( 'UploadFromUrlTest.png' )->exists() ) {
$this->deleteFile( 'UploadFromUrlTest.png' );
@ -26,15 +25,12 @@ class UploadFromUrlTest extends ApiTestCase {
protected function doApiRequest( array $params, array $unused = null,
$appendModule = false, User $user = null
) {
$sessionId = session_id();
session_write_close();
global $wgRequest;
$req = new FauxRequest( $params, true, $_SESSION );
$req = new FauxRequest( $params, true, $wgRequest->getSession() );
$module = new ApiMain( $req, true );
$module->execute();
wfSetupSession( $sessionId );
return array(
$module->getResult()->getResultData( null, array( 'Strip' => 'all' ) ),
$req

View file

@ -446,89 +446,4 @@ class UserTest extends MediaWikiTestCase {
$this->assertGreaterThan(
$touched, $user->getDBTouched(), "user_touched increased with casOnTouched() #2" );
}
public static function setExtendedLoginCookieDataProvider() {
$data = array();
$now = time();
$secondsInDay = 86400;
// Arbitrary durations, in units of days, to ensure it chooses the
// right one. There is a 5-minute grace period (see testSetExtendedLoginCookie)
// to work around slow tests, since we're not currently mocking time() for PHP.
$durationOne = $secondsInDay * 5;
$durationTwo = $secondsInDay * 29;
$durationThree = $secondsInDay * 17;
// If $wgExtendedLoginCookieExpiration is null, then the expiry passed to
// set cookie is time() + $wgCookieExpiration
$data[] = array(
null,
$durationOne,
$now + $durationOne,
);
// If $wgExtendedLoginCookieExpiration isn't null, then the expiry passed to
// set cookie is $now + $wgExtendedLoginCookieExpiration
$data[] = array(
$durationTwo,
$durationThree,
$now + $durationTwo,
);
return $data;
}
/**
* @dataProvider setExtendedLoginCookieDataProvider
* @covers User::getRequest
* @covers User::setCookie
* @backupGlobals enabled
*/
public function testSetExtendedLoginCookie(
$extendedLoginCookieExpiration,
$cookieExpiration,
$expectedExpiry
) {
$this->setMwGlobals( array(
'wgExtendedLoginCookieExpiration' => $extendedLoginCookieExpiration,
'wgCookieExpiration' => $cookieExpiration,
) );
$response = $this->getMock( 'WebResponse' );
$setcookieSpy = $this->any();
$response->expects( $setcookieSpy )
->method( 'setcookie' );
$request = new MockWebRequest( $response );
$user = new UserProxy( User::newFromSession( $request ) );
$user->setExtendedLoginCookie( 'name', 'value', true );
$setcookieInvocations = $setcookieSpy->getInvocations();
$setcookieInvocation = end( $setcookieInvocations );
$actualExpiry = $setcookieInvocation->parameters[2];
// TODO: ± 600 seconds compensates for
// slow-running tests. However, the dependency on the time
// function should be removed. This requires some way
// to mock/isolate User->setExtendedLoginCookie's call to time()
$this->assertEquals( $expectedExpiry, $actualExpiry, '', 600 );
}
}
class UserProxy extends User {
/**
* @var User
*/
protected $user;
public function __construct( User $user ) {
$this->user = $user;
}
public function setExtendedLoginCookie( $name, $value, $secure ) {
$this->user->setExtendedLoginCookie( $name, $value, $secure );
}
}

View file

@ -0,0 +1,29 @@
<?php
namespace MediaWiki\Session;
/**
* Dummy session backend
*
* This isn't a real backend, but implements some methods that SessionBackend
* does so tests can run.
*/
class DummySessionBackend {
public $data = array(
'foo' => 1,
'bar' => 2,
0 => 'zero',
);
public $dirty = false;
public function &getData() {
return $this->data;
}
public function dirty() {
$this->dirty = true;
}
public function deregisterSession( $index ) {
}
}

View file

@ -0,0 +1,60 @@
<?php
use MediaWiki\Session\SessionProvider;
use MediaWiki\Session\SessionInfo;
use MediaWiki\Session\SessionBackend;
use MediaWiki\Session\UserInfo;
/**
* Dummy session provider
*
* An implementation of a session provider that doesn't actually do anything.
*/
class DummySessionProvider extends SessionProvider {
const ID = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa';
public function provideSessionInfo( WebRequest $request ) {
return new SessionInfo( SessionInfo::MIN_PRIORITY, array(
'provider' => $this,
'id' => self::ID,
'persisted' => true,
'userInfo' => UserInfo::newAnonymous(),
) );
}
public function newSessionInfo( $id = null ) {
return new SessionInfo( SessionInfo::MIN_PRIORITY, array(
'id' => $id,
'idIsSafe' => true,
'provider' => $this,
'persisted' => false,
'userInfo' => UserInfo::newAnonymous(),
) );
}
public function persistsSessionId() {
return true;
}
public function canChangeUser() {
return $this->persistsSessionId();
}
public function persistSession( SessionBackend $session, WebRequest $request ) {
}
public function unpersistSession( WebRequest $request ) {
}
public function immutableSessionCouldExistForUser( $user ) {
return false;
}
public function preventImmutableSessionsForUser( $user ) {
}
public function suggestLoginUsername( WebRequest $request ) {
return $request->getCookie( 'UserName' );
}
}

View file

@ -73,6 +73,7 @@ class PHPUnitMaintClass extends Maintenance {
global $wgLanguageConverterCacheType, $wgUseDatabaseMessages;
global $wgLocaltimezone, $wgLocalisationCacheConf;
global $wgDevelopmentWarnings;
global $wgSessionProviders;
// Inject test autoloader
require_once __DIR__ . '/../TestsAutoLoader.php';
@ -103,6 +104,19 @@ class PHPUnitMaintClass extends Maintenance {
$wgLocalisationCacheConf['storeClass'] = 'LCStoreNull';
// Generic MediaWiki\Session\SessionManager configuration for tests
// We use CookieSessionProvider because things might be expecting
// cookies to show up in a FauxRequest somewhere.
$wgSessionProviders = array(
array(
'class' => 'MediaWiki\\Session\\CookieSessionProvider',
'args' => array( array(
'priority' => 30,
'callUserSetCookiesHook' => true,
) ),
),
);
// Bug 44192 Do not attempt to send a real e-mail
Hooks::clear( 'AlternateUserMailer' );
Hooks::register(