$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
* 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 ===
* 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 ====
'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',
'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',
"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",
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).
$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
$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
'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.
$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.
*/
*
* @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
*/
*/
$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 }
/************************************************************************//**
return $this->base->getAllHeaders();
}
+ public function getSession() {
+ return $this->base->getSession();
+ }
+
public function getSessionData( $key ) {
return $this->base->getSessionData( $key );
}
* @file
*/
+use MediaWiki\Session\SessionManager;
+
/**
* WebRequest clone which takes values from a provided array.
*
*/
class FauxRequest extends WebRequest {
private $wasPosted = false;
- private $session = array();
private $requestUrl;
protected $cookies = array();
* @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
*/
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;
}
}
}
- public function checkSessionCookie() {
- return false;
- }
-
/**
* @since 1.25
*/
}
/**
- * @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
use Liuggio\StatsdClient\Sender\SocketSender;
use MediaWiki\Logger\LoggerFactory;
+use MediaWiki\Session\SessionManager;
// Hide compatibility functions from Doxygen
/// @cond
/**
* 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' )
}
/**
- * 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' );
}
/**
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.
(
*/
use MediaWiki\Logger\LoggerFactory;
+use MediaWiki\Session\SessionManager;
use WrappedString\WrappedString;
/**
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' )
);
* @return string
*/
public function getVaryHeader() {
+ foreach ( SessionManager::singleton()->getVaryHeaders() as $header => $options ) {
+ $this->addVaryHeader( $header, $options );
+ }
return 'Vary: ' . join( ', ', array_keys( $this->mVaryHeader ) );
}
}
$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;
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
$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 );
// 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' );
/**
// 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
*/
*/
$wgParser = new StubObject( 'wgParser', $wgParserConf['class'], array( $wgParserConf ) );
-if ( !is_object( $wgAuth ) ) {
- $wgAuth = new AuthPlugin;
- Hooks::run( 'AuthPluginSetup', array( &$wgAuth ) );
-}
-
/**
* @var Title $wgTitle
*/
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;
* @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
*/
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 );
}
/**
- * 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;
}
/**
}
/**
- * 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 );
}
/**
*/
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
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.
$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 ) . '"' );
+ }
}
}
*/
class FauxResponse extends WebResponse {
private $headers;
- private $cookies;
+ private $cookies = array();
private $code;
/**
$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.
$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';
}
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();
}
$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' );
$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() );
// 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:
// @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:
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();
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" );
return array(
'ip' => $this->getRequest()->getIP(),
'headers' => $this->getRequest()->getAllHeaders(),
- 'sessionId' => session_id(),
+ 'sessionId' => MediaWiki\Session\SessionManager::getGlobalSession()->getId(),
'userId' => $this->getUser()->getId()
);
}
* @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." );
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 );
$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;
)->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();
}
}
* 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 );
}
}
+++ /dev/null
-<?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() );
- }
- }
-}
--- /dev/null
+<?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' );
+ }
+
+}
--- /dev/null
+<?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' );
+ }
+}
--- /dev/null
+<?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();
+ }
+ }
+
+}
--- /dev/null
+<?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;
+ }
+
+ /**@}*/
+
+}
--- /dev/null
+<?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' );
+ }
+ }
+ }
+
+}
--- /dev/null
+<?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;
+ }
+
+}
--- /dev/null
+<?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();
+ }
+
+}
--- /dev/null
+<?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;
+ }
+
+ /**@}*/
+
+}
--- /dev/null
+<?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();
+
+}
--- /dev/null
+<?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 );
+ }
+
+}
--- /dev/null
+<?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();
+
+}
--- /dev/null
+<?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() .
+ '>';
+ }
+
+}
* @ingroup SpecialPage
*/
use MediaWiki\Logger\LoggerFactory;
+use MediaWiki\Session\SessionManager;
/**
* Implements Special:UserLogin
* @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();
}
$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
if ( $user->isLoggedIn() ) {
$this->mUsername = $user->getName();
} else {
- $this->mUsername = $this->getRequest()->getCookie( 'UserName' );
+ $this->mUsername = $this->getRequest()->getSession()->suggestLoginUsername();
}
}
function hasSessionCookie() {
global $wgDisableCookieCheck;
- return $wgDisableCookieCheck ? true : $this->getRequest()->checkSessionCookie();
+ return $wgDisableCookieCheck ||
+ SessionManager::singleton()->getPersistedSessionId( $this->getRequest() ) !== null;
}
/**
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 ) );
}
$wgCookieSecure = false;
}
- wfResetSessionID();
+ MediaWiki\Session\SessionManager::getGlobalSession()->resetId();
}
/**
$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();
'userName' => $user->getName(),
'leaveMessage' => $this->mAsync == 'async-leavemessage',
'ignoreWarnings' => $this->mIgnoreWarnings,
- 'sessionId' => session_id(),
+ 'sessionId' => MediaWiki\Session\SessionManager::getGlobalSession()->getId(),
'sessionKey' => $sessionKey,
) );
$job->initializeSessionData();
* @file
*/
+use MediaWiki\Session\SessionManager;
+
/**
* String Some punctuation to prevent editing from broken text-mangling proxies.
* @ingroup Constants
'apihighlimits',
'applychangetags',
'autoconfirmed',
+ 'autocreateaccount',
'autopatrol',
'bigdelete',
'block',
* - '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.
*/
* @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();
}
/**
- * 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
$user->saveSettings();
}
+ SessionManager::singleton()->preventSessionsForUser( $user->getName() );
+
return $user;
}
$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
}
/**
- * 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;
}
/**
/**
* 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;
protected function setCookie(
$name, $value, $exp = 0, $secure = null, $params = array(), $request = null
) {
+ wfDeprecated( __METHOD__, '1.27' );
if ( $request === null ) {
$request = $this->getRequest();
}
/**
* 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
* @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 );
}
*
* @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
protected function setExtendedLoginCookie( $name, $value, $secure ) {
global $wgExtendedLoginCookieExpiration, $wgCookieExpiration;
+ wfDeprecated( __METHOD__, '1.27' );
+
$exp = time();
$exp += $wgExtendedLoginCookieExpiration !== null
? $wgExtendedLoginCookieExpiration
}
/**
- * 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.
* @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 = 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;
- }
- Hooks::run( 'UserSetCookies', array( $this, &$session, &$cookies ) );
-
- foreach ( $session as $name => $value ) {
- $request->setSessionData( $name, $value );
+ $session = $this->getRequest()->getSession();
+ if ( $request && $session->getRequest() !== $request ) {
+ $session = $session->sessionWithRequest( $request );
}
- 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 );
+ $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->setUser( $this );
}
- /**
- * 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->setRememberUser( $rememberMe );
+ if ( $secure !== null ) {
+ $session->setForceHTTPS( $secure );
}
+
+ $session->persist();
+
+ ScopedCallback::consume( $delay );
}
/**
}
/**
- * 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 );
+ }
}
/**
"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:",
"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.",
"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",
"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",
"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."
}
"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}}",
"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.",
"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}}",
"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}}",
"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."
}
# 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",
'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",
'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",
}
protected function tearDown() {
+ global $wgRequest;
+
$status = ob_get_status();
if ( isset( $status['name'] ) &&
$status['name'] === 'MediaWikiTestCase::wfResetOutputBuffersBarrier'
$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' ) );
false,
$user
);
+
+ // doEditContent() probably started the session via
+ // User::loadFromSession(). Close it now.
+ if ( session_id() !== '' ) {
+ session_write_close();
+ session_id( '' );
+ }
}
}
--- /dev/null
+<?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;
+ }
+ }
+ }
+}
$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;
protected function tearDown() {
// Avoid leaking session over tests
- if ( session_id() != '' ) {
- global $wgUser;
- $wgUser->logout();
- session_destroy();
- }
+ MediaWiki\Session\SessionManager::getGlobalSession()->clear();
parent::tearDown();
}
'wgEnableAPI' => true,
) );
- wfSetupSession();
-
$this->clearFakeUploads();
}
* @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();
$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(
--- /dev/null
+<?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 ) {
+ }
+
+}
--- /dev/null
+<?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', '' ) );
+ }
+
+}
--- /dev/null
+<?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 ) ),
+ );
+ }
+
+}
--- /dev/null
+<?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();
+ }
+
+}
--- /dev/null
+<?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 );
+ }
+
+}
--- /dev/null
+<?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, '==' );
+ }
+}
--- /dev/null
+<?php
+
+namespace MediaWiki\Session;
+
+use Psr\Log\LogLevel;
+use MediaWikiTestCase;
+use User;
+
+/**
+ * @group Session
+ * @group Database
+ * @covers MediaWiki\Session\SessionManager
+ */
+class SessionManagerTest extends MediaWikiTestCase {
+
+ protected $config, $logger, $store;
+
+ protected function getManager() {
+ \ObjectCache::$instances['testSessionStore'] = new TestBagOStuff();
+ $this->config = new \HashConfig( array(
+ 'LanguageCode' => 'en',
+ 'SessionCacheType' => 'testSessionStore',
+ 'ObjectCacheSessionExpiry' => 100,
+ 'SessionProviders' => array(
+ array( 'class' => 'DummySessionProvider' ),
+ )
+ ) );
+ $this->logger = new \TestLogger( false, function ( $m ) {
+ return substr( $m, 0, 15 ) === 'SessionBackend ' ? null : $m;
+ } );
+ $this->store = new TestBagOStuff();
+
+ return new SessionManager( array(
+ 'config' => $this->config,
+ 'logger' => $this->logger,
+ 'store' => $this->store,
+ ) );
+ }
+
+ protected function objectCacheDef( $object ) {
+ return array( 'factory' => function () use ( $object ) {
+ return $object;
+ } );
+ }
+
+ public function testSingleton() {
+ $reset = TestUtils::setSessionManagerSingleton( null );
+
+ $singleton = SessionManager::singleton();
+ $this->assertInstanceOf( 'MediaWiki\\Session\\SessionManager', $singleton );
+ $this->assertSame( $singleton, SessionManager::singleton() );
+ }
+
+ public function testGetGlobalSession() {
+ $context = \RequestContext::getMain();
+
+ if ( !PHPSessionHandler::isInstalled() ) {
+ PHPSessionHandler::install( SessionManager::singleton() );
+ }
+ $rProp = new \ReflectionProperty( 'MediaWiki\\Session\\PHPSessionHandler', 'instance' );
+ $rProp->setAccessible( true );
+ $handler = \TestingAccessWrapper::newFromObject( $rProp->getValue() );
+ $oldEnable = $handler->enable;
+ $reset[] = new \ScopedCallback( function () use ( $handler, $oldEnable ) {
+ if ( $handler->enable ) {
+ session_write_close();
+ }
+ $handler->enable = $oldEnable;
+ } );
+ $reset[] = TestUtils::setSessionManagerSingleton( $this->getManager() );
+
+ $handler->enable = true;
+ $request = new \FauxRequest();
+ $context->setRequest( $request );
+ $id = $request->getSession()->getId();
+
+ session_id( '' );
+ $session = SessionManager::getGlobalSession();
+ $this->assertSame( $id, $session->getId() );
+
+ session_id( 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx' );
+ $session = SessionManager::getGlobalSession();
+ $this->assertSame( 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx', $session->getId() );
+ $this->assertSame( 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx', $request->getSession()->getId() );
+
+ session_write_close();
+ $handler->enable = false;
+ $request = new \FauxRequest();
+ $context->setRequest( $request );
+ $id = $request->getSession()->getId();
+
+ session_id( '' );
+ $session = SessionManager::getGlobalSession();
+ $this->assertSame( $id, $session->getId() );
+
+ session_id( 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx' );
+ $session = SessionManager::getGlobalSession();
+ $this->assertSame( $id, $session->getId() );
+ $this->assertSame( $id, $request->getSession()->getId() );
+ }
+
+ public function testConstructor() {
+ $manager = \TestingAccessWrapper::newFromObject( $this->getManager() );
+ $this->assertSame( $this->config, $manager->config );
+ $this->assertSame( $this->logger, $manager->logger );
+ $this->assertSame( $this->store, $manager->store );
+
+ $manager = \TestingAccessWrapper::newFromObject( new SessionManager() );
+ $this->assertSame( \RequestContext::getMain()->getConfig(), $manager->config );
+
+ $manager = \TestingAccessWrapper::newFromObject( new SessionManager( array(
+ 'config' => $this->config,
+ ) ) );
+ $this->assertSame( \ObjectCache::$instances['testSessionStore'], $manager->store );
+
+ foreach ( array(
+ 'config' => '$options[\'config\'] must be an instance of Config',
+ 'logger' => '$options[\'logger\'] must be an instance of LoggerInterface',
+ 'store' => '$options[\'store\'] must be an instance of BagOStuff',
+ ) as $key => $error ) {
+ try {
+ new SessionManager( array( $key => new \stdClass ) );
+ $this->fail( 'Expected exception not thrown' );
+ } catch ( \InvalidArgumentException $ex ) {
+ $this->assertSame( $error, $ex->getMessage() );
+ }
+ }
+ }
+
+ public function testGetSessionForRequest() {
+ $manager = $this->getManager();
+ $request = new \FauxRequest();
+
+ $id1 = '';
+ $id2 = '';
+ $idEmpty = 'empty-session-------------------';
+
+ $providerBuilder = $this->getMockBuilder( 'DummySessionProvider' )
+ ->setMethods(
+ array( 'provideSessionInfo', 'newSessionInfo', '__toString', 'describe' )
+ );
+
+ $provider1 = $providerBuilder->getMock();
+ $provider1->expects( $this->any() )->method( 'provideSessionInfo' )
+ ->with( $this->identicalTo( $request ) )
+ ->will( $this->returnCallback( function ( $request ) {
+ return $request->info1;
+ } ) );
+ $provider1->expects( $this->any() )->method( 'newSessionInfo' )
+ ->will( $this->returnCallback( function () use ( $idEmpty, $provider1 ) {
+ return new SessionInfo( SessionInfo::MIN_PRIORITY, array(
+ 'provider' => $provider1,
+ 'id' => $idEmpty,
+ 'persisted' => true,
+ 'idIsSafe' => true,
+ ) );
+ } ) );
+ $provider1->expects( $this->any() )->method( '__toString' )
+ ->will( $this->returnValue( 'Provider1' ) );
+ $provider1->expects( $this->any() )->method( 'describe' )
+ ->will( $this->returnValue( '#1 sessions' ) );
+
+ $provider2 = $providerBuilder->getMock();
+ $provider2->expects( $this->any() )->method( 'provideSessionInfo' )
+ ->with( $this->identicalTo( $request ) )
+ ->will( $this->returnCallback( function ( $request ) {
+ return $request->info2;
+ } ) );
+ $provider2->expects( $this->any() )->method( '__toString' )
+ ->will( $this->returnValue( 'Provider2' ) );
+ $provider2->expects( $this->any() )->method( 'describe' )
+ ->will( $this->returnValue( '#2 sessions' ) );
+
+ $this->config->set( 'SessionProviders', array(
+ $this->objectCacheDef( $provider1 ),
+ $this->objectCacheDef( $provider2 ),
+ ) );
+
+ // No provider returns info
+ $request->info1 = null;
+ $request->info2 = null;
+ $session = $manager->getSessionForRequest( $request );
+ $this->assertInstanceOf( 'MediaWiki\\Session\\Session', $session );
+ $this->assertSame( $idEmpty, $session->getId() );
+ $this->assertNull( $manager->getPersistedSessionId( $request ) );
+
+ // Both providers return info, picks best one
+ $request->info1 = new SessionInfo( SessionInfo::MIN_PRIORITY + 1, array(
+ 'provider' => $provider1,
+ 'id' => ( $id1 = $manager->generateSessionId() ),
+ 'persisted' => true,
+ 'idIsSafe' => true,
+ ) );
+ $request->info2 = new SessionInfo( SessionInfo::MIN_PRIORITY + 2, array(
+ 'provider' => $provider2,
+ 'id' => ( $id2 = $manager->generateSessionId() ),
+ 'persisted' => true,
+ 'idIsSafe' => true,
+ ) );
+ $session = $manager->getSessionForRequest( $request );
+ $this->assertInstanceOf( 'MediaWiki\\Session\\Session', $session );
+ $this->assertSame( $id2, $session->getId() );
+ $this->assertSame( $id2, $manager->getPersistedSessionId( $request ) );
+
+ $request->info1 = new SessionInfo( SessionInfo::MIN_PRIORITY + 2, array(
+ 'provider' => $provider1,
+ 'id' => ( $id1 = $manager->generateSessionId() ),
+ 'persisted' => true,
+ 'idIsSafe' => true,
+ ) );
+ $request->info2 = new SessionInfo( SessionInfo::MIN_PRIORITY + 1, array(
+ 'provider' => $provider2,
+ 'id' => ( $id2 = $manager->generateSessionId() ),
+ 'persisted' => true,
+ 'idIsSafe' => true,
+ ) );
+ $session = $manager->getSessionForRequest( $request );
+ $this->assertInstanceOf( 'MediaWiki\\Session\\Session', $session );
+ $this->assertSame( $id1, $session->getId() );
+ $this->assertSame( $id1, $manager->getPersistedSessionId( $request ) );
+
+ // Tied priorities
+ $request->info1 = new SessionInfo( SessionInfo::MAX_PRIORITY, array(
+ 'provider' => $provider1,
+ 'id' => ( $id1 = $manager->generateSessionId() ),
+ 'persisted' => true,
+ 'userInfo' => UserInfo::newAnonymous(),
+ 'idIsSafe' => true,
+ ) );
+ $request->info2 = new SessionInfo( SessionInfo::MAX_PRIORITY, array(
+ 'provider' => $provider2,
+ 'id' => ( $id2 = $manager->generateSessionId() ),
+ 'persisted' => true,
+ 'userInfo' => UserInfo::newAnonymous(),
+ 'idIsSafe' => true,
+ ) );
+ try {
+ $manager->getSessionForRequest( $request );
+ $this->fail( 'Expcected exception not thrown' );
+ } catch ( \OverFlowException $ex ) {
+ $this->assertStringStartsWith(
+ 'Multiple sessions for this request tied for top priority: ',
+ $ex->getMessage()
+ );
+ $this->assertCount( 2, $ex->sessionInfos );
+ $this->assertContains( $request->info1, $ex->sessionInfos );
+ $this->assertContains( $request->info2, $ex->sessionInfos );
+ }
+ try {
+ $manager->getPersistedSessionId( $request );
+ $this->fail( 'Expcected exception not thrown' );
+ } catch ( \OverFlowException $ex ) {
+ $this->assertStringStartsWith(
+ 'Multiple sessions for this request tied for top priority: ',
+ $ex->getMessage()
+ );
+ $this->assertCount( 2, $ex->sessionInfos );
+ $this->assertContains( $request->info1, $ex->sessionInfos );
+ $this->assertContains( $request->info2, $ex->sessionInfos );
+ }
+
+ // Bad provider
+ $request->info1 = new SessionInfo( SessionInfo::MAX_PRIORITY, array(
+ 'provider' => $provider2,
+ 'id' => ( $id1 = $manager->generateSessionId() ),
+ 'persisted' => true,
+ 'idIsSafe' => true,
+ ) );
+ $request->info2 = null;
+ try {
+ $manager->getSessionForRequest( $request );
+ $this->fail( 'Expcected exception not thrown' );
+ } catch ( \UnexpectedValueException $ex ) {
+ $this->assertSame(
+ 'Provider1 returned session info for a different provider: ' . $request->info1,
+ $ex->getMessage()
+ );
+ }
+ try {
+ $manager->getPersistedSessionId( $request );
+ $this->fail( 'Expcected exception not thrown' );
+ } catch ( \UnexpectedValueException $ex ) {
+ $this->assertSame(
+ 'Provider1 returned session info for a different provider: ' . $request->info1,
+ $ex->getMessage()
+ );
+ }
+
+ // Unusable session info
+ $this->logger->setCollect( true );
+ $request->info1 = new SessionInfo( SessionInfo::MAX_PRIORITY, array(
+ 'provider' => $provider1,
+ 'id' => ( $id1 = $manager->generateSessionId() ),
+ 'persisted' => true,
+ 'userInfo' => UserInfo::newFromName( 'UTSysop', false ),
+ 'idIsSafe' => true,
+ ) );
+ $request->info2 = new SessionInfo( SessionInfo::MIN_PRIORITY, array(
+ 'provider' => $provider2,
+ 'id' => ( $id2 = $manager->generateSessionId() ),
+ 'persisted' => true,
+ 'idIsSafe' => true,
+ ) );
+ $session = $manager->getSessionForRequest( $request );
+ $this->assertInstanceOf( 'MediaWiki\\Session\\Session', $session );
+ $this->assertSame( $id2, $session->getId() );
+ $this->assertSame( $id2, $manager->getPersistedSessionId( $request ) );
+ $this->logger->setCollect( false );
+
+ // Unpersisted session ID
+ $request->info1 = new SessionInfo( SessionInfo::MAX_PRIORITY, array(
+ 'provider' => $provider1,
+ 'id' => ( $id1 = $manager->generateSessionId() ),
+ 'persisted' => false,
+ 'userInfo' => UserInfo::newFromName( 'UTSysop', true ),
+ 'idIsSafe' => true,
+ ) );
+ $request->info2 = null;
+ $session = $manager->getSessionForRequest( $request );
+ $this->assertInstanceOf( 'MediaWiki\\Session\\Session', $session );
+ $this->assertSame( $id1, $session->getId() );
+ $session->persist();
+ $this->assertTrue( $session->isPersistent(), 'sanity check' );
+ $this->assertNull( $manager->getPersistedSessionId( $request ) );
+ }
+
+ public function testGetSessionById() {
+ $manager = $this->getManager();
+
+ try {
+ $manager->getSessionById( 'bad' );
+ $this->fail( 'Expected exception not thrown' );
+ } catch ( \InvalidArgumentException $ex ) {
+ $this->assertSame( 'Invalid session ID', $ex->getMessage() );
+ }
+
+ // Unknown session ID
+ $id = $manager->generateSessionId();
+ $session = $manager->getSessionById( $id );
+ $this->assertInstanceOf( 'MediaWiki\\Session\\Session', $session );
+ $this->assertSame( $id, $session->getId() );
+
+ $id = $manager->generateSessionId();
+ $this->assertNull( $manager->getSessionById( $id, true ) );
+
+ // Known but unloadable session ID
+ $this->logger->setCollect( true );
+ $id = $manager->generateSessionId();
+ $this->store->setRawSession( $id, array( 'metadata' => array(
+ 'provider' => 'DummySessionProvider',
+ 'userId' => 0,
+ 'userName' => null,
+ 'userToken' => null,
+ ) ) );
+
+ try {
+ $manager->getSessionById( $id );
+ $this->fail( 'Expected exception not thrown' );
+ } catch ( \UnexpectedValueException $ex ) {
+ $this->assertSame(
+ 'Can neither load the session nor create an empty session',
+ $ex->getMessage()
+ );
+ }
+
+ $this->assertNull( $manager->getSessionById( $id, true ) );
+ $this->logger->setCollect( false );
+
+ // Known session ID
+ $this->store->setSession( $id, array() );
+ $session = $manager->getSessionById( $id );
+ $this->assertInstanceOf( 'MediaWiki\\Session\\Session', $session );
+ $this->assertSame( $id, $session->getId() );
+ }
+
+ public function testGetEmptySession() {
+ $manager = $this->getManager();
+ $pmanager = \TestingAccessWrapper::newFromObject( $manager );
+ $request = new \FauxRequest();
+
+ $providerBuilder = $this->getMockBuilder( 'DummySessionProvider' )
+ ->setMethods( array( 'provideSessionInfo', 'newSessionInfo', '__toString' ) );
+
+ $expectId = null;
+ $info1 = null;
+ $info2 = null;
+
+ $provider1 = $providerBuilder->getMock();
+ $provider1->expects( $this->any() )->method( 'provideSessionInfo' )
+ ->will( $this->returnValue( null ) );
+ $provider1->expects( $this->any() )->method( 'newSessionInfo' )
+ ->with( $this->callback( function ( $id ) use ( &$expectId ) {
+ return $id === $expectId;
+ } ) )
+ ->will( $this->returnCallback( function () use ( &$info1 ) {
+ return $info1;
+ } ) );
+ $provider1->expects( $this->any() )->method( '__toString' )
+ ->will( $this->returnValue( 'MockProvider1' ) );
+
+ $provider2 = $providerBuilder->getMock();
+ $provider2->expects( $this->any() )->method( 'provideSessionInfo' )
+ ->will( $this->returnValue( null ) );
+ $provider2->expects( $this->any() )->method( 'newSessionInfo' )
+ ->with( $this->callback( function ( $id ) use ( &$expectId ) {
+ return $id === $expectId;
+ } ) )
+ ->will( $this->returnCallback( function () use ( &$info2 ) {
+ return $info2;
+ } ) );
+ $provider1->expects( $this->any() )->method( '__toString' )
+ ->will( $this->returnValue( 'MockProvider2' ) );
+
+ $this->config->set( 'SessionProviders', array(
+ $this->objectCacheDef( $provider1 ),
+ $this->objectCacheDef( $provider2 ),
+ ) );
+
+ // No info
+ $expectId = null;
+ $info1 = null;
+ $info2 = null;
+ try {
+ $manager->getEmptySession();
+ $this->fail( 'Expected exception not thrown' );
+ } catch ( \UnexpectedValueException $ex ) {
+ $this->assertSame(
+ 'No provider could provide an empty session!',
+ $ex->getMessage()
+ );
+ }
+
+ // Info
+ $expectId = null;
+ $info1 = new SessionInfo( SessionInfo::MIN_PRIORITY, array(
+ 'provider' => $provider1,
+ 'id' => 'empty---------------------------',
+ 'persisted' => true,
+ 'idIsSafe' => true,
+ ) );
+ $info2 = null;
+ $session = $manager->getEmptySession();
+ $this->assertInstanceOf( 'MediaWiki\\Session\\Session', $session );
+ $this->assertSame( 'empty---------------------------', $session->getId() );
+
+ // Info, explicitly
+ $expectId = 'expected------------------------';
+ $info1 = new SessionInfo( SessionInfo::MIN_PRIORITY, array(
+ 'provider' => $provider1,
+ 'id' => $expectId,
+ 'persisted' => true,
+ 'idIsSafe' => true,
+ ) );
+ $info2 = null;
+ $session = $pmanager->getEmptySessionInternal( null, $expectId );
+ $this->assertInstanceOf( 'MediaWiki\\Session\\Session', $session );
+ $this->assertSame( $expectId, $session->getId() );
+
+ // Wrong ID
+ $expectId = 'expected-----------------------2';
+ $info1 = new SessionInfo( SessionInfo::MIN_PRIORITY, array(
+ 'provider' => $provider1,
+ 'id' => "un$expectId",
+ 'persisted' => true,
+ 'idIsSafe' => true,
+ ) );
+ $info2 = null;
+ try {
+ $pmanager->getEmptySessionInternal( null, $expectId );
+ $this->fail( 'Expected exception not thrown' );
+ } catch ( \UnexpectedValueException $ex ) {
+ $this->assertSame(
+ 'MockProvider1 returned empty session info with a wrong id: ' .
+ "un$expectId != $expectId",
+ $ex->getMessage()
+ );
+ }
+
+ // Unsafe ID
+ $expectId = 'expected-----------------------2';
+ $info1 = new SessionInfo( SessionInfo::MIN_PRIORITY, array(
+ 'provider' => $provider1,
+ 'id' => $expectId,
+ 'persisted' => true,
+ ) );
+ $info2 = null;
+ try {
+ $pmanager->getEmptySessionInternal( null, $expectId );
+ $this->fail( 'Expected exception not thrown' );
+ } catch ( \UnexpectedValueException $ex ) {
+ $this->assertSame(
+ 'MockProvider1 returned empty session info with id flagged unsafe',
+ $ex->getMessage()
+ );
+ }
+
+ // Wrong provider
+ $expectId = null;
+ $info1 = new SessionInfo( SessionInfo::MIN_PRIORITY, array(
+ 'provider' => $provider2,
+ 'id' => 'empty---------------------------',
+ 'persisted' => true,
+ 'idIsSafe' => true,
+ ) );
+ $info2 = null;
+ try {
+ $manager->getEmptySession();
+ $this->fail( 'Expected exception not thrown' );
+ } catch ( \UnexpectedValueException $ex ) {
+ $this->assertSame(
+ 'MockProvider1 returned an empty session info for a different provider: ' . $info1,
+ $ex->getMessage()
+ );
+ }
+
+ // Highest priority wins
+ $expectId = null;
+ $info1 = new SessionInfo( SessionInfo::MIN_PRIORITY + 1, array(
+ 'provider' => $provider1,
+ 'id' => 'empty1--------------------------',
+ 'persisted' => true,
+ 'idIsSafe' => true,
+ ) );
+ $info2 = new SessionInfo( SessionInfo::MIN_PRIORITY, array(
+ 'provider' => $provider2,
+ 'id' => 'empty2--------------------------',
+ 'persisted' => true,
+ 'idIsSafe' => true,
+ ) );
+ $session = $manager->getEmptySession();
+ $this->assertInstanceOf( 'MediaWiki\\Session\\Session', $session );
+ $this->assertSame( 'empty1--------------------------', $session->getId() );
+
+ $expectId = null;
+ $info1 = new SessionInfo( SessionInfo::MIN_PRIORITY + 1, array(
+ 'provider' => $provider1,
+ 'id' => 'empty1--------------------------',
+ 'persisted' => true,
+ 'idIsSafe' => true,
+ ) );
+ $info2 = new SessionInfo( SessionInfo::MIN_PRIORITY + 2, array(
+ 'provider' => $provider2,
+ 'id' => 'empty2--------------------------',
+ 'persisted' => true,
+ 'idIsSafe' => true,
+ ) );
+ $session = $manager->getEmptySession();
+ $this->assertInstanceOf( 'MediaWiki\\Session\\Session', $session );
+ $this->assertSame( 'empty2--------------------------', $session->getId() );
+
+ // Tied priorities throw an exception
+ $expectId = null;
+ $info1 = new SessionInfo( SessionInfo::MIN_PRIORITY, array(
+ 'provider' => $provider1,
+ 'id' => 'empty1--------------------------',
+ 'persisted' => true,
+ 'userInfo' => UserInfo::newAnonymous(),
+ 'idIsSafe' => true,
+ ) );
+ $info2 = new SessionInfo( SessionInfo::MIN_PRIORITY, array(
+ 'provider' => $provider2,
+ 'id' => 'empty2--------------------------',
+ 'persisted' => true,
+ 'userInfo' => UserInfo::newAnonymous(),
+ 'idIsSafe' => true,
+ ) );
+ try {
+ $manager->getEmptySession();
+ $this->fail( 'Expected exception not thrown' );
+ } catch ( \UnexpectedValueException $ex ) {
+ $this->assertStringStartsWith(
+ 'Multiple empty sessions tied for top priority: ',
+ $ex->getMessage()
+ );
+ }
+
+ // Bad id
+ try {
+ $pmanager->getEmptySessionInternal( null, 'bad' );
+ $this->fail( 'Expected exception not thrown' );
+ } catch ( \InvalidArgumentException $ex ) {
+ $this->assertSame( 'Invalid session ID', $ex->getMessage() );
+ }
+
+ // Session already exists
+ $expectId = 'expected-----------------------3';
+ $this->store->setSessionMeta( $expectId, array(
+ 'provider' => 'MockProvider2',
+ 'userId' => 0,
+ 'userName' => null,
+ 'userToken' => null,
+ ) );
+ try {
+ $pmanager->getEmptySessionInternal( null, $expectId );
+ $this->fail( 'Expected exception not thrown' );
+ } catch ( \InvalidArgumentException $ex ) {
+ $this->assertSame( 'Session ID already exists', $ex->getMessage() );
+ }
+ }
+
+ public function testGetVaryHeaders() {
+ $manager = $this->getManager();
+
+ $providerBuilder = $this->getMockBuilder( 'DummySessionProvider' )
+ ->setMethods( array( 'getVaryHeaders', '__toString' ) );
+
+ $provider1 = $providerBuilder->getMock();
+ $provider1->expects( $this->once() )->method( 'getVaryHeaders' )
+ ->will( $this->returnValue( array(
+ 'Foo' => null,
+ 'Bar' => array( 'X', 'Bar1' ),
+ 'Quux' => null,
+ ) ) );
+ $provider1->expects( $this->any() )->method( '__toString' )
+ ->will( $this->returnValue( 'MockProvider1' ) );
+
+ $provider2 = $providerBuilder->getMock();
+ $provider2->expects( $this->once() )->method( 'getVaryHeaders' )
+ ->will( $this->returnValue( array(
+ 'Baz' => null,
+ 'Bar' => array( 'X', 'Bar2' ),
+ 'Quux' => array( 'Quux' ),
+ ) ) );
+ $provider2->expects( $this->any() )->method( '__toString' )
+ ->will( $this->returnValue( 'MockProvider2' ) );
+
+ $this->config->set( 'SessionProviders', array(
+ $this->objectCacheDef( $provider1 ),
+ $this->objectCacheDef( $provider2 ),
+ ) );
+
+ $expect = array(
+ 'Foo' => array(),
+ 'Bar' => array( 'X', 'Bar1', 3 => 'Bar2' ),
+ 'Quux' => array( 'Quux' ),
+ 'Baz' => array(),
+ 'Quux' => array( 'Quux' ),
+ );
+
+ $this->assertEquals( $expect, $manager->getVaryHeaders() );
+
+ // Again, to ensure it's cached
+ $this->assertEquals( $expect, $manager->getVaryHeaders() );
+ }
+
+ public function testGetVaryCookies() {
+ $manager = $this->getManager();
+
+ $providerBuilder = $this->getMockBuilder( 'DummySessionProvider' )
+ ->setMethods( array( 'getVaryCookies', '__toString' ) );
+
+ $provider1 = $providerBuilder->getMock();
+ $provider1->expects( $this->once() )->method( 'getVaryCookies' )
+ ->will( $this->returnValue( array( 'Foo', 'Bar' ) ) );
+ $provider1->expects( $this->any() )->method( '__toString' )
+ ->will( $this->returnValue( 'MockProvider1' ) );
+
+ $provider2 = $providerBuilder->getMock();
+ $provider2->expects( $this->once() )->method( 'getVaryCookies' )
+ ->will( $this->returnValue( array( 'Foo', 'Baz' ) ) );
+ $provider2->expects( $this->any() )->method( '__toString' )
+ ->will( $this->returnValue( 'MockProvider2' ) );
+
+ $this->config->set( 'SessionProviders', array(
+ $this->objectCacheDef( $provider1 ),
+ $this->objectCacheDef( $provider2 ),
+ ) );
+
+ $expect = array( 'Foo', 'Bar', 'Baz' );
+
+ $this->assertEquals( $expect, $manager->getVaryCookies() );
+
+ // Again, to ensure it's cached
+ $this->assertEquals( $expect, $manager->getVaryCookies() );
+ }
+
+ public function testGetProviders() {
+ $realManager = $this->getManager();
+ $manager = \TestingAccessWrapper::newFromObject( $realManager );
+
+ $this->config->set( 'SessionProviders', array(
+ array( 'class' => 'DummySessionProvider' ),
+ ) );
+ $providers = $manager->getProviders();
+ $this->assertArrayHasKey( 'DummySessionProvider', $providers );
+ $provider = \TestingAccessWrapper::newFromObject( $providers['DummySessionProvider'] );
+ $this->assertSame( $manager->logger, $provider->logger );
+ $this->assertSame( $manager->config, $provider->config );
+ $this->assertSame( $realManager, $provider->getManager() );
+
+ $this->config->set( 'SessionProviders', array(
+ array( 'class' => 'DummySessionProvider' ),
+ array( 'class' => 'DummySessionProvider' ),
+ ) );
+ $manager->sessionProviders = null;
+ try {
+ $manager->getProviders();
+ $this->fail( 'Expected exception not thrown' );
+ } catch ( \UnexpectedValueException $ex ) {
+ $this->assertSame(
+ 'Duplicate provider name "DummySessionProvider"',
+ $ex->getMessage()
+ );
+ }
+ }
+
+ public function testShutdown() {
+ $manager = \TestingAccessWrapper::newFromObject( $this->getManager() );
+ $manager->setLogger( new \Psr\Log\NullLogger() );
+
+ $mock = $this->getMock( 'stdClass', array( 'save' ) );
+ $mock->expects( $this->once() )->method( 'save' );
+
+ $manager->allSessionBackends = array( $mock );
+ $manager->shutdown();
+ }
+
+ public function testGetSessionFromInfo() {
+ $manager = \TestingAccessWrapper::newFromObject( $this->getManager() );
+ $request = new \FauxRequest();
+
+ $id = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa';
+
+ $info = new SessionInfo( SessionInfo::MIN_PRIORITY, array(
+ 'provider' => $manager->getProvider( 'DummySessionProvider' ),
+ 'id' => $id,
+ 'persisted' => true,
+ 'userInfo' => UserInfo::newFromName( 'UTSysop', true ),
+ 'idIsSafe' => true,
+ ) );
+ \TestingAccessWrapper::newFromObject( $info )->idIsSafe = true;
+ $session1 = \TestingAccessWrapper::newFromObject(
+ $manager->getSessionFromInfo( $info, $request )
+ );
+ $session2 = \TestingAccessWrapper::newFromObject(
+ $manager->getSessionFromInfo( $info, $request )
+ );
+
+ $this->assertSame( $session1->backend, $session2->backend );
+ $this->assertNotEquals( $session1->index, $session2->index );
+ $this->assertSame( $session1->getSessionId(), $session2->getSessionId() );
+ $this->assertSame( $id, $session1->getId() );
+
+ \TestingAccessWrapper::newFromObject( $info )->idIsSafe = false;
+ $session3 = $manager->getSessionFromInfo( $info, $request );
+ $this->assertNotSame( $id, $session3->getId() );
+ }
+
+ public function testBackendRegistration() {
+ $manager = $this->getManager();
+
+ $session = $manager->getSessionForRequest( new \FauxRequest );
+ $backend = \TestingAccessWrapper::newFromObject( $session )->backend;
+ $sessionId = $session->getSessionId();
+ $id = (string)$sessionId;
+
+ $this->assertSame( $sessionId, $manager->getSessionById( $id )->getSessionId() );
+
+ $manager->changeBackendId( $backend );
+ $this->assertSame( $sessionId, $session->getSessionId() );
+ $this->assertNotEquals( $id, (string)$sessionId );
+ $id = (string)$sessionId;
+
+ $this->assertSame( $sessionId, $manager->getSessionById( $id )->getSessionId() );
+
+ // Destruction of the session here causes the backend to be deregistered
+ $session = null;
+
+ try {
+ $manager->changeBackendId( $backend );
+ $this->fail( 'Expected exception not thrown' );
+ } catch ( \InvalidArgumentException $ex ) {
+ $this->assertSame(
+ 'Backend was not registered with this SessionManager', $ex->getMessage()
+ );
+ }
+
+ try {
+ $manager->deregisterSessionBackend( $backend );
+ $this->fail( 'Expected exception not thrown' );
+ } catch ( \InvalidArgumentException $ex ) {
+ $this->assertSame(
+ 'Backend was not registered with this SessionManager', $ex->getMessage()
+ );
+ }
+
+ $session = $manager->getSessionById( $id );
+ $this->assertSame( $sessionId, $session->getSessionId() );
+ }
+
+ public function testGenerateSessionId() {
+ $manager = $this->getManager();
+
+ $id = $manager->generateSessionId();
+ $this->assertTrue( SessionManager::validateSessionId( $id ), "Generated ID: $id" );
+ }
+
+ public function testAutoCreateUser() {
+ global $wgGroupPermissions;
+
+ $that = $this;
+
+ \ObjectCache::$instances[__METHOD__] = new \HashBagOStuff();
+ $this->setMwGlobals( array( 'wgMainCacheType' => __METHOD__ ) );
+
+ $this->stashMwGlobals( array( 'wgGroupPermissions' ) );
+ $wgGroupPermissions['*']['createaccount'] = true;
+ $wgGroupPermissions['*']['autocreateaccount'] = false;
+
+ // Replace the global singleton with one configured for testing
+ $manager = $this->getManager();
+ $reset = TestUtils::setSessionManagerSingleton( $manager );
+
+ $logger = new \TestLogger( true, function ( $m ) {
+ if ( substr( $m, 0, 15 ) === 'SessionBackend ' ) {
+ // Don't care.
+ return null;
+ }
+ $m = str_replace( 'MediaWiki\Session\SessionManager::autoCreateUser: ', '', $m );
+ $m = preg_replace( '/ - from: .*$/', ' - from: XXX', $m );
+ return $m;
+ } );
+ $manager->setLogger( $logger );
+
+ $session = SessionManager::getGlobalSession();
+
+ // Can't create an already-existing user
+ $user = User::newFromName( 'UTSysop' );
+ $id = $user->getId();
+ $this->assertFalse( $manager->autoCreateUser( $user ) );
+ $this->assertSame( $id, $user->getId() );
+ $this->assertSame( 'UTSysop', $user->getName() );
+ $this->assertSame( array(), $logger->getBuffer() );
+ $logger->clearBuffer();
+
+ // Sanity check that creation works at all
+ $user = User::newFromName( 'UTSessionAutoCreate1' );
+ $this->assertSame( 0, $user->getId(), 'sanity check' );
+ $this->assertTrue( $manager->autoCreateUser( $user ) );
+ $this->assertNotEquals( 0, $user->getId() );
+ $this->assertSame( 'UTSessionAutoCreate1', $user->getName() );
+ $this->assertEquals(
+ $user->getId(), User::idFromName( 'UTSessionAutoCreate1', User::READ_LATEST )
+ );
+ $this->assertSame( array(
+ array( LogLevel::INFO, 'creating new user (UTSessionAutoCreate1) - from: XXX' ),
+ ), $logger->getBuffer() );
+ $logger->clearBuffer();
+
+ // Check lack of permissions
+ $wgGroupPermissions['*']['createaccount'] = false;
+ $wgGroupPermissions['*']['autocreateaccount'] = false;
+ $user = User::newFromName( 'UTDoesNotExist' );
+ $this->assertFalse( $manager->autoCreateUser( $user ) );
+ $this->assertSame( 0, $user->getId() );
+ $this->assertNotSame( 'UTDoesNotExist', $user->getName() );
+ $this->assertEquals( 0, User::idFromName( 'UTDoesNotExist', User::READ_LATEST ) );
+ $session->clear();
+ $this->assertSame( array(
+ array( LogLevel::DEBUG, 'user is blocked from this wiki, blacklisting' ),
+ ), $logger->getBuffer() );
+ $logger->clearBuffer();
+
+ // Check other permission
+ $wgGroupPermissions['*']['createaccount'] = false;
+ $wgGroupPermissions['*']['autocreateaccount'] = true;
+ $user = User::newFromName( 'UTSessionAutoCreate2' );
+ $this->assertSame( 0, $user->getId(), 'sanity check' );
+ $this->assertTrue( $manager->autoCreateUser( $user ) );
+ $this->assertNotEquals( 0, $user->getId() );
+ $this->assertSame( 'UTSessionAutoCreate2', $user->getName() );
+ $this->assertEquals(
+ $user->getId(), User::idFromName( 'UTSessionAutoCreate2', User::READ_LATEST )
+ );
+ $this->assertSame( array(
+ array( LogLevel::INFO, 'creating new user (UTSessionAutoCreate2) - from: XXX' ),
+ ), $logger->getBuffer() );
+ $logger->clearBuffer();
+
+ // Test account-creation block
+ $anon = new User;
+ $block = new \Block( array(
+ 'address' => $anon->getName(),
+ 'user' => $id,
+ 'reason' => __METHOD__,
+ 'expiry' => time() + 100500,
+ 'createAccount' => true,
+ ) );
+ $block->insert();
+ $this->assertInstanceOf( 'Block', $anon->isBlockedFromCreateAccount(), 'sanity check' );
+ $reset2 = new \ScopedCallback( array( $block, 'delete' ) );
+ $user = User::newFromName( 'UTDoesNotExist' );
+ $this->assertFalse( $manager->autoCreateUser( $user ) );
+ $this->assertSame( 0, $user->getId() );
+ $this->assertNotSame( 'UTDoesNotExist', $user->getName() );
+ $this->assertEquals( 0, User::idFromName( 'UTDoesNotExist', User::READ_LATEST ) );
+ \ScopedCallback::consume( $reset2 );
+ $session->clear();
+ $this->assertSame( array(
+ array( LogLevel::DEBUG, 'user is blocked from this wiki, blacklisting' ),
+ ), $logger->getBuffer() );
+ $logger->clearBuffer();
+
+ // Sanity check that creation still works
+ $user = User::newFromName( 'UTSessionAutoCreate3' );
+ $this->assertSame( 0, $user->getId(), 'sanity check' );
+ $this->assertTrue( $manager->autoCreateUser( $user ) );
+ $this->assertNotEquals( 0, $user->getId() );
+ $this->assertSame( 'UTSessionAutoCreate3', $user->getName() );
+ $this->assertEquals(
+ $user->getId(), User::idFromName( 'UTSessionAutoCreate3', User::READ_LATEST )
+ );
+ $this->assertSame( array(
+ array( LogLevel::INFO, 'creating new user (UTSessionAutoCreate3) - from: XXX' ),
+ ), $logger->getBuffer() );
+ $logger->clearBuffer();
+
+ // Test prevention by AuthPlugin
+ global $wgAuth;
+ $oldWgAuth = $wgAuth;
+ $mockWgAuth = $this->getMock( 'AuthPlugin', array( 'autoCreate' ) );
+ $mockWgAuth->expects( $this->once() )->method( 'autoCreate' )
+ ->will( $this->returnValue( false ) );
+ $this->setMwGlobals( array(
+ 'wgAuth' => $mockWgAuth,
+ ) );
+ $user = User::newFromName( 'UTDoesNotExist' );
+ $this->assertFalse( $manager->autoCreateUser( $user ) );
+ $this->assertSame( 0, $user->getId() );
+ $this->assertNotSame( 'UTDoesNotExist', $user->getName() );
+ $this->assertEquals( 0, User::idFromName( 'UTDoesNotExist', User::READ_LATEST ) );
+ $this->setMwGlobals( array(
+ 'wgAuth' => $oldWgAuth,
+ ) );
+ $session->clear();
+ $this->assertSame( array(
+ array( LogLevel::DEBUG, 'denied by AuthPlugin' ),
+ ), $logger->getBuffer() );
+ $logger->clearBuffer();
+
+ // Test prevention by wfReadOnly()
+ $this->setMwGlobals( array(
+ 'wgReadOnly' => 'Because',
+ ) );
+ $user = User::newFromName( 'UTDoesNotExist' );
+ $this->assertFalse( $manager->autoCreateUser( $user ) );
+ $this->assertSame( 0, $user->getId() );
+ $this->assertNotSame( 'UTDoesNotExist', $user->getName() );
+ $this->assertEquals( 0, User::idFromName( 'UTDoesNotExist', User::READ_LATEST ) );
+ $this->setMwGlobals( array(
+ 'wgReadOnly' => false,
+ ) );
+ $session->clear();
+ $this->assertSame( array(
+ array( LogLevel::DEBUG, 'denied by wfReadOnly()' ),
+ ), $logger->getBuffer() );
+ $logger->clearBuffer();
+
+ // Test prevention by a previous session
+ $session->set( 'MWSession::AutoCreateBlacklist', 'test' );
+ $user = User::newFromName( 'UTDoesNotExist' );
+ $this->assertFalse( $manager->autoCreateUser( $user ) );
+ $this->assertSame( 0, $user->getId() );
+ $this->assertNotSame( 'UTDoesNotExist', $user->getName() );
+ $this->assertEquals( 0, User::idFromName( 'UTDoesNotExist', User::READ_LATEST ) );
+ $session->clear();
+ $this->assertSame( array(
+ array( LogLevel::DEBUG, 'blacklisted in session (test)' ),
+ ), $logger->getBuffer() );
+ $logger->clearBuffer();
+
+ // Test uncreatable name
+ $user = User::newFromName( 'UTDoesNotExist@' );
+ $this->assertFalse( $manager->autoCreateUser( $user ) );
+ $this->assertSame( 0, $user->getId() );
+ $this->assertNotSame( 'UTDoesNotExist@', $user->getName() );
+ $this->assertEquals( 0, User::idFromName( 'UTDoesNotExist', User::READ_LATEST ) );
+ $session->clear();
+ $this->assertSame( array(
+ array( LogLevel::DEBUG, 'Invalid username, blacklisting' ),
+ ), $logger->getBuffer() );
+ $logger->clearBuffer();
+
+ // Test AbortAutoAccount hook
+ $mock = $this->getMock( __CLASS__, array( 'onAbortAutoAccount' ) );
+ $mock->expects( $this->once() )->method( 'onAbortAutoAccount' )
+ ->will( $this->returnCallback( function ( User $user, &$msg ) {
+ $msg = 'No way!';
+ return false;
+ } ) );
+ $this->mergeMwGlobalArrayValue( 'wgHooks', array( 'AbortAutoAccount' => array( $mock ) ) );
+ $user = User::newFromName( 'UTDoesNotExist' );
+ $this->assertFalse( $manager->autoCreateUser( $user ) );
+ $this->assertSame( 0, $user->getId() );
+ $this->assertNotSame( 'UTDoesNotExist', $user->getName() );
+ $this->assertEquals( 0, User::idFromName( 'UTDoesNotExist', User::READ_LATEST ) );
+ $this->mergeMwGlobalArrayValue( 'wgHooks', array( 'AbortAutoAccount' => array() ) );
+ $session->clear();
+ $this->assertSame( array(
+ array( LogLevel::DEBUG, 'denied by hook: No way!' ),
+ ), $logger->getBuffer() );
+ $logger->clearBuffer();
+
+ // Test AbortAutoAccount hook screwing up the name
+ $mock = $this->getMock( 'stdClass', array( 'onAbortAutoAccount' ) );
+ $mock->expects( $this->once() )->method( 'onAbortAutoAccount' )
+ ->will( $this->returnCallback( function ( User $user ) {
+ $user->setName( 'UTDoesNotExistEither' );
+ } ) );
+ $this->mergeMwGlobalArrayValue( 'wgHooks', array( 'AbortAutoAccount' => array( $mock ) ) );
+ try {
+ $user = User::newFromName( 'UTDoesNotExist' );
+ $manager->autoCreateUser( $user );
+ $this->fail( 'Expected exception not thrown' );
+ } catch ( \UnexpectedValueException $ex ) {
+ $this->assertSame(
+ 'AbortAutoAccount hook tried to change the user name',
+ $ex->getMessage()
+ );
+ }
+ $this->assertSame( 0, $user->getId() );
+ $this->assertNotSame( 'UTDoesNotExist', $user->getName() );
+ $this->assertNotSame( 'UTDoesNotExistEither', $user->getName() );
+ $this->assertEquals( 0, User::idFromName( 'UTDoesNotExist', User::READ_LATEST ) );
+ $this->assertEquals( 0, User::idFromName( 'UTDoesNotExistEither', User::READ_LATEST ) );
+ $this->mergeMwGlobalArrayValue( 'wgHooks', array( 'AbortAutoAccount' => array() ) );
+ $session->clear();
+ $this->assertSame( array(), $logger->getBuffer() );
+ $logger->clearBuffer();
+
+ // Test for "exception backoff"
+ $user = User::newFromName( 'UTDoesNotExist' );
+ $cache = \ObjectCache::getLocalClusterInstance();
+ $backoffKey = wfMemcKey( 'MWSession', 'autocreate-failed', md5( $user->getName() ) );
+ $cache->set( $backoffKey, 1, 60 * 10 );
+ $this->assertFalse( $manager->autoCreateUser( $user ) );
+ $this->assertSame( 0, $user->getId() );
+ $this->assertNotSame( 'UTDoesNotExist', $user->getName() );
+ $this->assertEquals( 0, User::idFromName( 'UTDoesNotExist', User::READ_LATEST ) );
+ $cache->delete( $backoffKey );
+ $session->clear();
+ $this->assertSame( array(
+ array( LogLevel::DEBUG, 'denied by prior creation attempt failures' ),
+ ), $logger->getBuffer() );
+ $logger->clearBuffer();
+
+ // Sanity check that creation still works, and test completion hook
+ $cb = $this->callback( function ( User $user ) use ( $that ) {
+ $that->assertNotEquals( 0, $user->getId() );
+ $that->assertSame( 'UTSessionAutoCreate4', $user->getName() );
+ $that->assertEquals(
+ $user->getId(), User::idFromName( 'UTSessionAutoCreate4', User::READ_LATEST )
+ );
+ return true;
+ } );
+ $mock = $this->getMock( 'stdClass',
+ array( 'onAuthPluginAutoCreate', 'onLocalUserCreated' ) );
+ $mock->expects( $this->once() )->method( 'onAuthPluginAutoCreate' )
+ ->with( $cb );
+ $mock->expects( $this->once() )->method( 'onLocalUserCreated' )
+ ->with( $cb, $this->identicalTo( true ) );
+ $this->mergeMwGlobalArrayValue( 'wgHooks', array(
+ 'AuthPluginAutoCreate' => array( $mock ),
+ 'LocalUserCreated' => array( $mock ),
+ ) );
+ $user = User::newFromName( 'UTSessionAutoCreate4' );
+ $this->assertSame( 0, $user->getId(), 'sanity check' );
+ $this->assertTrue( $manager->autoCreateUser( $user ) );
+ $this->assertNotEquals( 0, $user->getId() );
+ $this->assertSame( 'UTSessionAutoCreate4', $user->getName() );
+ $this->assertEquals(
+ $user->getId(),
+ User::idFromName( 'UTSessionAutoCreate4', User::READ_LATEST )
+ );
+ $this->mergeMwGlobalArrayValue( 'wgHooks', array(
+ 'AuthPluginAutoCreate' => array(),
+ 'LocalUserCreated' => array(),
+ ) );
+ $this->assertSame( array(
+ array( LogLevel::INFO, 'creating new user (UTSessionAutoCreate4) - from: XXX' ),
+ ), $logger->getBuffer() );
+ $logger->clearBuffer();
+ }
+
+ public function onAbortAutoAccount( User $user, &$msg ) {
+ }
+
+ public function testPreventSessionsForUser() {
+ $manager = $this->getManager();
+
+ $providerBuilder = $this->getMockBuilder( 'DummySessionProvider' )
+ ->setMethods( array( 'preventSessionsForUser', '__toString' ) );
+
+ $provider1 = $providerBuilder->getMock();
+ $provider1->expects( $this->once() )->method( 'preventSessionsForUser' )
+ ->with( $this->equalTo( 'UTSysop' ) );
+ $provider1->expects( $this->any() )->method( '__toString' )
+ ->will( $this->returnValue( 'MockProvider1' ) );
+
+ $this->config->set( 'SessionProviders', array(
+ $this->objectCacheDef( $provider1 ),
+ ) );
+
+ $user = User::newFromName( 'UTSysop' );
+ $token = $user->getToken( true );
+
+ $this->assertFalse( $manager->isUserSessionPrevented( 'UTSysop' ) );
+ $manager->preventSessionsForUser( 'UTSysop' );
+ $this->assertNotEquals( $token, User::newFromName( 'UTSysop' )->getToken() );
+ $this->assertTrue( $manager->isUserSessionPrevented( 'UTSysop' ) );
+ }
+
+ public function testLoadSessionInfoFromStore() {
+ $manager = $this->getManager();
+ $logger = new \TestLogger( true, function ( $m ) {
+ return preg_replace(
+ '/^Session \[\d+\]\w+<(?:null|anon|[+-]:\d+:\w+)>\w+: /', 'Session X: ', $m
+ );
+ } );
+ $manager->setLogger( $logger );
+ $request = new \FauxRequest();
+
+ // TestingAccessWrapper can't handle methods with reference arguments, sigh.
+ $rClass = new \ReflectionClass( $manager );
+ $rMethod = $rClass->getMethod( 'loadSessionInfoFromStore' );
+ $rMethod->setAccessible( true );
+ $loadSessionInfoFromStore = function ( &$info ) use ( $rMethod, $manager, $request ) {
+ return $rMethod->invokeArgs( $manager, array( &$info, $request ) );
+ };
+
+ $userInfo = UserInfo::newFromName( 'UTSysop', true );
+ $unverifiedUserInfo = UserInfo::newFromName( 'UTSysop', false );
+
+ $id = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa';
+ $metadata = array(
+ 'userId' => $userInfo->getId(),
+ 'userName' => $userInfo->getName(),
+ 'userToken' => $userInfo->getToken( true ),
+ 'provider' => 'Mock',
+ );
+
+ $builder = $this->getMockBuilder( 'MediaWiki\\Session\\SessionProvider' )
+ ->setMethods( array( '__toString', 'mergeMetadata', 'refreshSessionInfo' ) );
+
+ $provider = $builder->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( 'refreshSessionInfo' )
+ ->will( $this->returnValue( true ) );
+ $provider->expects( $this->any() )->method( '__toString' )
+ ->will( $this->returnValue( 'Mock' ) );
+ $provider->expects( $this->any() )->method( 'mergeMetadata' )
+ ->will( $this->returnCallback( function ( $a, $b ) {
+ if ( $b === array( 'Throw' ) ) {
+ throw new \UnexpectedValueException( 'no merge!' );
+ }
+ return array( 'Merged' );
+ } ) );
+
+ $provider2 = $builder->getMockForAbstractClass();
+ $provider2->setManager( $manager );
+ $provider2->expects( $this->any() )->method( 'persistsSessionId' )
+ ->will( $this->returnValue( false ) );
+ $provider2->expects( $this->any() )->method( 'canChangeUser' )
+ ->will( $this->returnValue( false ) );
+ $provider2->expects( $this->any() )->method( '__toString' )
+ ->will( $this->returnValue( 'Mock2' ) );
+ $provider2->expects( $this->any() )->method( 'refreshSessionInfo' )
+ ->will( $this->returnCallback( function ( $info, $request, &$metadata ) {
+ $metadata['changed'] = true;
+ return true;
+ } ) );
+
+ $provider3 = $builder->getMockForAbstractClass();
+ $provider3->setManager( $manager );
+ $provider3->expects( $this->any() )->method( 'persistsSessionId' )
+ ->will( $this->returnValue( true ) );
+ $provider3->expects( $this->any() )->method( 'canChangeUser' )
+ ->will( $this->returnValue( true ) );
+ $provider3->expects( $this->once() )->method( 'refreshSessionInfo' )
+ ->will( $this->returnValue( false ) );
+ $provider3->expects( $this->any() )->method( '__toString' )
+ ->will( $this->returnValue( 'Mock3' ) );
+
+ \TestingAccessWrapper::newFromObject( $manager )->sessionProviders = array(
+ (string)$provider => $provider,
+ (string)$provider2 => $provider2,
+ (string)$provider3 => $provider3,
+ );
+
+ // No metadata, basic usage
+ $info = new SessionInfo( SessionInfo::MIN_PRIORITY, array(
+ 'provider' => $provider,
+ 'id' => $id,
+ 'userInfo' => $userInfo
+ ) );
+ $this->assertFalse( $info->isIdSafe(), 'sanity check' );
+ $this->assertTrue( $loadSessionInfoFromStore( $info ) );
+ $this->assertFalse( $info->isIdSafe() );
+ $this->assertSame( array(), $logger->getBuffer() );
+
+ $info = new SessionInfo( SessionInfo::MIN_PRIORITY, array(
+ 'provider' => $provider,
+ 'userInfo' => $userInfo
+ ) );
+ $this->assertTrue( $info->isIdSafe(), 'sanity check' );
+ $this->assertTrue( $loadSessionInfoFromStore( $info ) );
+ $this->assertTrue( $info->isIdSafe() );
+ $this->assertSame( array(), $logger->getBuffer() );
+
+ $info = new SessionInfo( SessionInfo::MIN_PRIORITY, array(
+ 'provider' => $provider2,
+ 'id' => $id,
+ 'userInfo' => $userInfo
+ ) );
+ $this->assertFalse( $info->isIdSafe(), 'sanity check' );
+ $this->assertTrue( $loadSessionInfoFromStore( $info ) );
+ $this->assertTrue( $info->isIdSafe() );
+ $this->assertSame( array(), $logger->getBuffer() );
+
+ // Unverified user, no metadata
+ $info = new SessionInfo( SessionInfo::MIN_PRIORITY, array(
+ 'provider' => $provider,
+ 'id' => $id,
+ 'userInfo' => $unverifiedUserInfo
+ ) );
+ $this->assertSame( $unverifiedUserInfo, $info->getUserInfo() );
+ $this->assertFalse( $loadSessionInfoFromStore( $info ) );
+ $this->assertSame( array(
+ array( LogLevel::WARNING, 'Session X: Unverified user provided and no metadata to auth it' )
+ ), $logger->getBuffer() );
+ $logger->clearBuffer();
+
+ // No metadata, missing data
+ $info = new SessionInfo( SessionInfo::MIN_PRIORITY, array(
+ 'id' => $id,
+ 'userInfo' => $userInfo
+ ) );
+ $this->assertFalse( $loadSessionInfoFromStore( $info ) );
+ $this->assertSame( array(
+ array( LogLevel::WARNING, 'Session X: Null provider and no metadata' ),
+ ), $logger->getBuffer() );
+ $logger->clearBuffer();
+
+ $info = new SessionInfo( SessionInfo::MIN_PRIORITY, array(
+ 'provider' => $provider,
+ 'id' => $id,
+ ) );
+ $this->assertFalse( $info->isIdSafe(), 'sanity check' );
+ $this->assertTrue( $loadSessionInfoFromStore( $info ) );
+ $this->assertInstanceOf( 'MediaWiki\\Session\\UserInfo', $info->getUserInfo() );
+ $this->assertTrue( $info->getUserInfo()->isVerified() );
+ $this->assertTrue( $info->getUserInfo()->isAnon() );
+ $this->assertFalse( $info->isIdSafe() );
+ $this->assertSame( array(), $logger->getBuffer() );
+
+ $info = new SessionInfo( SessionInfo::MIN_PRIORITY, array(
+ 'provider' => $provider2,
+ 'id' => $id,
+ ) );
+ $this->assertFalse( $info->isIdSafe(), 'sanity check' );
+ $this->assertFalse( $loadSessionInfoFromStore( $info ) );
+ $this->assertSame( array(
+ array( LogLevel::INFO, 'Session X: No user provided and provider cannot set user' )
+ ), $logger->getBuffer() );
+ $logger->clearBuffer();
+
+ // Incomplete/bad metadata
+ $this->store->setRawSession( $id, true );
+ $this->assertFalse( $loadSessionInfoFromStore( $info ) );
+ $this->assertSame( array(
+ array( LogLevel::WARNING, 'Session X: Bad data' ),
+ ), $logger->getBuffer() );
+ $logger->clearBuffer();
+
+ $this->store->setRawSession( $id, array( 'data' => array() ) );
+ $this->assertFalse( $loadSessionInfoFromStore( $info ) );
+ $this->assertSame( array(
+ array( LogLevel::WARNING, 'Session X: Bad data structure' ),
+ ), $logger->getBuffer() );
+ $logger->clearBuffer();
+
+ $this->store->deleteSession( $id );
+ $this->store->setRawSession( $id, array( 'metadata' => $metadata ) );
+ $this->assertFalse( $loadSessionInfoFromStore( $info ) );
+ $this->assertSame( array(
+ array( LogLevel::WARNING, 'Session X: Bad data structure' ),
+ ), $logger->getBuffer() );
+ $logger->clearBuffer();
+
+ $this->store->setRawSession( $id, array( 'metadata' => $metadata, 'data' => true ) );
+ $this->assertFalse( $loadSessionInfoFromStore( $info ) );
+ $this->assertSame( array(
+ array( LogLevel::WARNING, 'Session X: Bad data structure' ),
+ ), $logger->getBuffer() );
+ $logger->clearBuffer();
+
+ $this->store->setRawSession( $id, array( 'metadata' => true, 'data' => array() ) );
+ $this->assertFalse( $loadSessionInfoFromStore( $info ) );
+ $this->assertSame( array(
+ array( LogLevel::WARNING, 'Session X: Bad data structure' ),
+ ), $logger->getBuffer() );
+ $logger->clearBuffer();
+
+ foreach ( $metadata as $key => $dummy ) {
+ $tmp = $metadata;
+ unset( $tmp[$key] );
+ $this->store->setRawSession( $id, array( 'metadata' => $tmp, 'data' => array() ) );
+ $this->assertFalse( $loadSessionInfoFromStore( $info ) );
+ $this->assertSame( array(
+ array( LogLevel::WARNING, 'Session X: Bad metadata' ),
+ ), $logger->getBuffer() );
+ $logger->clearBuffer();
+ }
+
+ // Basic usage with metadata
+ $this->store->setRawSession( $id, array( 'metadata' => $metadata, 'data' => array() ) );
+ $info = new SessionInfo( SessionInfo::MIN_PRIORITY, array(
+ 'provider' => $provider,
+ 'id' => $id,
+ 'userInfo' => $userInfo
+ ) );
+ $this->assertFalse( $info->isIdSafe(), 'sanity check' );
+ $this->assertTrue( $loadSessionInfoFromStore( $info ) );
+ $this->assertTrue( $info->isIdSafe() );
+ $this->assertSame( array(), $logger->getBuffer() );
+
+ // Mismatched provider
+ $this->store->setSessionMeta( $id, array( 'provider' => 'Bad' ) + $metadata );
+ $info = new SessionInfo( SessionInfo::MIN_PRIORITY, array(
+ 'provider' => $provider,
+ 'id' => $id,
+ 'userInfo' => $userInfo
+ ) );
+ $this->assertFalse( $loadSessionInfoFromStore( $info ) );
+ $this->assertSame( array(
+ array( LogLevel::WARNING, 'Session X: Wrong provider, Bad !== Mock' ),
+ ), $logger->getBuffer() );
+ $logger->clearBuffer();
+
+ // Unknown provider
+ $this->store->setSessionMeta( $id, array( 'provider' => 'Bad' ) + $metadata );
+ $info = new SessionInfo( SessionInfo::MIN_PRIORITY, array(
+ 'id' => $id,
+ 'userInfo' => $userInfo
+ ) );
+ $this->assertFalse( $loadSessionInfoFromStore( $info ) );
+ $this->assertSame( array(
+ array( LogLevel::WARNING, 'Session X: Unknown provider, Bad' ),
+ ), $logger->getBuffer() );
+ $logger->clearBuffer();
+
+ // Fill in provider
+ $this->store->setSessionMeta( $id, $metadata );
+ $info = new SessionInfo( SessionInfo::MIN_PRIORITY, array(
+ 'id' => $id,
+ 'userInfo' => $userInfo
+ ) );
+ $this->assertFalse( $info->isIdSafe(), 'sanity check' );
+ $this->assertTrue( $loadSessionInfoFromStore( $info ) );
+ $this->assertTrue( $info->isIdSafe() );
+ $this->assertSame( array(), $logger->getBuffer() );
+
+ // Bad user metadata
+ $this->store->setSessionMeta( $id, array( 'userId' => -1, 'userToken' => null ) + $metadata );
+ $info = new SessionInfo( SessionInfo::MIN_PRIORITY, array(
+ 'provider' => $provider,
+ 'id' => $id,
+ ) );
+ $this->assertFalse( $loadSessionInfoFromStore( $info ) );
+ $this->assertSame( array(
+ array( LogLevel::ERROR, 'Session X: Invalid ID' ),
+ ), $logger->getBuffer() );
+ $logger->clearBuffer();
+
+ $this->store->setSessionMeta(
+ $id, array( 'userId' => 0, 'userName' => '<X>', 'userToken' => null ) + $metadata
+ );
+ $info = new SessionInfo( SessionInfo::MIN_PRIORITY, array(
+ 'provider' => $provider,
+ 'id' => $id,
+ ) );
+ $this->assertFalse( $loadSessionInfoFromStore( $info ) );
+ $this->assertSame( array(
+ array( LogLevel::ERROR, 'Session X: Invalid user name' ),
+ ), $logger->getBuffer() );
+ $logger->clearBuffer();
+
+ // Mismatched user by ID
+ $this->store->setSessionMeta(
+ $id, array( 'userId' => $userInfo->getId() + 1, 'userToken' => null ) + $metadata
+ );
+ $info = new SessionInfo( SessionInfo::MIN_PRIORITY, array(
+ 'provider' => $provider,
+ 'id' => $id,
+ 'userInfo' => $userInfo
+ ) );
+ $this->assertFalse( $loadSessionInfoFromStore( $info ) );
+ $this->assertSame( array(
+ array( LogLevel::WARNING, 'Session X: User ID mismatch, 2 !== 1' ),
+ ), $logger->getBuffer() );
+ $logger->clearBuffer();
+
+ // Mismatched user by name
+ $this->store->setSessionMeta(
+ $id, array( 'userId' => 0, 'userName' => 'X', 'userToken' => null ) + $metadata
+ );
+ $info = new SessionInfo( SessionInfo::MIN_PRIORITY, array(
+ 'provider' => $provider,
+ 'id' => $id,
+ 'userInfo' => $userInfo
+ ) );
+ $this->assertFalse( $loadSessionInfoFromStore( $info ) );
+ $this->assertSame( array(
+ array( LogLevel::WARNING, 'Session X: User name mismatch, X !== UTSysop' ),
+ ), $logger->getBuffer() );
+ $logger->clearBuffer();
+
+ // ID matches, name doesn't
+ $this->store->setSessionMeta(
+ $id, array( 'userId' => $userInfo->getId(), 'userName' => 'X', 'userToken' => null ) + $metadata
+ );
+ $info = new SessionInfo( SessionInfo::MIN_PRIORITY, array(
+ 'provider' => $provider,
+ 'id' => $id,
+ 'userInfo' => $userInfo
+ ) );
+ $this->assertFalse( $loadSessionInfoFromStore( $info ) );
+ $this->assertSame( array(
+ array(
+ LogLevel::WARNING, 'Session X: User ID matched but name didn\'t (rename?), X !== UTSysop'
+ ),
+ ), $logger->getBuffer() );
+ $logger->clearBuffer();
+
+ // Mismatched anon user
+ $this->store->setSessionMeta(
+ $id, array( 'userId' => 0, 'userName' => null, 'userToken' => null ) + $metadata
+ );
+ $info = new SessionInfo( SessionInfo::MIN_PRIORITY, array(
+ 'provider' => $provider,
+ 'id' => $id,
+ 'userInfo' => $userInfo
+ ) );
+ $this->assertFalse( $loadSessionInfoFromStore( $info ) );
+ $this->assertSame( array(
+ array(
+ LogLevel::WARNING, 'Session X: Metadata has an anonymous user, but a non-anon user was provided'
+ ),
+ ), $logger->getBuffer() );
+ $logger->clearBuffer();
+
+ // Lookup user by ID
+ $this->store->setSessionMeta( $id, array( 'userToken' => null ) + $metadata );
+ $info = new SessionInfo( SessionInfo::MIN_PRIORITY, array(
+ 'provider' => $provider,
+ 'id' => $id,
+ ) );
+ $this->assertFalse( $info->isIdSafe(), 'sanity check' );
+ $this->assertTrue( $loadSessionInfoFromStore( $info ) );
+ $this->assertSame( $userInfo->getId(), $info->getUserInfo()->getId() );
+ $this->assertTrue( $info->isIdSafe() );
+ $this->assertSame( array(), $logger->getBuffer() );
+
+ // Lookup user by name
+ $this->store->setSessionMeta(
+ $id, array( 'userId' => 0, 'userName' => 'UTSysop', 'userToken' => null ) + $metadata
+ );
+ $info = new SessionInfo( SessionInfo::MIN_PRIORITY, array(
+ 'provider' => $provider,
+ 'id' => $id,
+ ) );
+ $this->assertFalse( $info->isIdSafe(), 'sanity check' );
+ $this->assertTrue( $loadSessionInfoFromStore( $info ) );
+ $this->assertSame( $userInfo->getId(), $info->getUserInfo()->getId() );
+ $this->assertTrue( $info->isIdSafe() );
+ $this->assertSame( array(), $logger->getBuffer() );
+
+ // Lookup anonymous user
+ $this->store->setSessionMeta(
+ $id, array( 'userId' => 0, 'userName' => null, 'userToken' => null ) + $metadata
+ );
+ $info = new SessionInfo( SessionInfo::MIN_PRIORITY, array(
+ 'provider' => $provider,
+ 'id' => $id,
+ ) );
+ $this->assertFalse( $info->isIdSafe(), 'sanity check' );
+ $this->assertTrue( $loadSessionInfoFromStore( $info ) );
+ $this->assertTrue( $info->getUserInfo()->isAnon() );
+ $this->assertTrue( $info->isIdSafe() );
+ $this->assertSame( array(), $logger->getBuffer() );
+
+ // Unverified user with metadata
+ $this->store->setSessionMeta( $id, $metadata );
+ $info = new SessionInfo( SessionInfo::MIN_PRIORITY, array(
+ 'provider' => $provider,
+ 'id' => $id,
+ 'userInfo' => $unverifiedUserInfo
+ ) );
+ $this->assertFalse( $info->isIdSafe(), 'sanity check' );
+ $this->assertTrue( $loadSessionInfoFromStore( $info ) );
+ $this->assertTrue( $info->getUserInfo()->isVerified() );
+ $this->assertSame( $unverifiedUserInfo->getId(), $info->getUserInfo()->getId() );
+ $this->assertSame( $unverifiedUserInfo->getName(), $info->getUserInfo()->getName() );
+ $this->assertTrue( $info->isIdSafe() );
+ $this->assertSame( array(), $logger->getBuffer() );
+
+ // Unverified user with metadata
+ $this->store->setSessionMeta( $id, $metadata );
+ $info = new SessionInfo( SessionInfo::MIN_PRIORITY, array(
+ 'provider' => $provider,
+ 'id' => $id,
+ 'userInfo' => $unverifiedUserInfo
+ ) );
+ $this->assertFalse( $info->isIdSafe(), 'sanity check' );
+ $this->assertTrue( $loadSessionInfoFromStore( $info ) );
+ $this->assertTrue( $info->getUserInfo()->isVerified() );
+ $this->assertSame( $unverifiedUserInfo->getId(), $info->getUserInfo()->getId() );
+ $this->assertSame( $unverifiedUserInfo->getName(), $info->getUserInfo()->getName() );
+ $this->assertTrue( $info->isIdSafe() );
+ $this->assertSame( array(), $logger->getBuffer() );
+
+ // Wrong token
+ $this->store->setSessionMeta( $id, array( 'userToken' => 'Bad' ) + $metadata );
+ $info = new SessionInfo( SessionInfo::MIN_PRIORITY, array(
+ 'provider' => $provider,
+ 'id' => $id,
+ 'userInfo' => $userInfo
+ ) );
+ $this->assertFalse( $loadSessionInfoFromStore( $info ) );
+ $this->assertSame( array(
+ array( LogLevel::WARNING, 'Session X: User token mismatch' ),
+ ), $logger->getBuffer() );
+ $logger->clearBuffer();
+
+ // Provider metadata
+ $this->store->setSessionMeta( $id, array( 'provider' => 'Mock2' ) + $metadata );
+ $info = new SessionInfo( SessionInfo::MIN_PRIORITY, array(
+ 'provider' => $provider2,
+ 'id' => $id,
+ 'userInfo' => $userInfo,
+ 'metadata' => array( 'Info' ),
+ ) );
+ $this->assertTrue( $loadSessionInfoFromStore( $info ) );
+ $this->assertSame( array( 'Info', 'changed' => true ), $info->getProviderMetadata() );
+ $this->assertSame( array(), $logger->getBuffer() );
+
+ $this->store->setSessionMeta( $id, array( 'providerMetadata' => array( 'Saved' ) ) + $metadata );
+ $info = new SessionInfo( SessionInfo::MIN_PRIORITY, array(
+ 'provider' => $provider,
+ 'id' => $id,
+ 'userInfo' => $userInfo,
+ ) );
+ $this->assertTrue( $loadSessionInfoFromStore( $info ) );
+ $this->assertSame( array( 'Saved' ), $info->getProviderMetadata() );
+ $this->assertSame( array(), $logger->getBuffer() );
+
+ $info = new SessionInfo( SessionInfo::MIN_PRIORITY, array(
+ 'provider' => $provider,
+ 'id' => $id,
+ 'userInfo' => $userInfo,
+ 'metadata' => array( 'Info' ),
+ ) );
+ $this->assertTrue( $loadSessionInfoFromStore( $info ) );
+ $this->assertSame( array( 'Merged' ), $info->getProviderMetadata() );
+ $this->assertSame( array(), $logger->getBuffer() );
+
+ $info = new SessionInfo( SessionInfo::MIN_PRIORITY, array(
+ 'provider' => $provider,
+ 'id' => $id,
+ 'userInfo' => $userInfo,
+ 'metadata' => array( 'Throw' ),
+ ) );
+ $this->assertFalse( $loadSessionInfoFromStore( $info ) );
+ $this->assertSame( array(
+ array( LogLevel::WARNING, 'Session X: Metadata merge failed: no merge!' ),
+ ), $logger->getBuffer() );
+ $logger->clearBuffer();
+
+ // Remember from session
+ $this->store->setSessionMeta( $id, $metadata );
+ $info = new SessionInfo( SessionInfo::MIN_PRIORITY, array(
+ 'provider' => $provider,
+ 'id' => $id,
+ ) );
+ $this->assertTrue( $loadSessionInfoFromStore( $info ) );
+ $this->assertFalse( $info->wasRemembered() );
+ $this->assertSame( array(), $logger->getBuffer() );
+
+ $this->store->setSessionMeta( $id, array( 'remember' => true ) + $metadata );
+ $info = new SessionInfo( SessionInfo::MIN_PRIORITY, array(
+ 'provider' => $provider,
+ 'id' => $id,
+ ) );
+ $this->assertTrue( $loadSessionInfoFromStore( $info ) );
+ $this->assertTrue( $info->wasRemembered() );
+ $this->assertSame( array(), $logger->getBuffer() );
+
+ $this->store->setSessionMeta( $id, array( 'remember' => false ) + $metadata );
+ $info = new SessionInfo( SessionInfo::MIN_PRIORITY, array(
+ 'provider' => $provider,
+ 'id' => $id,
+ 'userInfo' => $userInfo
+ ) );
+ $this->assertTrue( $loadSessionInfoFromStore( $info ) );
+ $this->assertTrue( $info->wasRemembered() );
+ $this->assertSame( array(), $logger->getBuffer() );
+
+ // forceHTTPS from session
+ $this->store->setSessionMeta( $id, $metadata );
+ $info = new SessionInfo( SessionInfo::MIN_PRIORITY, array(
+ 'provider' => $provider,
+ 'id' => $id,
+ 'userInfo' => $userInfo
+ ) );
+ $this->assertTrue( $loadSessionInfoFromStore( $info ) );
+ $this->assertFalse( $info->forceHTTPS() );
+ $this->assertSame( array(), $logger->getBuffer() );
+
+ $this->store->setSessionMeta( $id, array( 'forceHTTPS' => true ) + $metadata );
+ $info = new SessionInfo( SessionInfo::MIN_PRIORITY, array(
+ 'provider' => $provider,
+ 'id' => $id,
+ 'userInfo' => $userInfo
+ ) );
+ $this->assertTrue( $loadSessionInfoFromStore( $info ) );
+ $this->assertTrue( $info->forceHTTPS() );
+ $this->assertSame( array(), $logger->getBuffer() );
+
+ $this->store->setSessionMeta( $id, array( 'forceHTTPS' => false ) + $metadata );
+ $info = new SessionInfo( SessionInfo::MIN_PRIORITY, array(
+ 'provider' => $provider,
+ 'id' => $id,
+ 'userInfo' => $userInfo,
+ 'forceHTTPS' => true
+ ) );
+ $this->assertTrue( $loadSessionInfoFromStore( $info ) );
+ $this->assertTrue( $info->forceHTTPS() );
+ $this->assertSame( array(), $logger->getBuffer() );
+
+ // Provider refreshSessionInfo() returning false
+ $info = new SessionInfo( SessionInfo::MIN_PRIORITY, array(
+ 'provider' => $provider3,
+ ) );
+ $this->assertFalse( $loadSessionInfoFromStore( $info ) );
+ $this->assertSame( array(), $logger->getBuffer() );
+
+ // Hook
+ $that = $this;
+ $called = false;
+ $data = array( 'foo' => 1 );
+ $this->store->setSession( $id, array( 'metadata' => $metadata, 'data' => $data ) );
+ $info = new SessionInfo( SessionInfo::MIN_PRIORITY, array(
+ 'provider' => $provider,
+ 'id' => $id,
+ 'userInfo' => $userInfo
+ ) );
+ $this->mergeMwGlobalArrayValue( 'wgHooks', array(
+ 'SessionCheckInfo' => array( function ( &$reason, $i, $r, $m, $d ) use (
+ $that, $info, $metadata, $data, $request, &$called
+ ) {
+ $that->assertSame( $info->getId(), $i->getId() );
+ $that->assertSame( $info->getProvider(), $i->getProvider() );
+ $that->assertSame( $info->getUserInfo(), $i->getUserInfo() );
+ $that->assertSame( $request, $r );
+ $that->assertEquals( $metadata, $m );
+ $that->assertEquals( $data, $d );
+ $called = true;
+ return false;
+ } )
+ ) );
+ $this->assertFalse( $loadSessionInfoFromStore( $info ) );
+ $this->assertTrue( $called );
+ $this->assertSame( array(
+ array( LogLevel::WARNING, 'Session X: Hook aborted' ),
+ ), $logger->getBuffer() );
+ $logger->clearBuffer();
+ }
+
+}
--- /dev/null
+<?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' ) )
+ );
+ }
+
+}
--- /dev/null
+<?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 );
+ }
+
+}
--- /dev/null
+<?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 ) );
+ }
+
+}
--- /dev/null
+<?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;
+ }
+
+}
--- /dev/null
+<?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() );
+ }
+
+}
'wgAllowCopyUploads' => true,
'wgAllowAsyncCopyUploads' => true,
) );
- wfSetupSession();
if ( wfLocalFile( 'UploadFromUrlTest.png' )->exists() ) {
$this->deleteFile( 'UploadFromUrlTest.png' );
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
$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 );
- }
}
--- /dev/null
+<?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 ) {
+ }
+}
--- /dev/null
+<?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' );
+ }
+
+}
global $wgLanguageConverterCacheType, $wgUseDatabaseMessages;
global $wgLocaltimezone, $wgLocalisationCacheConf;
global $wgDevelopmentWarnings;
+ global $wgSessionProviders;
// Inject test autoloader
require_once __DIR__ . '/../TestsAutoLoader.php';
$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(