Add SessionManager
SessionManager is a general-purpose session management framework, rather than the cookie-based sessions that PHP wants to provide us. While fallback is provided for using $_SESSION and other PHP session management functions, they should be avoided in favor of using SessionManager directly. For proof-of-concept extensions, see OAuth change Ib40b221 and CentralAuth change I27ccabdb. Bug: T111296 Change-Id: Ic1ffea74f3ccc8f93c8a23b795ecab6f06abca72
This commit is contained in:
parent
8ff78d13df
commit
a73c5b7395
64 changed files with 9725 additions and 663 deletions
|
|
@ -62,6 +62,30 @@ production.
|
|||
$wgSharedDB and $wgSharedTables are properly set even on the "central" wiki
|
||||
that all others are sharing from and that $wgLocalDatabases is set to the
|
||||
full list of sharing wikis on all those wikis.
|
||||
* Massive overhaul to session handling:
|
||||
** $wgSessionsInObjectCache is no longer supported and must be true, due to
|
||||
MediaWiki\Session\SessionManager. $wgSessionHandler is similarly no longer
|
||||
used.
|
||||
** ObjectCacheSessionHandler is removed, replaced with
|
||||
MediaWiki\Session\PhpSessionHandler.
|
||||
** PHP session handling in general ($_SESSION, session_id(), and so on) is
|
||||
deprecated. Use MediaWiki\Session\SessionManager instead. A new config
|
||||
variable, $wgPHPSessionHandling, is available to cause use of $_SESSION to
|
||||
issue a deprecation warning or to cause most PHP session handling to throw
|
||||
exceptions.
|
||||
** Deprecated UserSetCookies hook. Session-handling extensions should generally
|
||||
be creating a custom subclass of CookieSessionProvider. Other extensions
|
||||
messing with cookies can no longer count on user data being saved in cookies
|
||||
versus other methods.
|
||||
** Deprecated UserLoadFromSession hook, extensions should create a
|
||||
MediaWiki\Session\SessionProvider.
|
||||
** The User cannot be loaded from session until after Setup.php completes.
|
||||
Attempts to do so will be ignored and the User will remain unloaded.
|
||||
* MediaWiki will now auto-create users as necessary, removing the need for
|
||||
extensions to do so. An 'autocreateaccount' right is added to allow
|
||||
auto-creation when 'createaccount' is not granted to all users.
|
||||
* Deprecated AuthPluginAutoCreate hook in favor of LocalUserCreated.
|
||||
* Most cookie-handling methods in User are deprecated.
|
||||
|
||||
=== New features in 1.27 ===
|
||||
* $wgDataCenterId and $wgDataCenterRoles where added, which will serve as
|
||||
|
|
@ -106,6 +130,10 @@ production.
|
|||
* It is now possible to patrol file uploads (both for new files and new versions
|
||||
of existing files). Special:NewFiles has gained an option to filter by patrol
|
||||
status. This functionality can be disabled using $wgUseFilePatrol.
|
||||
* MediaWiki\Session infrastructure allows for easier use of session mechanisms
|
||||
other than the usual cookies.
|
||||
** SessionMetadata and SessionCheckInfo hooks allow for setting and checking
|
||||
custom session metadata.
|
||||
|
||||
=== External library changes in 1.27 ===
|
||||
|
||||
|
|
@ -119,6 +147,7 @@ production.
|
|||
* Added wikimedia/cldr-plural-rule-parser v1.0.0.
|
||||
* Added wikimedia/relpath v1.0.3.
|
||||
* Added wikimedia/running-stat v1.1.0.
|
||||
* Added wikimedia/php-session-serializer v1.0.3.
|
||||
|
||||
==== Removed and replaced external libraries ====
|
||||
|
||||
|
|
|
|||
13
autoload.php
13
autoload.php
|
|
@ -778,6 +778,18 @@ $wgAutoloadLocalClasses = array(
|
|||
'MediaWiki\\Logger\\Monolog\\WikiProcessor' => __DIR__ . '/includes/debug/logger/monolog/WikiProcessor.php',
|
||||
'MediaWiki\\Logger\\NullSpi' => __DIR__ . '/includes/debug/logger/NullSpi.php',
|
||||
'MediaWiki\\Logger\\Spi' => __DIR__ . '/includes/debug/logger/Spi.php',
|
||||
'MediaWiki\\Session\\CookieSessionProvider' => __DIR__ . '/includes/session/CookieSessionProvider.php',
|
||||
'MediaWiki\\Session\\ImmutableSessionProviderWithCookie' => __DIR__ . '/includes/session/ImmutableSessionProviderWithCookie.php',
|
||||
'MediaWiki\\Session\\PHPSessionHandler' => __DIR__ . '/includes/session/PHPSessionHandler.php',
|
||||
'MediaWiki\\Session\\Session' => __DIR__ . '/includes/session/Session.php',
|
||||
'MediaWiki\\Session\\SessionBackend' => __DIR__ . '/includes/session/SessionBackend.php',
|
||||
'MediaWiki\\Session\\SessionId' => __DIR__ . '/includes/session/SessionId.php',
|
||||
'MediaWiki\\Session\\SessionInfo' => __DIR__ . '/includes/session/SessionInfo.php',
|
||||
'MediaWiki\\Session\\SessionManager' => __DIR__ . '/includes/session/SessionManager.php',
|
||||
'MediaWiki\\Session\\SessionManagerInterface' => __DIR__ . '/includes/session/SessionManagerInterface.php',
|
||||
'MediaWiki\\Session\\SessionProvider' => __DIR__ . '/includes/session/SessionProvider.php',
|
||||
'MediaWiki\\Session\\SessionProviderInterface' => __DIR__ . '/includes/session/SessionProviderInterface.php',
|
||||
'MediaWiki\\Session\\UserInfo' => __DIR__ . '/includes/session/UserInfo.php',
|
||||
'MediaWiki\\Site\\MediaWikiPageNameNormalizer' => __DIR__ . '/includes/site/MediaWikiPageNameNormalizer.php',
|
||||
'MediaWiki\\Tidy\\Html5Depurate' => __DIR__ . '/includes/tidy/Html5Depurate.php',
|
||||
'MediaWiki\\Tidy\\RaggettBase' => __DIR__ . '/includes/tidy/RaggettBase.php',
|
||||
|
|
@ -862,7 +874,6 @@ $wgAutoloadLocalClasses = array(
|
|||
'ORAField' => __DIR__ . '/includes/db/DatabaseOracle.php',
|
||||
'ORAResult' => __DIR__ . '/includes/db/DatabaseOracle.php',
|
||||
'ObjectCache' => __DIR__ . '/includes/objectcache/ObjectCache.php',
|
||||
'ObjectCacheSessionHandler' => __DIR__ . '/includes/objectcache/ObjectCacheSessionHandler.php',
|
||||
'ObjectFactory' => __DIR__ . '/includes/libs/ObjectFactory.php',
|
||||
'ObjectFileCache' => __DIR__ . '/includes/cache/ObjectFileCache.php',
|
||||
'OldChangesList' => __DIR__ . '/includes/changes/OldChangesList.php',
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@
|
|||
"wikimedia/cldr-plural-rule-parser": "1.0.0",
|
||||
"wikimedia/composer-merge-plugin": "1.3.0",
|
||||
"wikimedia/ip-set": "1.0.1",
|
||||
"wikimedia/php-session-serializer": "1.0.3",
|
||||
"wikimedia/relpath": "1.0.3",
|
||||
"wikimedia/running-stat": "1.1.0",
|
||||
"wikimedia/utfnormal": "1.0.3",
|
||||
|
|
|
|||
|
|
@ -741,8 +741,9 @@ viewing.
|
|||
redirect was followed.
|
||||
&$article: target article (object)
|
||||
|
||||
'AuthPluginAutoCreate': Called when creating a local account for an user logged
|
||||
in from an external authentication method.
|
||||
'AuthPluginAutoCreate': DEPRECATED! Use the 'LocalUserCreated' hook instead.
|
||||
Called when creating a local account for an user logged in from an external
|
||||
authentication method.
|
||||
$user: User object created locally
|
||||
|
||||
'AuthPluginSetup': Update or replace authentication plugin object ($wgAuth).
|
||||
|
|
@ -2574,6 +2575,20 @@ $targetUser: the user whom to send watchlist email notification
|
|||
$title: the page title
|
||||
$enotif: EmailNotification object
|
||||
|
||||
'SessionCheckInfo': Validate a MediaWiki\Session\SessionInfo as it's being
|
||||
loaded from storage. Return false to prevent it from being used.
|
||||
&$reason: String rejection reason to be logged
|
||||
$info: MediaWiki\Session\SessionInfo being validated
|
||||
$request: WebRequest being loaded from
|
||||
$metadata: Array|false Metadata array for the MediaWiki\Session\Session
|
||||
$data: Array|false Data array for the MediaWiki\Session\Session
|
||||
|
||||
'SessionMetadata': Add metadata to a session being saved.
|
||||
$backend: MediaWiki\Session\SessionBackend being saved.
|
||||
&$metadata: Array Metadata to be stored. Add new keys here.
|
||||
$requests: Array of WebRequests potentially being saved to. Generally 0-1 real
|
||||
request and 0+ FauxRequests.
|
||||
|
||||
'SetupAfterCache': Called in Setup.php, after cache objects are set
|
||||
|
||||
'ShortPagesQuery': Allow extensions to modify the query used by
|
||||
|
|
@ -3292,8 +3307,9 @@ $name: user name
|
|||
$user: user object
|
||||
&$s: database query object
|
||||
|
||||
'UserLoadFromSession': Called to authenticate users on external/environmental
|
||||
means; occurs before session is loaded.
|
||||
'UserLoadFromSession': DEPRECATED! Create a MediaWiki\Session\SessionProvider instead.
|
||||
Called to authenticate users on external/environmental means; occurs before
|
||||
session is loaded.
|
||||
$user: user object being loaded
|
||||
&$result: set this to a boolean value to abort the normal authentication
|
||||
process
|
||||
|
|
@ -3384,9 +3400,13 @@ $user: User object
|
|||
'UserSaveSettings': Called when saving user settings.
|
||||
$user: User object
|
||||
|
||||
'UserSetCookies': Called when setting user cookies.
|
||||
'UserSetCookies': DEPRECATED! If you're trying to replace core session cookie
|
||||
handling, you want to create a subclass of MediaWiki\Session\CookieSessionProvider
|
||||
instead. Otherwise, you can no longer count on user data being saved to cookies
|
||||
versus some other mechanism.
|
||||
Called when setting user cookies.
|
||||
$user: User object
|
||||
&$session: session array, will be added to $_SESSION
|
||||
&$session: session array, will be added to the session
|
||||
&$cookies: cookies array mapping cookie name to its value
|
||||
|
||||
'UserSetEmail': Called when changing user email address.
|
||||
|
|
|
|||
|
|
@ -2181,7 +2181,7 @@ $wgMessageCacheType = CACHE_ANYTHING;
|
|||
$wgParserCacheType = CACHE_ANYTHING;
|
||||
|
||||
/**
|
||||
* The cache type for storing session data. Used if $wgSessionsInObjectCache is true.
|
||||
* The cache type for storing session data.
|
||||
*
|
||||
* For available types see $wgMainCacheType.
|
||||
*/
|
||||
|
|
@ -2316,30 +2316,29 @@ $wgParserCacheExpireTime = 86400;
|
|||
*
|
||||
* @deprecated since 1.20; Use $wgSessionsInObjectCache
|
||||
*/
|
||||
$wgSessionsInMemcached = false;
|
||||
$wgSessionsInMemcached = true;
|
||||
|
||||
/**
|
||||
* Store sessions in an object cache, configured by $wgSessionCacheType. This
|
||||
* can be useful to improve performance, or to avoid the locking behavior of
|
||||
* PHP's default session handler, which tends to prevent multiple requests for
|
||||
* the same user from acting concurrently.
|
||||
* @deprecated since 1.27, session data is always stored in object cache.
|
||||
*/
|
||||
$wgSessionsInObjectCache = false;
|
||||
$wgSessionsInObjectCache = true;
|
||||
|
||||
/**
|
||||
* The expiry time to use for session storage when $wgSessionsInObjectCache is
|
||||
* enabled, in seconds.
|
||||
* The expiry time to use for session storage, in seconds.
|
||||
*/
|
||||
$wgObjectCacheSessionExpiry = 3600;
|
||||
|
||||
/**
|
||||
* This is used for setting php's session.save_handler. In practice, you will
|
||||
* almost never need to change this ever. Other options might be 'user' or
|
||||
* 'session_mysql.' Setting to null skips setting this entirely (which might be
|
||||
* useful if you're doing cross-application sessions, see bug 11381)
|
||||
* @deprecated since 1.27, MediaWiki\\Session\\SessionManager doesn't use PHP session storage.
|
||||
*/
|
||||
$wgSessionHandler = null;
|
||||
|
||||
/**
|
||||
* Whether to use PHP session handling ($_SESSION and session_*() functions)
|
||||
* @var string 'enable', 'warn', or 'disable'
|
||||
*/
|
||||
$wgPHPSessionHandling = 'enable';
|
||||
|
||||
/**
|
||||
* If enabled, will send MemCached debugging information to $wgDebugLogFile
|
||||
*/
|
||||
|
|
@ -4660,6 +4659,24 @@ $wgUserrightsInterwikiDelimiter = '@';
|
|||
*/
|
||||
$wgSecureLogin = false;
|
||||
|
||||
/**
|
||||
* MediaWiki\Session\SessionProvider configuration.
|
||||
*
|
||||
* Value is an array of ObjectFactory specifications for the SessionProviders
|
||||
* to be used. Keys in the array are ignored. Order is not significant.
|
||||
*
|
||||
* @since 1.27
|
||||
*/
|
||||
$wgSessionProviders = array(
|
||||
'MediaWiki\\Session\\CookieSessionProvider' => array(
|
||||
'class' => 'MediaWiki\\Session\\CookieSessionProvider',
|
||||
'args' => array( array(
|
||||
'priority' => 30,
|
||||
'callUserSetCookiesHook' => true,
|
||||
) ),
|
||||
),
|
||||
);
|
||||
|
||||
/** @} */ # end user accounts }
|
||||
|
||||
/************************************************************************//**
|
||||
|
|
|
|||
|
|
@ -61,6 +61,10 @@ class DerivativeRequest extends FauxRequest {
|
|||
return $this->base->getAllHeaders();
|
||||
}
|
||||
|
||||
public function getSession() {
|
||||
return $this->base->getSession();
|
||||
}
|
||||
|
||||
public function getSessionData( $key ) {
|
||||
return $this->base->getSessionData( $key );
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,6 +23,8 @@
|
|||
* @file
|
||||
*/
|
||||
|
||||
use MediaWiki\Session\SessionManager;
|
||||
|
||||
/**
|
||||
* WebRequest clone which takes values from a provided array.
|
||||
*
|
||||
|
|
@ -30,7 +32,6 @@
|
|||
*/
|
||||
class FauxRequest extends WebRequest {
|
||||
private $wasPosted = false;
|
||||
private $session = array();
|
||||
private $requestUrl;
|
||||
protected $cookies = array();
|
||||
|
||||
|
|
@ -38,7 +39,8 @@ class FauxRequest extends WebRequest {
|
|||
* @param array $data Array of *non*-urlencoded key => value pairs, the
|
||||
* fake GET/POST values
|
||||
* @param bool $wasPosted Whether to treat the data as POST
|
||||
* @param array|null $session Session array or null
|
||||
* @param MediaWiki\\Session\\Session|array|null $session Session, session
|
||||
* data array, or null
|
||||
* @param string $protocol 'http' or 'https'
|
||||
* @throws MWException
|
||||
*/
|
||||
|
|
@ -53,8 +55,16 @@ class FauxRequest extends WebRequest {
|
|||
throw new MWException( "FauxRequest() got bogus data" );
|
||||
}
|
||||
$this->wasPosted = $wasPosted;
|
||||
if ( $session ) {
|
||||
$this->session = $session;
|
||||
if ( $session instanceof MediaWiki\Session\Session ) {
|
||||
$this->sessionId = $session->getSessionId();
|
||||
} elseif ( is_array( $session ) ) {
|
||||
$mwsession = SessionManager::singleton()->getEmptySession( $this );
|
||||
$this->sessionId = $mwsession->getSessionId();
|
||||
foreach ( $session as $key => $value ) {
|
||||
$mwsession->set( $key, $value );
|
||||
}
|
||||
} elseif ( $session !== null ) {
|
||||
throw new MWException( "FauxRequest() got bogus session" );
|
||||
}
|
||||
$this->protocol = $protocol;
|
||||
}
|
||||
|
|
@ -140,10 +150,6 @@ class FauxRequest extends WebRequest {
|
|||
}
|
||||
}
|
||||
|
||||
public function checkSessionCookie() {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @since 1.25
|
||||
*/
|
||||
|
|
@ -186,31 +192,15 @@ class FauxRequest extends WebRequest {
|
|||
}
|
||||
|
||||
/**
|
||||
* @param string $key
|
||||
* @return array|null
|
||||
*/
|
||||
public function getSessionData( $key ) {
|
||||
if ( isset( $this->session[$key] ) ) {
|
||||
return $this->session[$key];
|
||||
public function getSessionArray() {
|
||||
if ( $this->sessionId !== null ) {
|
||||
return iterator_to_array( $this->getSession() );
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $key
|
||||
* @param array $data
|
||||
*/
|
||||
public function setSessionData( $key, $data ) {
|
||||
$this->session[$key] = $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array|mixed|null
|
||||
*/
|
||||
public function getSessionArray() {
|
||||
return $this->session;
|
||||
}
|
||||
|
||||
/**
|
||||
* FauxRequests shouldn't depend on raw request data (but that could be implemented here)
|
||||
* @return string
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ if ( !defined( 'MEDIAWIKI' ) ) {
|
|||
|
||||
use Liuggio\StatsdClient\Sender\SocketSender;
|
||||
use MediaWiki\Logger\LoggerFactory;
|
||||
use MediaWiki\Session\SessionManager;
|
||||
|
||||
// Hide compatibility functions from Doxygen
|
||||
/// @cond
|
||||
|
|
@ -3007,9 +3008,12 @@ function wfBaseConvert( $input, $sourceBase, $destBase, $pad = 1,
|
|||
/**
|
||||
* Check if there is sufficient entropy in php's built-in session generation
|
||||
*
|
||||
* @deprecated since 1.27, PHP's session generation isn't used with
|
||||
* MediaWiki\\Session\\SessionManager
|
||||
* @return bool True = there is sufficient entropy
|
||||
*/
|
||||
function wfCheckEntropy() {
|
||||
wfDeprecated( __FUNCTION__, '1.27' );
|
||||
return (
|
||||
( wfIsWindows() && version_compare( PHP_VERSION, '5.3.3', '>=' ) )
|
||||
|| ini_get( 'session.entropy_file' )
|
||||
|
|
@ -3018,83 +3022,65 @@ function wfCheckEntropy() {
|
|||
}
|
||||
|
||||
/**
|
||||
* Override session_id before session startup if php's built-in
|
||||
* session generation code is not secure.
|
||||
* @deprecated since 1.27, PHP's session generation isn't used with
|
||||
* MediaWiki\\Session\\SessionManager
|
||||
*/
|
||||
function wfFixSessionID() {
|
||||
// If the cookie or session id is already set we already have a session and should abort
|
||||
if ( isset( $_COOKIE[session_name()] ) || session_id() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// PHP's built-in session entropy is enabled if:
|
||||
// - entropy_file is set or you're on Windows with php 5.3.3+
|
||||
// - AND entropy_length is > 0
|
||||
// We treat it as disabled if it doesn't have an entropy length of at least 32
|
||||
$entropyEnabled = wfCheckEntropy();
|
||||
|
||||
// If built-in entropy is not enabled or not sufficient override PHP's
|
||||
// built in session id generation code
|
||||
if ( !$entropyEnabled ) {
|
||||
wfDebug( __METHOD__ . ": PHP's built in entropy is disabled or not sufficient, " .
|
||||
"overriding session id generation using our cryptrand source.\n" );
|
||||
session_id( MWCryptRand::generateHex( 32 ) );
|
||||
}
|
||||
wfDeprecated( __FUNCTION__, '1.27' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset the session_id
|
||||
* Reset the session id
|
||||
*
|
||||
* @deprecated since 1.27, use MediaWiki\\Session\\SessionManager instead
|
||||
* @since 1.22
|
||||
*/
|
||||
function wfResetSessionID() {
|
||||
global $wgCookieSecure;
|
||||
$oldSessionId = session_id();
|
||||
$cookieParams = session_get_cookie_params();
|
||||
if ( wfCheckEntropy() && $wgCookieSecure == $cookieParams['secure'] ) {
|
||||
session_regenerate_id( false );
|
||||
} else {
|
||||
$tmp = $_SESSION;
|
||||
session_destroy();
|
||||
wfSetupSession( MWCryptRand::generateHex( 32 ) );
|
||||
$_SESSION = $tmp;
|
||||
wfDeprecated( __FUNCTION__, '1.27' );
|
||||
$session = SessionManager::getGlobalSession();
|
||||
$delay = $session->delaySave();
|
||||
|
||||
$session->resetId();
|
||||
|
||||
// Make sure a session is started, since that's what the old
|
||||
// wfResetSessionID() did.
|
||||
if ( session_id() !== $session->getId() ) {
|
||||
wfSetupSession( $session->getId() );
|
||||
}
|
||||
$newSessionId = session_id();
|
||||
|
||||
ScopedCallback::consume( $delay );
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialise php session
|
||||
*
|
||||
* @param bool $sessionId
|
||||
* @deprecated since 1.27, use MediaWiki\\Session\\SessionManager instead.
|
||||
* Generally, "using" SessionManager will be calling ->getSessionById() or
|
||||
* ::getGlobalSession() (depending on whether you were passing $sessionId
|
||||
* here), then calling $session->persist().
|
||||
* @param bool|string $sessionId
|
||||
*/
|
||||
function wfSetupSession( $sessionId = false ) {
|
||||
global $wgSessionsInObjectCache, $wgSessionHandler;
|
||||
global $wgCookiePath, $wgCookieDomain, $wgCookieSecure, $wgCookieHttpOnly;
|
||||
wfDeprecated( __FUNCTION__, '1.27' );
|
||||
|
||||
if ( $wgSessionsInObjectCache ) {
|
||||
ObjectCacheSessionHandler::install();
|
||||
} elseif ( $wgSessionHandler && $wgSessionHandler != ini_get( 'session.save_handler' ) ) {
|
||||
# Only set this if $wgSessionHandler isn't null and session.save_handler
|
||||
# hasn't already been set to the desired value (that causes errors)
|
||||
ini_set( 'session.save_handler', $wgSessionHandler );
|
||||
// If they're calling this, they probably want our session management even
|
||||
// if NO_SESSION was set for Setup.php.
|
||||
if ( !MediaWiki\Session\PHPSessionHandler::isInstalled() ) {
|
||||
MediaWiki\Session\PHPSessionHandler::install( SessionManager::singleton() );
|
||||
}
|
||||
|
||||
session_set_cookie_params(
|
||||
0, $wgCookiePath, $wgCookieDomain, $wgCookieSecure, $wgCookieHttpOnly );
|
||||
session_cache_limiter( 'private, must-revalidate' );
|
||||
if ( $sessionId ) {
|
||||
session_id( $sessionId );
|
||||
} else {
|
||||
wfFixSessionID();
|
||||
}
|
||||
|
||||
MediaWiki\suppressWarnings();
|
||||
session_start();
|
||||
MediaWiki\restoreWarnings();
|
||||
$session = SessionManager::getGlobalSession();
|
||||
$session->persist();
|
||||
|
||||
if ( $wgSessionsInObjectCache ) {
|
||||
ObjectCacheSessionHandler::renewCurrentSession();
|
||||
if ( session_id() !== $session->getId() ) {
|
||||
session_id( $session->getId() );
|
||||
}
|
||||
|
||||
MediaWiki\quietCall( 'session_start' );
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -664,8 +664,10 @@ class MediaWiki {
|
|||
if (
|
||||
$request->getProtocol() == 'http' &&
|
||||
(
|
||||
$request->getSession()->shouldForceHTTPS() ||
|
||||
// Check the cookie manually, for paranoia
|
||||
$request->getCookie( 'forceHTTPS', '' ) ||
|
||||
// check for prefixed version for currently logged in users
|
||||
// check for prefixed version that was used for a time in older MW versions
|
||||
$request->getCookie( 'forceHTTPS' ) ||
|
||||
// Avoid checking the user and groups unless it's enabled.
|
||||
(
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@
|
|||
*/
|
||||
|
||||
use MediaWiki\Logger\LoggerFactory;
|
||||
use MediaWiki\Session\SessionManager;
|
||||
use WrappedString\WrappedString;
|
||||
|
||||
/**
|
||||
|
|
@ -1977,11 +1978,9 @@ class OutputPage extends ContextSource {
|
|||
if ( $cookies === null ) {
|
||||
$config = $this->getConfig();
|
||||
$cookies = array_merge(
|
||||
SessionManager::singleton()->getVaryCookies(),
|
||||
array(
|
||||
$config->get( 'CookiePrefix' ) . 'Token',
|
||||
$config->get( 'CookiePrefix' ) . 'LoggedOut',
|
||||
"forceHTTPS",
|
||||
session_name()
|
||||
'forceHTTPS',
|
||||
),
|
||||
$config->get( 'CacheVaryCookies' )
|
||||
);
|
||||
|
|
@ -2033,6 +2032,9 @@ class OutputPage extends ContextSource {
|
|||
* @return string
|
||||
*/
|
||||
public function getVaryHeader() {
|
||||
foreach ( SessionManager::singleton()->getVaryHeaders() as $header => $options ) {
|
||||
$this->addVaryHeader( $header, $options );
|
||||
}
|
||||
return 'Vary: ' . join( ', ', array_keys( $this->mVaryHeader ) );
|
||||
}
|
||||
|
||||
|
|
@ -2050,6 +2052,10 @@ class OutputPage extends ContextSource {
|
|||
}
|
||||
$this->addVaryHeader( 'Cookie', $cookiesOption );
|
||||
|
||||
foreach ( SessionManager::singleton()->getVaryHeaders() as $header => $options ) {
|
||||
$this->addVaryHeader( $header, $options );
|
||||
}
|
||||
|
||||
$headers = array();
|
||||
foreach ( $this->mVaryHeader as $header => $option ) {
|
||||
$newheader = $header;
|
||||
|
|
@ -2173,8 +2179,8 @@ class OutputPage extends ContextSource {
|
|||
|
||||
if ( $this->mEnableClientCache ) {
|
||||
if (
|
||||
$config->get( 'UseSquid' ) && session_id() == '' && !$this->isPrintable() &&
|
||||
$this->mCdnMaxage != 0 && !$this->haveCacheVaryCookies()
|
||||
$config->get( 'UseSquid' ) && !SessionManager::getGlobalSession()->isPersistent() &&
|
||||
!$this->isPrintable() && $this->mCdnMaxage != 0 && !$this->haveCacheVaryCookies()
|
||||
) {
|
||||
if ( $config->get( 'UseESI' ) ) {
|
||||
# We'll purge the proxy cache explicitly, but require end user agents
|
||||
|
|
|
|||
|
|
@ -497,10 +497,25 @@ if ( $wgMaximalPasswordLength !== false ) {
|
|||
$wgPasswordPolicy['policies']['default']['MaximalPasswordLength'] = $wgMaximalPasswordLength;
|
||||
}
|
||||
|
||||
// Backwards compatibility with deprecated alias
|
||||
// Must be before call to wfSetupSession()
|
||||
if ( $wgSessionsInMemcached ) {
|
||||
$wgSessionsInObjectCache = true;
|
||||
// Backwards compatibility warning
|
||||
if ( !$wgSessionsInObjectCache && !$wgSessionsInMemcached ) {
|
||||
wfDeprecated( '$wgSessionsInObjectCache = false', '1.27' );
|
||||
if ( $wgSessionHandler ) {
|
||||
wfDeprecated( '$wgSessionsHandler', '1.27' );
|
||||
}
|
||||
$cacheType = get_class( ObjectCache::getInstance( $wgSessionCacheType ) );
|
||||
wfDebugLog(
|
||||
"Session data will be stored in \"$cacheType\" cache with " .
|
||||
"expiry $wgObjectCacheSessionExpiry seconds"
|
||||
);
|
||||
}
|
||||
$wgSessionsInObjectCache = true;
|
||||
|
||||
if ( $wgPHPSessionHandling !== 'enable' &&
|
||||
$wgPHPSessionHandling !== 'warn' &&
|
||||
$wgPHPSessionHandling !== 'disable'
|
||||
) {
|
||||
$wgPHPSessionHandling = 'warn';
|
||||
}
|
||||
|
||||
Profiler::instance()->scopedProfileOut( $ps_default );
|
||||
|
|
@ -655,20 +670,6 @@ Profiler::instance()->scopedProfileOut( $ps_memcached );
|
|||
// Most of the config is out, some might want to run hooks here.
|
||||
Hooks::run( 'SetupAfterCache' );
|
||||
|
||||
$ps_session = Profiler::instance()->scopedProfileIn( $fname . '-session' );
|
||||
|
||||
if ( !defined( 'MW_NO_SESSION' ) && !$wgCommandLineMode ) {
|
||||
// If session.auto_start is there, we can't touch session name
|
||||
if ( !wfIniGetBool( 'session.auto_start' ) ) {
|
||||
session_name( $wgSessionName ? $wgSessionName : $wgCookiePrefix . '_session' );
|
||||
}
|
||||
|
||||
if ( $wgRequest->checkSessionCookie() || isset( $_COOKIE[$wgCookiePrefix . 'Token'] ) ) {
|
||||
wfSetupSession();
|
||||
}
|
||||
}
|
||||
|
||||
Profiler::instance()->scopedProfileOut( $ps_session );
|
||||
$ps_globals = Profiler::instance()->scopedProfileIn( $fname . '-globals' );
|
||||
|
||||
/**
|
||||
|
|
@ -681,6 +682,56 @@ $wgContLang->initContLang();
|
|||
// Now that variant lists may be available...
|
||||
$wgRequest->interpolateTitle();
|
||||
|
||||
if ( !is_object( $wgAuth ) ) {
|
||||
$wgAuth = new AuthPlugin;
|
||||
Hooks::run( 'AuthPluginSetup', array( &$wgAuth ) );
|
||||
}
|
||||
|
||||
// Set up the session
|
||||
$ps_session = Profiler::instance()->scopedProfileIn( $fname . '-session' );
|
||||
if ( !defined( 'MW_NO_SESSION' ) && !$wgCommandLineMode ) {
|
||||
// If session.auto_start is there, we can't touch session name
|
||||
if ( $wgPHPSessionHandling !== 'disable' && !wfIniGetBool( 'session.auto_start' ) ) {
|
||||
session_name( $wgSessionName ? $wgSessionName : $wgCookiePrefix . '_session' );
|
||||
}
|
||||
|
||||
// Create the SessionManager singleton and set up our session handler
|
||||
MediaWiki\Session\PHPSessionHandler::install(
|
||||
MediaWiki\Session\SessionManager::singleton()
|
||||
);
|
||||
|
||||
// Initialize the session
|
||||
try {
|
||||
$session = MediaWiki\Session\SessionManager::getGlobalSession();
|
||||
} catch ( OverflowException $ex ) {
|
||||
if ( isset( $ex->sessionInfos ) && count( $ex->sessionInfos ) >= 2 ) {
|
||||
// The exception is because the request had multiple possible
|
||||
// sessions tied for top priority. Report this to the user.
|
||||
$list = array();
|
||||
foreach ( $ex->sessionInfos as $info ) {
|
||||
$list[] = $info->getProvider()->describe( $wgContLang );
|
||||
}
|
||||
$list = $wgContLang->listToText( $list );
|
||||
throw new HttpError( 400,
|
||||
Message::newFromKey( 'sessionmanager-tie', $list )->inLanguage( $wgContLang )->plain()
|
||||
);
|
||||
}
|
||||
|
||||
// Not the one we want, rethrow
|
||||
throw $ex;
|
||||
}
|
||||
|
||||
$session->renew();
|
||||
if ( MediaWiki\Session\PHPSessionHandler::isEnabled() &&
|
||||
( $session->isPersistent() || $session->shouldRememberUser() )
|
||||
) {
|
||||
// Start the PHP-session for backwards compatibility
|
||||
session_id( $session->getId() );
|
||||
MediaWiki\quietCall( 'session_start' );
|
||||
}
|
||||
}
|
||||
Profiler::instance()->scopedProfileOut( $ps_session );
|
||||
|
||||
/**
|
||||
* @var User $wgUser
|
||||
*/
|
||||
|
|
@ -701,11 +752,6 @@ $wgOut = RequestContext::getMain()->getOutput(); // BackCompat
|
|||
*/
|
||||
$wgParser = new StubObject( 'wgParser', $wgParserConf['class'], array( $wgParserConf ) );
|
||||
|
||||
if ( !is_object( $wgAuth ) ) {
|
||||
$wgAuth = new AuthPlugin;
|
||||
Hooks::run( 'AuthPluginSetup', array( &$wgAuth ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* @var Title $wgTitle
|
||||
*/
|
||||
|
|
@ -737,6 +783,16 @@ foreach ( $wgExtensionFunctions as $func ) {
|
|||
Profiler::instance()->scopedProfileOut( $ps_ext_func );
|
||||
}
|
||||
|
||||
// If the session user has a 0 id but a valid name, that means we need to
|
||||
// autocreate it.
|
||||
$sessionUser = MediaWiki\Session\SessionManager::getGlobalSession()->getUser();
|
||||
if ( $sessionUser->getId() === 0 && User::isValidUserName( $sessionUser->getName() ) ) {
|
||||
$ps_autocreate = Profiler::instance()->scopedProfileIn( $fname . '-autocreate' );
|
||||
MediaWiki\Session\SessionManager::autoCreateUser( $sessionUser );
|
||||
Profiler::instance()->scopedProfileOut( $ps_autocreate );
|
||||
}
|
||||
unset( $sessionUser );
|
||||
|
||||
wfDebug( "Fully initialised\n" );
|
||||
$wgFullyInitialised = true;
|
||||
|
||||
|
|
|
|||
|
|
@ -23,6 +23,8 @@
|
|||
* @file
|
||||
*/
|
||||
|
||||
use MediaWiki\Session\SessionManager;
|
||||
|
||||
/**
|
||||
* The WebRequest class encapsulates getting at data passed in the
|
||||
* URL or via a POSTed form stripping illegal input characters and
|
||||
|
|
@ -63,6 +65,13 @@ class WebRequest {
|
|||
*/
|
||||
protected $protocol;
|
||||
|
||||
/**
|
||||
* @var \\MediaWiki\\Session\\SessionId|null Session ID to use for this
|
||||
* request. We can't save the session directly due to reference cycles not
|
||||
* working too well (slow GC in Zend and never collected in HHVM).
|
||||
*/
|
||||
protected $sessionId = null;
|
||||
|
||||
public function __construct() {
|
||||
$this->requestTime = isset( $_SERVER['REQUEST_TIME_FLOAT'] )
|
||||
? $_SERVER['REQUEST_TIME_FLOAT'] : microtime( true );
|
||||
|
|
@ -638,18 +647,44 @@ class WebRequest {
|
|||
}
|
||||
|
||||
/**
|
||||
* Returns true if there is a session cookie set.
|
||||
* Return the session for this request
|
||||
* @since 1.27
|
||||
* @note For performance, keep the session locally if you will be making
|
||||
* much use of it instead of calling this method repeatedly.
|
||||
* @return MediaWiki\\Session\\Session
|
||||
*/
|
||||
public function getSession() {
|
||||
if ( $this->sessionId !== null ) {
|
||||
return SessionManager::singleton()->getSessionById( (string)$this->sessionId, false, $this );
|
||||
}
|
||||
|
||||
$session = SessionManager::singleton()->getSessionForRequest( $this );
|
||||
$this->sessionId = $session->getSessionId();
|
||||
return $session;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the session for this request
|
||||
* @since 1.27
|
||||
* @private For use by MediaWiki\\Session classes only
|
||||
* @param MediaWiki\\Session\\SessionId $sessionId
|
||||
*/
|
||||
public function setSessionId( MediaWiki\Session\SessionId $sessionId ) {
|
||||
$this->sessionId = $sessionId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the request has a persistent session.
|
||||
* This does not necessarily mean that the user is logged in!
|
||||
*
|
||||
* If you want to check for an open session, use session_id()
|
||||
* instead; that will also tell you if the session was opened
|
||||
* during the current request (in which case the cookie will
|
||||
* be sent back to the client at the end of the script run).
|
||||
*
|
||||
* @deprecated since 1.27, use
|
||||
* \\MediaWiki\\Session\\SessionManager::singleton()->getPersistedSessionId()
|
||||
* instead.
|
||||
* @return bool
|
||||
*/
|
||||
public function checkSessionCookie() {
|
||||
return isset( $_COOKIE[session_name()] );
|
||||
wfDeprecated( __METHOD__, '1.27' );
|
||||
return SessionManager::singleton()->getPersistedSessionId( $this ) !== null;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -907,26 +942,25 @@ class WebRequest {
|
|||
}
|
||||
|
||||
/**
|
||||
* Get data from $_SESSION
|
||||
* Get data from the session
|
||||
*
|
||||
* @param string $key Name of key in $_SESSION
|
||||
* @note Prefer $this->getSession() instead if making multiple calls.
|
||||
* @param string $key Name of key in the session
|
||||
* @return mixed
|
||||
*/
|
||||
public function getSessionData( $key ) {
|
||||
if ( !isset( $_SESSION[$key] ) ) {
|
||||
return null;
|
||||
}
|
||||
return $_SESSION[$key];
|
||||
return $this->getSession()->get( $key );
|
||||
}
|
||||
|
||||
/**
|
||||
* Set session data
|
||||
*
|
||||
* @param string $key Name of key in $_SESSION
|
||||
* @note Prefer $this->getSession() instead if making multiple calls.
|
||||
* @param string $key Name of key in the session
|
||||
* @param mixed $data
|
||||
*/
|
||||
public function setSessionData( $key, $data ) {
|
||||
$_SESSION[$key] = $data;
|
||||
return $this->getSession()->set( $key, $data );
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -27,6 +27,11 @@
|
|||
*/
|
||||
class WebResponse {
|
||||
|
||||
/** @var array Used to record set cookies, because PHP's setcookie() will
|
||||
* happily send an identical Set-Cookie to the client.
|
||||
*/
|
||||
protected static $setCookies = array();
|
||||
|
||||
/**
|
||||
* Output an HTTP header, wrapper for PHP's header()
|
||||
* @param string $string Header to output
|
||||
|
|
@ -62,6 +67,15 @@ class WebResponse {
|
|||
HttpStatus::header( $code );
|
||||
}
|
||||
|
||||
/**
|
||||
* Test if headers have been sent
|
||||
* @since 1.27
|
||||
* @return bool
|
||||
*/
|
||||
public function headersSent() {
|
||||
return headers_sent();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the browser cookie
|
||||
* @param string $name The name of the cookie.
|
||||
|
|
@ -115,25 +129,26 @@ class WebResponse {
|
|||
$func = $options['raw'] ? 'setrawcookie' : 'setcookie';
|
||||
|
||||
if ( Hooks::run( 'WebResponseSetCookie', array( &$name, &$value, &$expire, $options ) ) ) {
|
||||
wfDebugLog( 'cookie',
|
||||
$func . ': "' . implode( '", "',
|
||||
array(
|
||||
$options['prefix'] . $name,
|
||||
$value,
|
||||
$expire,
|
||||
$options['path'],
|
||||
$options['domain'],
|
||||
$options['secure'],
|
||||
$options['httpOnly'] ) ) . '"' );
|
||||
|
||||
call_user_func( $func,
|
||||
$options['prefix'] . $name,
|
||||
$value,
|
||||
$expire,
|
||||
$options['path'],
|
||||
$options['domain'],
|
||||
$options['secure'],
|
||||
$options['httpOnly'] );
|
||||
$cookie = $options['prefix'] . $name;
|
||||
$data = array(
|
||||
(string)$cookie,
|
||||
(string)$value,
|
||||
(int)$expire,
|
||||
(string)$options['path'],
|
||||
(string)$options['domain'],
|
||||
(bool)$options['secure'],
|
||||
(bool)$options['httpOnly'],
|
||||
);
|
||||
if ( !isset( self::$setCookies[$cookie] ) ||
|
||||
self::$setCookies[$cookie] !== array( $func, $data )
|
||||
) {
|
||||
wfDebugLog( 'cookie', $func . ': "' . implode( '", "', $data ) . '"' );
|
||||
if ( call_user_func_array( $func, $data ) ) {
|
||||
self::$setCookies[$cookie] = array( $func, $data );
|
||||
}
|
||||
} else {
|
||||
wfDebugLog( 'cookie', 'already set ' . $func . ': "' . implode( '", "', $data ) . '"' );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -156,7 +171,7 @@ class WebResponse {
|
|||
*/
|
||||
class FauxResponse extends WebResponse {
|
||||
private $headers;
|
||||
private $cookies;
|
||||
private $cookies = array();
|
||||
private $code;
|
||||
|
||||
/**
|
||||
|
|
@ -192,6 +207,10 @@ class FauxResponse extends WebResponse {
|
|||
$this->code = intval( $code );
|
||||
}
|
||||
|
||||
public function headersSent() {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $key The name of the header to get (case insensitive).
|
||||
* @return string|null The header value (if set); null otherwise.
|
||||
|
|
|
|||
|
|
@ -83,7 +83,8 @@ class RawAction extends FormlessAction {
|
|||
$response->header( 'Content-type: ' . $contentType . '; charset=UTF-8' );
|
||||
// Output may contain user-specific data;
|
||||
// vary generated content for open sessions on private wikis
|
||||
$privateCache = !User::isEveryoneAllowed( 'read' ) && ( $smaxage == 0 || session_id() != '' );
|
||||
$privateCache = !User::isEveryoneAllowed( 'read' ) &&
|
||||
( $smaxage == 0 || MediaWiki\Session\SessionManager::getGlobalSession()->isPersistent() );
|
||||
// Don't accidentally cache cookies if user is logged in (T55032)
|
||||
$privateCache = $privateCache || $this->getUser()->isLoggedIn();
|
||||
$mode = $privateCache ? 'private' : 'public';
|
||||
|
|
|
|||
|
|
@ -32,10 +32,8 @@ class SubmitAction extends EditAction {
|
|||
}
|
||||
|
||||
public function show() {
|
||||
if ( session_id() === '' ) {
|
||||
// Send a cookie so anons get talk message notifications
|
||||
wfSetupSession();
|
||||
}
|
||||
// Send a cookie so anons get talk message notifications
|
||||
MediaWiki\Session\SessionManager::getGlobalSession()->persist();
|
||||
|
||||
parent::show();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -59,10 +59,8 @@ class ApiCreateAccount extends ApiBase {
|
|||
|
||||
$params = $this->extractRequestParams();
|
||||
|
||||
// Init session if necessary
|
||||
if ( session_id() == '' ) {
|
||||
wfSetupSession();
|
||||
}
|
||||
// Make sure session is persisted
|
||||
MediaWiki\Session\SessionManager::getGlobalSession()->persist();
|
||||
|
||||
if ( $params['mailpassword'] && !$params['email'] ) {
|
||||
$this->dieUsageMsg( 'noemail' );
|
||||
|
|
|
|||
|
|
@ -62,9 +62,19 @@ class ApiLogin extends ApiBase {
|
|||
|
||||
$result = array();
|
||||
|
||||
// Init session if necessary
|
||||
if ( session_id() == '' ) {
|
||||
wfSetupSession();
|
||||
// Make sure session is persisted
|
||||
$session = MediaWiki\Session\SessionManager::getGlobalSession();
|
||||
$session->persist();
|
||||
|
||||
// Make sure it's possible to log in
|
||||
if ( !$session->canSetUser() ) {
|
||||
$this->getResult()->addValue( null, 'login', array(
|
||||
'result' => 'Aborted',
|
||||
'reason' => 'Cannot log in when using ' .
|
||||
$session->getProvider()->describe( Language::factory( 'en' ) ),
|
||||
) );
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$context = new DerivativeContext( $this->getContext() );
|
||||
|
|
@ -107,7 +117,7 @@ class ApiLogin extends ApiBase {
|
|||
// SessionManager/AuthManager are *really* going to break it.
|
||||
$result['lgtoken'] = $user->getToken();
|
||||
$result['cookieprefix'] = $this->getConfig()->get( 'CookiePrefix' );
|
||||
$result['sessionid'] = session_id();
|
||||
$result['sessionid'] = MediaWiki\Session\SessionManager::getGlobalSession()->getId();
|
||||
break;
|
||||
|
||||
case LoginForm::NEED_TOKEN:
|
||||
|
|
@ -116,7 +126,7 @@ class ApiLogin extends ApiBase {
|
|||
|
||||
// @todo: See above about deprecation
|
||||
$result['cookieprefix'] = $this->getConfig()->get( 'CookiePrefix' );
|
||||
$result['sessionid'] = session_id();
|
||||
$result['sessionid'] = MediaWiki\Session\SessionManager::getGlobalSession()->getId();
|
||||
break;
|
||||
|
||||
case LoginForm::WRONG_TOKEN:
|
||||
|
|
|
|||
|
|
@ -33,6 +33,16 @@
|
|||
class ApiLogout extends ApiBase {
|
||||
|
||||
public function execute() {
|
||||
// Make sure it's possible to log out
|
||||
$session = MediaWiki\Session\SessionManager::getGlobalSession();
|
||||
if ( !$session->canSetUser() ) {
|
||||
$this->dieUsage(
|
||||
'Cannot log out when using ' .
|
||||
$session->getProvider()->describe( Language::factory( 'en' ) ),
|
||||
'cannotlogout'
|
||||
);
|
||||
}
|
||||
|
||||
$user = $this->getUser();
|
||||
$oldName = $user->getName();
|
||||
$user->logout();
|
||||
|
|
|
|||
|
|
@ -769,7 +769,7 @@ class ApiMain extends ApiBase {
|
|||
return;
|
||||
}
|
||||
// Logged out, send normal public headers below
|
||||
} elseif ( session_id() != '' ) {
|
||||
} elseif ( MediaWiki\Session\SessionManager::getGlobalSession()->isPersistent() ) {
|
||||
// Logged in or otherwise has session (e.g. anonymous users who have edited)
|
||||
// Mark request private
|
||||
$response->header( "Cache-Control: $privateCache" );
|
||||
|
|
|
|||
|
|
@ -513,7 +513,7 @@ class RequestContext implements IContextSource, MutableContext {
|
|||
return array(
|
||||
'ip' => $this->getRequest()->getIP(),
|
||||
'headers' => $this->getRequest()->getAllHeaders(),
|
||||
'sessionId' => session_id(),
|
||||
'sessionId' => MediaWiki\Session\SessionManager::getGlobalSession()->getId(),
|
||||
'userId' => $this->getUser()->getId()
|
||||
);
|
||||
}
|
||||
|
|
@ -541,7 +541,9 @@ class RequestContext implements IContextSource, MutableContext {
|
|||
* @since 1.21
|
||||
*/
|
||||
public static function importScopedSession( array $params ) {
|
||||
if ( session_id() != '' && strlen( $params['sessionId'] ) ) {
|
||||
if ( strlen( $params['sessionId'] ) &&
|
||||
MediaWiki\Session\SessionManager::getGlobalSession()->isPersistent()
|
||||
) {
|
||||
// Sanity check to avoid sending random cookies for the wrong users.
|
||||
// This method should only called by CLI scripts or by HTTP job runners.
|
||||
throw new MWException( "Sessions can only be imported when none is active." );
|
||||
|
|
@ -563,23 +565,37 @@ class RequestContext implements IContextSource, MutableContext {
|
|||
global $wgRequest, $wgUser;
|
||||
|
||||
$context = RequestContext::getMain();
|
||||
|
||||
// Commit and close any current session
|
||||
session_write_close(); // persist
|
||||
session_id( '' ); // detach
|
||||
$_SESSION = array(); // clear in-memory array
|
||||
// Remove any user IP or agent information
|
||||
$context->setRequest( new FauxRequest() );
|
||||
if ( MediaWiki\Session\PHPSessionHandler::isEnabled() ) {
|
||||
session_write_close(); // persist
|
||||
session_id( '' ); // detach
|
||||
$_SESSION = array(); // clear in-memory array
|
||||
}
|
||||
|
||||
// Get new session, if applicable
|
||||
$session = null;
|
||||
if ( strlen( $params['sessionId'] ) ) { // don't make a new random ID
|
||||
$session = MediaWiki\Session\SessionManager::singleton()
|
||||
->getSessionById( $params['sessionId'] );
|
||||
}
|
||||
|
||||
// Remove any user IP or agent information, and attach the request
|
||||
// with the new session.
|
||||
$context->setRequest( new FauxRequest( array(), false, $session ) );
|
||||
$wgRequest = $context->getRequest(); // b/c
|
||||
|
||||
// Now that all private information is detached from the user, it should
|
||||
// be safe to load the new user. If errors occur or an exception is thrown
|
||||
// and caught (leaving the main context in a mixed state), there is no risk
|
||||
// of the User object being attached to the wrong IP, headers, or session.
|
||||
$context->setUser( $user );
|
||||
$wgUser = $context->getUser(); // b/c
|
||||
if ( strlen( $params['sessionId'] ) ) { // don't make a new random ID
|
||||
wfSetupSession( $params['sessionId'] ); // sets $_SESSION
|
||||
if ( $session && MediaWiki\Session\PHPSessionHandler::isEnabled() ) {
|
||||
session_id( $session->getId() );
|
||||
MediaWiki\quietCall( 'session_start' );
|
||||
}
|
||||
$request = new FauxRequest( array(), false, $_SESSION );
|
||||
$request = new FauxRequest( array(), false, $session );
|
||||
$request->setIP( $params['ip'] );
|
||||
foreach ( $params['headers'] as $name => $value ) {
|
||||
$request->setHeader( $name, $value );
|
||||
|
|
|
|||
|
|
@ -93,10 +93,10 @@ class UploadFromUrlJob extends Job {
|
|||
$this->params['url'] )->text()
|
||||
);
|
||||
} else {
|
||||
wfSetupSession( $this->params['sessionId'] );
|
||||
$this->storeResultInSession( 'Warning',
|
||||
$session = MediaWiki\Session\SessionManager::singleton()
|
||||
->getSessionById( $this->params['sessionId'] );
|
||||
$this->storeResultInSession( $session, 'Warning',
|
||||
'warnings', $warnings );
|
||||
session_write_close();
|
||||
}
|
||||
|
||||
return true;
|
||||
|
|
@ -139,15 +139,15 @@ class UploadFromUrlJob extends Job {
|
|||
)->text() );
|
||||
}
|
||||
} else {
|
||||
wfSetupSession( $this->params['sessionId'] );
|
||||
$session = MediaWiki\Session\SessionManager::singleton()
|
||||
->getSessionById( $this->params['sessionId'] );
|
||||
if ( $status->isOk() ) {
|
||||
$this->storeResultInSession( 'Success',
|
||||
$this->storeResultInSession( $session, 'Success',
|
||||
'filename', $this->upload->getLocalFile()->getName() );
|
||||
} else {
|
||||
$this->storeResultInSession( 'Failure',
|
||||
$this->storeResultInSession( $session, 'Failure',
|
||||
'errors', $status->getErrorsArray() );
|
||||
}
|
||||
session_write_close();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -155,33 +155,55 @@ class UploadFromUrlJob extends Job {
|
|||
* Store a result in the session data. Note that the caller is responsible
|
||||
* for appropriate session_start and session_write_close calls.
|
||||
*
|
||||
* @param MediaWiki\\Session\\Session $session Session to store result into
|
||||
* @param string $result The result (Success|Warning|Failure)
|
||||
* @param string $dataKey The key of the extra data
|
||||
* @param mixed $dataValue The extra data itself
|
||||
*/
|
||||
protected function storeResultInSession( $result, $dataKey, $dataValue ) {
|
||||
$session =& self::getSessionData( $this->params['sessionKey'] );
|
||||
$session['result'] = $result;
|
||||
$session[$dataKey] = $dataValue;
|
||||
protected function storeResultInSession(
|
||||
MediaWiki\Session\Session $session, $result, $dataKey, $dataValue
|
||||
) {
|
||||
$data = self::getSessionData( $session, $this->params['sessionKey'] );
|
||||
$data['result'] = $result;
|
||||
$data[$dataKey] = $dataValue;
|
||||
self::setSessionData( $session, $this->params['sessionKey'], $data );
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the session data. Sets the initial result to queued.
|
||||
*/
|
||||
public function initializeSessionData() {
|
||||
$session =& self::getSessionData( $this->params['sessionKey'] );
|
||||
$session['result'] = 'Queued';
|
||||
$session = MediaWiki\Session\SessionManager::getGlobalSession();
|
||||
$data = self::getSessionData( $session, $this->params['sessionKey'] );
|
||||
$data['result'] = 'Queued';
|
||||
self::setSessionData( $session, $this->params['sessionKey'], $data );
|
||||
}
|
||||
|
||||
/**
|
||||
* @param MediaWiki\\Session\\Session $session
|
||||
* @param string $key
|
||||
* @return mixed
|
||||
*/
|
||||
public static function &getSessionData( $key ) {
|
||||
if ( !isset( $_SESSION[self::SESSION_KEYNAME][$key] ) ) {
|
||||
$_SESSION[self::SESSION_KEYNAME][$key] = array();
|
||||
public static function getSessionData( MediaWiki\Session\Session $session, $key ) {
|
||||
$data = $session->get( self::SESSION_KEYNAME );
|
||||
if ( !is_array( $data ) || !isset( $data[$key] ) ) {
|
||||
self::setSessionData( $session, $key, array() );
|
||||
return array();
|
||||
}
|
||||
return $data[$key];
|
||||
}
|
||||
|
||||
return $_SESSION[self::SESSION_KEYNAME][$key];
|
||||
/**
|
||||
* @param MediaWiki\\Session\\Session $session
|
||||
* @param string $key
|
||||
* @param mixed $value
|
||||
*/
|
||||
public static function setSessionData( MediaWiki\Session\Session $session, $key, $value ) {
|
||||
$data = $session->get( self::SESSION_KEYNAME, array() );
|
||||
if ( !is_array( $data ) ) {
|
||||
$data = array();
|
||||
}
|
||||
$data[$key] = $value;
|
||||
$session->set( self::SESSION_KEYNAME, $data );
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,207 +0,0 @@
|
|||
<?php
|
||||
/**
|
||||
* Session storage in object cache.
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program; if not, write to the Free Software Foundation, Inc.,
|
||||
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
* http://www.gnu.org/copyleft/gpl.html
|
||||
*
|
||||
* @file
|
||||
* @ingroup Cache
|
||||
*/
|
||||
|
||||
use MediaWiki\Logger\LoggerFactory;
|
||||
|
||||
/**
|
||||
* Session storage in object cache.
|
||||
* Used if $wgSessionsInObjectCache is true.
|
||||
*
|
||||
* @ingroup Cache
|
||||
*/
|
||||
class ObjectCacheSessionHandler {
|
||||
/** @var array Map of (session ID => SHA-1 of the data) */
|
||||
protected static $hashCache = array();
|
||||
|
||||
/**
|
||||
* Install a session handler for the current web request
|
||||
*/
|
||||
static function install() {
|
||||
session_set_save_handler(
|
||||
array( __CLASS__, 'open' ),
|
||||
array( __CLASS__, 'close' ),
|
||||
array( __CLASS__, 'read' ),
|
||||
array( __CLASS__, 'write' ),
|
||||
array( __CLASS__, 'destroy' ),
|
||||
array( __CLASS__, 'gc' ) );
|
||||
|
||||
// It's necessary to register a shutdown function to call session_write_close(),
|
||||
// because by the time the request shutdown function for the session module is
|
||||
// called, the BagOStuff has already been destroyed. Shutdown functions registered
|
||||
// this way are called before object destruction.
|
||||
register_shutdown_function( array( __CLASS__, 'handleShutdown' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the cache storage object to use for session storage
|
||||
* @return BagOStuff
|
||||
*/
|
||||
protected static function getCache() {
|
||||
global $wgSessionCacheType;
|
||||
|
||||
return ObjectCache::getInstance( $wgSessionCacheType );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a cache key for the given session id.
|
||||
*
|
||||
* @param string $id Session id
|
||||
* @return string Cache key
|
||||
*/
|
||||
protected static function getKey( $id ) {
|
||||
return wfMemcKey( 'session', $id );
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $data
|
||||
* @return string
|
||||
*/
|
||||
protected static function getHash( $data ) {
|
||||
return sha1( serialize( $data ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback when opening a session.
|
||||
*
|
||||
* @param string $save_path Path used to store session files, unused
|
||||
* @param string $session_name Session name
|
||||
* @return bool Success
|
||||
*/
|
||||
static function open( $save_path, $session_name ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback when closing a session.
|
||||
* NOP.
|
||||
*
|
||||
* @return bool Success
|
||||
*/
|
||||
static function close() {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback when reading session data.
|
||||
*
|
||||
* @param string $id Session id
|
||||
* @return mixed Session data
|
||||
*/
|
||||
static function read( $id ) {
|
||||
$stime = microtime( true );
|
||||
$data = self::getCache()->get( self::getKey( $id ) );
|
||||
$real = microtime( true ) - $stime;
|
||||
|
||||
RequestContext::getMain()->getStats()->timing( "session.read", 1000 * $real );
|
||||
|
||||
self::$hashCache = array( $id => self::getHash( $data ) );
|
||||
|
||||
return ( $data === false ) ? '' : $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback when writing session data.
|
||||
*
|
||||
* @param string $id Session id
|
||||
* @param string $data Session data
|
||||
* @return bool Success
|
||||
*/
|
||||
static function write( $id, $data ) {
|
||||
global $wgObjectCacheSessionExpiry;
|
||||
|
||||
// Only issue a write if anything changed (PHP 5.6 already does this)
|
||||
if ( !isset( self::$hashCache[$id] )
|
||||
|| self::getHash( $data ) !== self::$hashCache[$id]
|
||||
) {
|
||||
$stime = microtime( true );
|
||||
self::getCache()->set( self::getKey( $id ), $data, $wgObjectCacheSessionExpiry );
|
||||
$real = microtime( true ) - $stime;
|
||||
|
||||
RequestContext::getMain()->getStats()->timing( "session.write", 1000 * $real );
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback to destroy a session when calling session_destroy().
|
||||
*
|
||||
* @param string $id Session id
|
||||
* @return bool Success
|
||||
*/
|
||||
static function destroy( $id ) {
|
||||
$stime = microtime( true );
|
||||
self::getCache()->delete( self::getKey( $id ) );
|
||||
$real = microtime( true ) - $stime;
|
||||
|
||||
RequestContext::getMain()->getStats()->timing( "session.destroy", 1000 * $real );
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback to execute garbage collection.
|
||||
* NOP: Object caches perform garbage collection implicitly
|
||||
*
|
||||
* @param int $maxlifetime Maximum session life time
|
||||
* @return bool Success
|
||||
*/
|
||||
static function gc( $maxlifetime ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Shutdown function.
|
||||
* See the comment inside ObjectCacheSessionHandler::install for rationale.
|
||||
*/
|
||||
static function handleShutdown() {
|
||||
session_write_close();
|
||||
}
|
||||
|
||||
/**
|
||||
* Pre-emptive session renewal function
|
||||
*/
|
||||
static function renewCurrentSession() {
|
||||
global $wgObjectCacheSessionExpiry;
|
||||
|
||||
// Once a session is at half TTL, renew it
|
||||
$window = $wgObjectCacheSessionExpiry / 2;
|
||||
$logger = LoggerFactory::getInstance( 'SessionHandler' );
|
||||
|
||||
$now = microtime( true );
|
||||
// Session are only written in object stores when $_SESSION changes,
|
||||
// which also renews the TTL ($wgObjectCacheSessionExpiry). If a user
|
||||
// is active but not causing session data changes, it may suddenly
|
||||
// expire as they view a form, blocking the first submission.
|
||||
// Make a dummy change every so often to avoid this.
|
||||
if ( !isset( $_SESSION['wsExpiresUnix'] ) ) {
|
||||
$_SESSION['wsExpiresUnix'] = $now + $wgObjectCacheSessionExpiry;
|
||||
|
||||
$logger->info( "Set expiry for session " . session_id(), array() );
|
||||
} elseif ( ( $now + $window ) > $_SESSION['wsExpiresUnix'] ) {
|
||||
$_SESSION['wsExpiresUnix'] = $now + $wgObjectCacheSessionExpiry;
|
||||
|
||||
$logger->info( "Renewed session " . session_id(), array() );
|
||||
}
|
||||
}
|
||||
}
|
||||
324
includes/session/CookieSessionProvider.php
Normal file
324
includes/session/CookieSessionProvider.php
Normal file
|
|
@ -0,0 +1,324 @@
|
|||
<?php
|
||||
/**
|
||||
* MediaWiki cookie-based session provider interface
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program; if not, write to the Free Software Foundation, Inc.,
|
||||
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
* http://www.gnu.org/copyleft/gpl.html
|
||||
*
|
||||
* @file
|
||||
* @ingroup Session
|
||||
*/
|
||||
|
||||
namespace MediaWiki\Session;
|
||||
|
||||
use Config;
|
||||
use User;
|
||||
use WebRequest;
|
||||
|
||||
/**
|
||||
* A CookieSessionProvider persists sessions using cookies
|
||||
*
|
||||
* @ingroup Session
|
||||
* @since 1.27
|
||||
*/
|
||||
class CookieSessionProvider extends SessionProvider {
|
||||
|
||||
protected $params = array();
|
||||
protected $cookieOptions = array();
|
||||
|
||||
/**
|
||||
* @param array $params Keys include:
|
||||
* - priority: (required) Priority of the returned sessions
|
||||
* - callUserSetCookiesHook: Whether to call the deprecated hook
|
||||
* - sessionName: Session cookie name. Doesn't honor 'prefix'. Defaults to
|
||||
* $wgSessionName, or $wgCookiePrefix . '_session' if that is unset.
|
||||
* - cookieOptions: Options to pass to WebRequest::setCookie():
|
||||
* - prefix: Cookie prefix, defaults to $wgCookiePrefix
|
||||
* - path: Cookie path, defaults to $wgCookiePath
|
||||
* - domain: Cookie domain, defaults to $wgCookieDomain
|
||||
* - secure: Cookie secure flag, defaults to $wgCookieSecure
|
||||
* - httpOnly: Cookie httpOnly flag, defaults to $wgCookieHttpOnly
|
||||
*/
|
||||
public function __construct( $params = array() ) {
|
||||
parent::__construct();
|
||||
|
||||
$params += array(
|
||||
'cookieOptions' => array(),
|
||||
// @codeCoverageIgnoreStart
|
||||
);
|
||||
// @codeCoverageIgnoreEnd
|
||||
|
||||
if ( !isset( $params['priority'] ) ) {
|
||||
throw new \InvalidArgumentException( __METHOD__ . ': priority must be specified' );
|
||||
}
|
||||
if ( $params['priority'] < SessionInfo::MIN_PRIORITY ||
|
||||
$params['priority'] > SessionInfo::MAX_PRIORITY
|
||||
) {
|
||||
throw new \InvalidArgumentException( __METHOD__ . ': Invalid priority' );
|
||||
}
|
||||
|
||||
if ( !is_array( $params['cookieOptions'] ) ) {
|
||||
throw new \InvalidArgumentException( __METHOD__ . ': cookieOptions must be an array' );
|
||||
}
|
||||
|
||||
$this->priority = $params['priority'];
|
||||
$this->cookieOptions = $params['cookieOptions'];
|
||||
$this->params = $params;
|
||||
unset( $this->params['priority'] );
|
||||
unset( $this->params['cookieOptions'] );
|
||||
}
|
||||
|
||||
public function setConfig( Config $config ) {
|
||||
parent::setConfig( $config );
|
||||
|
||||
// @codeCoverageIgnoreStart
|
||||
$this->params += array(
|
||||
// @codeCoverageIgnoreEnd
|
||||
'callUserSetCookiesHook' => false,
|
||||
'sessionName' =>
|
||||
$config->get( 'SessionName' ) ?: $config->get( 'CookiePrefix' ) . '_session',
|
||||
);
|
||||
|
||||
// @codeCoverageIgnoreStart
|
||||
$this->cookieOptions += array(
|
||||
// @codeCoverageIgnoreEnd
|
||||
'prefix' => $config->get( 'CookiePrefix' ),
|
||||
'path' => $config->get( 'CookiePath' ),
|
||||
'domain' => $config->get( 'CookieDomain' ),
|
||||
'secure' => $config->get( 'CookieSecure' ),
|
||||
'httpOnly' => $config->get( 'CookieHttpOnly' ),
|
||||
);
|
||||
}
|
||||
|
||||
public function provideSessionInfo( WebRequest $request ) {
|
||||
$info = array(
|
||||
'id' => $request->getCookie( $this->params['sessionName'], '' )
|
||||
);
|
||||
if ( !SessionManager::validateSessionId( $info['id'] ) ) {
|
||||
unset( $info['id'] );
|
||||
}
|
||||
|
||||
list( $userId, $userName, $token ) = $this->getUserInfoFromCookies( $request );
|
||||
if ( $userId !== null ) {
|
||||
try {
|
||||
$userInfo = UserInfo::newFromId( $userId );
|
||||
} catch ( \InvalidArgumentException $ex ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Sanity check
|
||||
if ( $userName !== null && $userInfo->getName() !== $userName ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ( $token !== null ) {
|
||||
if ( !hash_equals( $userInfo->getToken(), $token ) ) {
|
||||
return null;
|
||||
}
|
||||
$info['userInfo'] = $userInfo->verified();
|
||||
} elseif ( isset( $info['id'] ) ) { // No point if no session ID
|
||||
$info['userInfo'] = $userInfo;
|
||||
}
|
||||
}
|
||||
|
||||
if ( !$info ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$info += array(
|
||||
'provider' => $this,
|
||||
'persisted' => isset( $info['id'] ),
|
||||
'forceHTTPS' => $request->getCookie( 'forceHTTPS', '', false )
|
||||
);
|
||||
|
||||
return new SessionInfo( $this->priority, $info );
|
||||
}
|
||||
|
||||
public function persistsSessionId() {
|
||||
return true;
|
||||
}
|
||||
|
||||
public function canChangeUser() {
|
||||
return true;
|
||||
}
|
||||
|
||||
public function persistSession( SessionBackend $session, WebRequest $request ) {
|
||||
$response = $request->response();
|
||||
if ( $response->headersSent() ) {
|
||||
// Can't do anything now
|
||||
$this->logger->debug( __METHOD__ . ': Headers already sent' );
|
||||
return;
|
||||
}
|
||||
|
||||
$user = $session->getUser();
|
||||
|
||||
$cookies = $this->cookieDataToExport( $user, $session->shouldRememberUser() );
|
||||
$sessionData = $this->sessionDataToExport( $user );
|
||||
|
||||
// Legacy hook
|
||||
if ( $this->params['callUserSetCookiesHook'] && !$user->isAnon() ) {
|
||||
\Hooks::run( 'UserSetCookies', array( $user, &$sessionData, &$cookies ) );
|
||||
}
|
||||
|
||||
$options = $this->cookieOptions;
|
||||
if ( $session->shouldForceHTTPS() || $user->requiresHTTPS() ) {
|
||||
$response->setCookie( 'forceHTTPS', 'true', $session->shouldRememberUser() ? 0 : null,
|
||||
array( 'prefix' => '', 'secure' => false ) + $options );
|
||||
$options['secure'] = true;
|
||||
}
|
||||
|
||||
$response->setCookie( $this->params['sessionName'], $session->getId(), null,
|
||||
array( 'prefix' => '' ) + $options
|
||||
);
|
||||
|
||||
$extendedCookies = $this->config->get( 'ExtendedLoginCookies' );
|
||||
$extendedExpiry = $this->config->get( 'ExtendedLoginCookieExpiration' );
|
||||
|
||||
foreach ( $cookies as $key => $value ) {
|
||||
if ( $value === false ) {
|
||||
$response->clearCookie( $key, $options );
|
||||
} else {
|
||||
if ( $extendedExpiry !== null && in_array( $key, $extendedCookies ) ) {
|
||||
$expiry = time() + (int)$extendedExpiry;
|
||||
} else {
|
||||
$expiry = 0; // Default cookie expiration
|
||||
}
|
||||
$response->setCookie( $key, (string)$value, $expiry, $options );
|
||||
}
|
||||
}
|
||||
|
||||
$this->setLoggedOutCookie( $session->getLoggedOutTimestamp(), $request );
|
||||
|
||||
if ( $sessionData ) {
|
||||
$session->addData( $sessionData );
|
||||
}
|
||||
}
|
||||
|
||||
public function unpersistSession( WebRequest $request ) {
|
||||
$response = $request->response();
|
||||
if ( $response->headersSent() ) {
|
||||
// Can't do anything now
|
||||
$this->logger->debug( __METHOD__ . ': Headers already sent' );
|
||||
return;
|
||||
}
|
||||
|
||||
$cookies = array(
|
||||
'UserID' => false,
|
||||
'Token' => false,
|
||||
);
|
||||
|
||||
$response->clearCookie(
|
||||
$this->params['sessionName'], array( 'prefix' => '' ) + $this->cookieOptions
|
||||
);
|
||||
|
||||
foreach ( $cookies as $key => $value ) {
|
||||
$response->clearCookie( $key, $this->cookieOptions );
|
||||
}
|
||||
|
||||
$response->clearCookie( 'forceHTTPS',
|
||||
array( 'prefix' => '', 'secure' => false ) + $this->cookieOptions );
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the "logged out" cookie
|
||||
* @param int $loggedOut timestamp
|
||||
* @param WebRequest $request
|
||||
*/
|
||||
protected function setLoggedOutCookie( $loggedOut, WebRequest $request ) {
|
||||
if ( $loggedOut + 86400 > time() &&
|
||||
$loggedOut !== (int)$request->getCookie( 'LoggedOut', $this->cookieOptions['prefix'] )
|
||||
) {
|
||||
$request->response()->setCookie( 'LoggedOut', $loggedOut, $loggedOut + 86400,
|
||||
$this->cookieOptions );
|
||||
}
|
||||
}
|
||||
|
||||
public function getVaryCookies() {
|
||||
return array(
|
||||
// Vary on token and session because those are the real authn
|
||||
// determiners. UserID and UserName don't matter without those.
|
||||
$this->cookieOptions['prefix'] . 'Token',
|
||||
$this->cookieOptions['prefix'] . 'LoggedOut',
|
||||
$this->params['sessionName'],
|
||||
'forceHTTPS',
|
||||
);
|
||||
}
|
||||
|
||||
public function suggestLoginUsername( WebRequest $request ) {
|
||||
$name = $request->getCookie( 'UserName', $this->cookieOptions['prefix'] );
|
||||
if ( $name !== null ) {
|
||||
$name = User::getCanonicalName( $name, 'usable' );
|
||||
}
|
||||
return $name === false ? null : $name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch the user identity from cookies
|
||||
* @return array (int|null $id, string|null $token)
|
||||
*/
|
||||
protected function getUserInfoFromCookies( $request ) {
|
||||
$prefix = $this->cookieOptions['prefix'];
|
||||
return array(
|
||||
$request->getCookie( 'UserID', $prefix ),
|
||||
$request->getCookie( 'UserName', $prefix ),
|
||||
$request->getCookie( 'Token', $prefix ),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the data to store in cookies
|
||||
* @param User $user
|
||||
* @param bool $remember
|
||||
* @return array $cookies Set value false to unset the cookie
|
||||
*/
|
||||
protected function cookieDataToExport( $user, $remember ) {
|
||||
if ( $user->isAnon() ) {
|
||||
return array(
|
||||
'UserID' => false,
|
||||
'Token' => false,
|
||||
);
|
||||
} else {
|
||||
return array(
|
||||
'UserID' => $user->getId(),
|
||||
'UserName' => $user->getName(),
|
||||
'Token' => $remember ? (string)$user->getToken() : false,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return extra data to store in the session
|
||||
* @param User $user
|
||||
* @return array $session
|
||||
*/
|
||||
protected function sessionDataToExport( $user ) {
|
||||
// If we're calling the legacy hook, we should populate $session
|
||||
// like User::setCookies() did.
|
||||
if ( !$user->isAnon() && $this->params['callUserSetCookiesHook'] ) {
|
||||
return array(
|
||||
'wsUserID' => $user->getId(),
|
||||
'wsToken' => $user->getToken(),
|
||||
'wsUserName' => $user->getName(),
|
||||
);
|
||||
}
|
||||
|
||||
return array();
|
||||
}
|
||||
|
||||
public function whyNoSession() {
|
||||
return wfMessage( 'sessionprovider-nocookies' );
|
||||
}
|
||||
|
||||
}
|
||||
153
includes/session/ImmutableSessionProviderWithCookie.php
Normal file
153
includes/session/ImmutableSessionProviderWithCookie.php
Normal file
|
|
@ -0,0 +1,153 @@
|
|||
<?php
|
||||
/**
|
||||
* MediaWiki session provider base class
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program; if not, write to the Free Software Foundation, Inc.,
|
||||
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
* http://www.gnu.org/copyleft/gpl.html
|
||||
*
|
||||
* @file
|
||||
* @ingroup Session
|
||||
*/
|
||||
|
||||
namespace MediaWiki\Session;
|
||||
|
||||
use WebRequest;
|
||||
|
||||
/**
|
||||
* An ImmutableSessionProviderWithCookie doesn't persist the user, but
|
||||
* optionally can use a cookie to support multiple IDs per session.
|
||||
*
|
||||
* As mentioned in the documentation for SessionProvider, many methods that are
|
||||
* technically "cannot persist ID" could be turned into "can persist ID but
|
||||
* not changing User" using a session cookie. This class implements such an
|
||||
* optional session cookie.
|
||||
*
|
||||
* @ingroup Session
|
||||
* @since 1.27
|
||||
*/
|
||||
abstract class ImmutableSessionProviderWithCookie extends SessionProvider {
|
||||
|
||||
/** @var string|null */
|
||||
protected $sessionCookieName = null;
|
||||
protected $sessionCookieOptions = array();
|
||||
|
||||
/**
|
||||
* @param array $params Keys include:
|
||||
* - sessionCookieName: Session cookie name, if multiple sessions per
|
||||
* client are to be supported.
|
||||
* - sessionCookieOptions: Options to pass to WebResponse::setCookie().
|
||||
*/
|
||||
public function __construct( $params = array() ) {
|
||||
parent::__construct();
|
||||
|
||||
if ( isset( $params['sessionCookieName'] ) ) {
|
||||
if ( !is_string( $params['sessionCookieName'] ) ) {
|
||||
throw new \InvalidArgumentException( 'sessionCookieName must be a string' );
|
||||
}
|
||||
$this->sessionCookieName = $params['sessionCookieName'];
|
||||
}
|
||||
if ( isset( $params['sessionCookieOptions'] ) ) {
|
||||
if ( !is_array( $params['sessionCookieOptions'] ) ) {
|
||||
throw new \InvalidArgumentException( 'sessionCookieOptions must be an array' );
|
||||
}
|
||||
$this->sessionCookieOptions = $params['sessionCookieOptions'];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the session ID from the cookie, if any.
|
||||
*
|
||||
* Only call this if $this->sessionCookieName !== null. If
|
||||
* sessionCookieName is null, do some logic (probably involving a call to
|
||||
* $this->hashToSessionId()) to create the single session ID corresponding
|
||||
* to this WebRequest instead of calling this method.
|
||||
*
|
||||
* @param WebRequest $request
|
||||
* @return string|null
|
||||
*/
|
||||
protected function getSessionIdFromCookie( WebRequest $request ) {
|
||||
if ( $this->sessionCookieName === null ) {
|
||||
throw new \BadMethodCallException(
|
||||
__METHOD__ . ' may not be called when $this->sessionCookieName === null'
|
||||
);
|
||||
}
|
||||
|
||||
$prefix = isset( $this->sessionCookieOptions['prefix'] )
|
||||
? $this->sessionCookieOptions['prefix']
|
||||
: $this->config->get( 'CookiePrefix' );
|
||||
$id = $request->getCookie( $this->sessionCookieName, $prefix );
|
||||
return SessionManager::validateSessionId( $id ) ? $id : null;
|
||||
}
|
||||
|
||||
public function persistsSessionId() {
|
||||
return $this->sessionCookieName !== null;
|
||||
}
|
||||
|
||||
public function canChangeUser() {
|
||||
return false;
|
||||
}
|
||||
|
||||
public function persistSession( SessionBackend $session, WebRequest $request ) {
|
||||
if ( $this->sessionCookieName === null ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$response = $request->response();
|
||||
if ( $response->headersSent() ) {
|
||||
// Can't do anything now
|
||||
$this->logger->debug( __METHOD__ . ': Headers already sent' );
|
||||
return;
|
||||
}
|
||||
|
||||
$options = $this->sessionCookieOptions;
|
||||
if ( $session->shouldForceHTTPS() || $session->getUser()->requiresHTTPS() ) {
|
||||
$response->setCookie( 'forceHTTPS', 'true', $session->shouldRememberUser() ? 0 : null,
|
||||
array( 'prefix' => '', 'secure' => false ) + $options );
|
||||
$options['secure'] = true;
|
||||
}
|
||||
|
||||
$response->setCookie( $this->sessionCookieName, $session->getId(), null, $options );
|
||||
}
|
||||
|
||||
public function unpersistSession( WebRequest $request ) {
|
||||
if ( $this->sessionCookieName === null ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$response = $request->response();
|
||||
if ( $response->headersSent() ) {
|
||||
// Can't do anything now
|
||||
$this->logger->debug( __METHOD__ . ': Headers already sent' );
|
||||
return;
|
||||
}
|
||||
|
||||
$response->clearCookie( $this->sessionCookieName, $this->sessionCookieOptions );
|
||||
}
|
||||
|
||||
public function getVaryCookies() {
|
||||
if ( $this->sessionCookieName === null ) {
|
||||
return array();
|
||||
}
|
||||
|
||||
$prefix = isset( $this->sessionCookieOptions['prefix'] )
|
||||
? $this->sessionCookieOptions['prefix']
|
||||
: $this->config->get( 'CookiePrefix' );
|
||||
return array( $prefix . $this->sessionCookieName );
|
||||
}
|
||||
|
||||
public function whyNoSession() {
|
||||
return wfMessage( 'sessionprovider-nocookies' );
|
||||
}
|
||||
}
|
||||
369
includes/session/PHPSessionHandler.php
Normal file
369
includes/session/PHPSessionHandler.php
Normal file
|
|
@ -0,0 +1,369 @@
|
|||
<?php
|
||||
/**
|
||||
* Session storage in object cache.
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program; if not, write to the Free Software Foundation, Inc.,
|
||||
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
* http://www.gnu.org/copyleft/gpl.html
|
||||
*
|
||||
* @file
|
||||
* @ingroup Session
|
||||
*/
|
||||
|
||||
namespace MediaWiki\Session;
|
||||
|
||||
use Psr\Log\LoggerInterface;
|
||||
use BagOStuff;
|
||||
|
||||
/**
|
||||
* Adapter for PHP's session handling
|
||||
* @todo Once we drop support for PHP < 5.4, use SessionHandlerInterface
|
||||
* (should just be a matter of adding "implements SessionHandlerInterface" and
|
||||
* changing the session_set_save_handler() call).
|
||||
* @ingroup Session
|
||||
* @since 1.27
|
||||
*/
|
||||
class PHPSessionHandler {
|
||||
/** @var PHPSessionHandler */
|
||||
protected static $instance = null;
|
||||
|
||||
/** @var bool Whether PHP session handling is enabled */
|
||||
protected $enable = false;
|
||||
protected $warn = true;
|
||||
|
||||
/** @var SessionManager|null */
|
||||
protected $manager;
|
||||
|
||||
/** @var BagOStuff|null */
|
||||
protected $store;
|
||||
|
||||
/** @var LoggerInterface */
|
||||
protected $logger;
|
||||
|
||||
/** @var array Track original session fields for later modification check */
|
||||
protected $sessionFieldCache = array();
|
||||
|
||||
protected function __construct( SessionManager $manager ) {
|
||||
$this->setEnableFlags(
|
||||
\RequestContext::getMain()->getConfig()->get( 'PHPSessionHandling' )
|
||||
);
|
||||
$manager->setupPHPSessionHandler( $this );
|
||||
}
|
||||
|
||||
/**
|
||||
* Set $this->enable and $this->warn
|
||||
*
|
||||
* Separate just because there doesn't seem to be a good way to test it
|
||||
* otherwise.
|
||||
*
|
||||
* @param string $PHPSessionHandling See $wgPHPSessionHandling
|
||||
*/
|
||||
private function setEnableFlags( $PHPSessionHandling ) {
|
||||
switch ( $PHPSessionHandling ) {
|
||||
case 'enable':
|
||||
$this->enable = true;
|
||||
$this->warn = false;
|
||||
break;
|
||||
|
||||
case 'warn':
|
||||
$this->enable = true;
|
||||
$this->warn = true;
|
||||
break;
|
||||
|
||||
case 'disable':
|
||||
$this->enable = false;
|
||||
$this->warn = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test whether the handler is installed
|
||||
* @return bool
|
||||
*/
|
||||
public static function isInstalled() {
|
||||
return (bool)self::$instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Test whether the handler is installed and enabled
|
||||
* @return bool
|
||||
*/
|
||||
public static function isEnabled() {
|
||||
return self::$instance && self::$instance->enable;
|
||||
}
|
||||
|
||||
/**
|
||||
* Install a session handler for the current web request
|
||||
* @param SessionManager $manager
|
||||
*/
|
||||
public static function install( SessionManager $manager ) {
|
||||
if ( self::$instance ) {
|
||||
$manager->setupPHPSessionHandler( self::$instance );
|
||||
return;
|
||||
}
|
||||
|
||||
self::$instance = new self( $manager );
|
||||
|
||||
// Close any auto-started session, before we replace it
|
||||
session_write_close();
|
||||
|
||||
// Tell PHP not to mess with cookies itself
|
||||
ini_set( 'session.use_cookies', 0 );
|
||||
ini_set( 'session.use_trans_sid', 0 );
|
||||
|
||||
// Also set a sane serialization handler
|
||||
\Wikimedia\PhpSessionSerializer::setSerializeHandler();
|
||||
|
||||
session_set_save_handler(
|
||||
array( self::$instance, 'open' ),
|
||||
array( self::$instance, 'close' ),
|
||||
array( self::$instance, 'read' ),
|
||||
array( self::$instance, 'write' ),
|
||||
array( self::$instance, 'destroy' ),
|
||||
array( self::$instance, 'gc' )
|
||||
);
|
||||
|
||||
// It's necessary to register a shutdown function to call session_write_close(),
|
||||
// because by the time the request shutdown function for the session module is
|
||||
// called, other needed objects may have already been destroyed. Shutdown functions
|
||||
// registered this way are called before object destruction.
|
||||
register_shutdown_function( array( self::$instance, 'handleShutdown' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the manager, store, and logger
|
||||
* @private Use self::install().
|
||||
* @param SessionManager $manager
|
||||
* @param BagOStuff $store
|
||||
* @param LoggerInterface $store
|
||||
*/
|
||||
public function setManager(
|
||||
SessionManager $manager, BagOStuff $store, LoggerInterface $logger
|
||||
) {
|
||||
if ( $this->manager !== $manager ) {
|
||||
// Close any existing session before we change stores
|
||||
if ( $this->manager ) {
|
||||
session_write_close();
|
||||
}
|
||||
$this->manager = $manager;
|
||||
$this->store = $store;
|
||||
$this->logger = $logger;
|
||||
\Wikimedia\PhpSessionSerializer::setLogger( $this->logger );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the session (handler)
|
||||
* @private For internal use only
|
||||
* @param string $save_path Path used to store session files (ignored)
|
||||
* @param string $session_name Session name (ignored)
|
||||
* @return bool Success
|
||||
*/
|
||||
public function open( $save_path, $session_name ) {
|
||||
if ( self::$instance !== $this ) {
|
||||
throw new \UnexpectedValueException( __METHOD__ . ': Wrong instance called!' );
|
||||
}
|
||||
if ( !$this->enable ) {
|
||||
throw new \BadMethodCallException( 'Attempt to use PHP session management' );
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Close the session (handler)
|
||||
* @private For internal use only
|
||||
* @return bool Success
|
||||
*/
|
||||
public function close() {
|
||||
if ( self::$instance !== $this ) {
|
||||
throw new \UnexpectedValueException( __METHOD__ . ': Wrong instance called!' );
|
||||
}
|
||||
$this->sessionFieldCache = array();
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Read session data
|
||||
* @private For internal use only
|
||||
* @param string $id Session id
|
||||
* @return string Session data
|
||||
*/
|
||||
public function read( $id ) {
|
||||
if ( self::$instance !== $this ) {
|
||||
throw new \UnexpectedValueException( __METHOD__ . ': Wrong instance called!' );
|
||||
}
|
||||
if ( !$this->enable ) {
|
||||
throw new \BadMethodCallException( 'Attempt to use PHP session management' );
|
||||
}
|
||||
|
||||
$session = $this->manager->getSessionById( $id, true );
|
||||
if ( !$session ) {
|
||||
return '';
|
||||
}
|
||||
$session->persist();
|
||||
|
||||
$data = iterator_to_array( $session );
|
||||
$this->sessionFieldCache[$id] = $data;
|
||||
return (string)\Wikimedia\PhpSessionSerializer::encode( $data );
|
||||
}
|
||||
|
||||
/**
|
||||
* Write session data
|
||||
* @private For internal use only
|
||||
* @param string $id Session id
|
||||
* @param string $dataStr Session data. Not that you should ever call this
|
||||
* directly, but note that this has the same issues with code injection
|
||||
* via user-controlled data as does PHP's unserialize function.
|
||||
* @return bool Success
|
||||
*/
|
||||
public function write( $id, $dataStr ) {
|
||||
if ( self::$instance !== $this ) {
|
||||
throw new \UnexpectedValueException( __METHOD__ . ': Wrong instance called!' );
|
||||
}
|
||||
if ( !$this->enable ) {
|
||||
throw new \BadMethodCallException( 'Attempt to use PHP session management' );
|
||||
}
|
||||
|
||||
$session = $this->manager->getSessionById( $id );
|
||||
|
||||
// First, decode the string PHP handed us
|
||||
$data = \Wikimedia\PhpSessionSerializer::decode( $dataStr );
|
||||
if ( $data === null ) {
|
||||
// @codeCoverageIgnoreStart
|
||||
return false;
|
||||
// @codeCoverageIgnoreEnd
|
||||
}
|
||||
|
||||
// Now merge the data into the Session object.
|
||||
$changed = false;
|
||||
$cache = isset( $this->sessionFieldCache[$id] ) ? $this->sessionFieldCache[$id] : array();
|
||||
foreach ( $data as $key => $value ) {
|
||||
if ( !isset( $cache[$key] ) ) {
|
||||
if ( $session->exists( $key ) ) {
|
||||
// New in both, so ignore and log
|
||||
$this->logger->warning(
|
||||
__METHOD__ . ": Key \"$key\" added in both Session and \$_SESSION!"
|
||||
);
|
||||
} else {
|
||||
// New in $_SESSION, keep it
|
||||
$session->set( $key, $value );
|
||||
$changed = true;
|
||||
}
|
||||
} elseif ( $cache[$key] === $value ) {
|
||||
// Unchanged in $_SESSION, so ignore it
|
||||
} elseif ( !$session->exists( $key ) ) {
|
||||
// Deleted in Session, keep but log
|
||||
$this->logger->warning(
|
||||
__METHOD__ . ": Key \"$key\" deleted in Session and changed in \$_SESSION!"
|
||||
);
|
||||
$session->set( $key, $value );
|
||||
$changed = true;
|
||||
} elseif ( $cache[$key] === $session->get( $key ) ) {
|
||||
// Unchanged in Session, so keep it
|
||||
$session->set( $key, $value );
|
||||
$changed = true;
|
||||
} else {
|
||||
// Changed in both, so ignore and log
|
||||
$this->logger->warning(
|
||||
__METHOD__ . ": Key \"$key\" changed in both Session and \$_SESSION!"
|
||||
);
|
||||
}
|
||||
}
|
||||
// Anything deleted in $_SESSION and unchanged in Session should be deleted too
|
||||
// (but not if $_SESSION can't represent it at all)
|
||||
\Wikimedia\PhpSessionSerializer::setLogger( new \Psr\Log\NullLogger() );
|
||||
foreach ( $cache as $key => $value ) {
|
||||
if ( !isset( $data[$key] ) && $session->exists( $key ) &&
|
||||
\Wikimedia\PhpSessionSerializer::encode( array( $key => true ) )
|
||||
) {
|
||||
if ( $cache[$key] === $session->get( $key ) ) {
|
||||
// Unchanged in Session, delete it
|
||||
$session->remove( $key );
|
||||
$changed = true;
|
||||
} else {
|
||||
// Changed in Session, ignore deletion and log
|
||||
$this->logger->warning(
|
||||
__METHOD__ . ": Key \"$key\" changed in Session and deleted in \$_SESSION!"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
\Wikimedia\PhpSessionSerializer::setLogger( $this->logger );
|
||||
|
||||
// Save and update cache if anything changed
|
||||
if ( $changed ) {
|
||||
if ( $this->warn ) {
|
||||
wfDeprecated( '$_SESSION', '1.27' );
|
||||
$this->logger->warning( 'Something wrote to $_SESSION!' );
|
||||
}
|
||||
|
||||
$session->save();
|
||||
$this->sessionFieldCache[$id] = iterator_to_array( $session );
|
||||
}
|
||||
|
||||
$session->persist();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Destroy a session
|
||||
* @private For internal use only
|
||||
* @param string $id Session id
|
||||
* @return bool Success
|
||||
*/
|
||||
public function destroy( $id ) {
|
||||
if ( self::$instance !== $this ) {
|
||||
throw new \UnexpectedValueException( __METHOD__ . ': Wrong instance called!' );
|
||||
}
|
||||
if ( !$this->enable ) {
|
||||
throw new \BadMethodCallException( 'Attempt to use PHP session management' );
|
||||
}
|
||||
$session = $this->manager->getSessionById( $id, true );
|
||||
if ( $session ) {
|
||||
$session->clear();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute garbage collection.
|
||||
* @private For internal use only
|
||||
* @param int $maxlifetime Maximum session life time (ignored)
|
||||
* @return bool Success
|
||||
*/
|
||||
public function gc( $maxlifetime ) {
|
||||
if ( self::$instance !== $this ) {
|
||||
throw new \UnexpectedValueException( __METHOD__ . ': Wrong instance called!' );
|
||||
}
|
||||
$before = date( 'YmdHis', time() );
|
||||
$this->store->deleteObjectsExpiringBefore( $before );
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Shutdown function.
|
||||
*
|
||||
* See the comment inside self::install for rationale.
|
||||
* @codeCoverageIgnore
|
||||
* @private For internal use only
|
||||
*/
|
||||
public function handleShutdown() {
|
||||
if ( $this->enable ) {
|
||||
session_write_close();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
364
includes/session/Session.php
Normal file
364
includes/session/Session.php
Normal file
|
|
@ -0,0 +1,364 @@
|
|||
<?php
|
||||
/**
|
||||
* MediaWiki session
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program; if not, write to the Free Software Foundation, Inc.,
|
||||
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
* http://www.gnu.org/copyleft/gpl.html
|
||||
*
|
||||
* @file
|
||||
* @ingroup Session
|
||||
*/
|
||||
|
||||
namespace MediaWiki\Session;
|
||||
|
||||
use User;
|
||||
use WebRequest;
|
||||
|
||||
/**
|
||||
* Manages data for an an authenticated session
|
||||
*
|
||||
* A Session represents the fact that the current HTTP request is part of a
|
||||
* session. There are two broad types of Sessions, based on whether they
|
||||
* return true or false from self::canSetUser():
|
||||
* * When true (mutable), the Session identifies multiple requests as part of
|
||||
* a session generically, with no tie to a particular user.
|
||||
* * When false (immutable), the Session identifies multiple requests as part
|
||||
* of a session by identifying and authenticating the request itself as
|
||||
* belonging to a particular user.
|
||||
*
|
||||
* The Session object also serves as a replacement for PHP's $_SESSION,
|
||||
* managing access to per-session data.
|
||||
*
|
||||
* @todo Once we drop support for PHP 5.3.3, implementing ArrayAccess would be nice.
|
||||
* @ingroup Session
|
||||
* @since 1.27
|
||||
*/
|
||||
final class Session implements \Countable, \Iterator {
|
||||
/** @var SessionBackend Session backend */
|
||||
private $backend;
|
||||
|
||||
/** @var int Session index */
|
||||
private $index;
|
||||
|
||||
/**
|
||||
* @param SessionBackend $backend
|
||||
* @param int $index
|
||||
*/
|
||||
public function __construct( SessionBackend $backend, $index ) {
|
||||
$this->backend = $backend;
|
||||
$this->index = $index;
|
||||
}
|
||||
|
||||
public function __destruct() {
|
||||
$this->backend->deregisterSession( $this->index );
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the session ID
|
||||
* @return string
|
||||
*/
|
||||
public function getId() {
|
||||
return $this->backend->getId();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the SessionId object
|
||||
* @private For internal use by WebRequest
|
||||
* @return SessionId
|
||||
*/
|
||||
public function getSessionId() {
|
||||
return $this->backend->getSessionId();
|
||||
}
|
||||
|
||||
/**
|
||||
* Changes the session ID
|
||||
* @return string New ID (might be the same as the old)
|
||||
*/
|
||||
public function resetId() {
|
||||
return $this->backend->resetId();
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch the SessionProvider for this session
|
||||
* @return SessionProviderInterface
|
||||
*/
|
||||
public function getProvider() {
|
||||
return $this->backend->getProvider();
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicate whether this session is persisted across requests
|
||||
*
|
||||
* For example, if cookies are set.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isPersistent() {
|
||||
return $this->backend->isPersistent();
|
||||
}
|
||||
|
||||
/**
|
||||
* Make this session persisted across requests
|
||||
*
|
||||
* If the session is already persistent, equivalent to calling
|
||||
* $this->renew().
|
||||
*/
|
||||
public function persist() {
|
||||
$this->backend->persist();
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicate whether the user should be remembered independently of the
|
||||
* session ID.
|
||||
* @return bool
|
||||
*/
|
||||
public function shouldRememberUser() {
|
||||
return $this->backend->shouldRememberUser();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set whether the user should be remembered independently of the session
|
||||
* ID.
|
||||
* @param bool $remember
|
||||
*/
|
||||
public function setRememberUser( $remember ) {
|
||||
$this->backend->setRememberUser( $remember );
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the request associated with this session
|
||||
* @return WebRequest
|
||||
*/
|
||||
public function getRequest() {
|
||||
return $this->backend->getRequest( $this->index );
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the authenticated user for this session
|
||||
* @return User
|
||||
*/
|
||||
public function getUser() {
|
||||
return $this->backend->getUser();
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicate whether the session user info can be changed
|
||||
* @return bool
|
||||
*/
|
||||
public function canSetUser() {
|
||||
return $this->backend->canSetUser();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a new user for this session
|
||||
* @note This should only be called when the user has been authenticated
|
||||
* @param User $user User to set on the session.
|
||||
* This may become a "UserValue" in the future, or User may be refactored
|
||||
* into such.
|
||||
*/
|
||||
public function setUser( $user ) {
|
||||
$this->backend->setUser( $user );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a suggested username for the login form
|
||||
* @return string|null
|
||||
*/
|
||||
public function suggestLoginUsername() {
|
||||
return $this->backend->suggestLoginUsername( $this->index );
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether HTTPS should be forced
|
||||
* @return bool
|
||||
*/
|
||||
public function shouldForceHTTPS() {
|
||||
return $this->backend->shouldForceHTTPS();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set whether HTTPS should be forced
|
||||
* @param bool $force
|
||||
*/
|
||||
public function setForceHTTPS( $force ) {
|
||||
$this->backend->setForceHTTPS( $force );
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch the "logged out" timestamp
|
||||
* @return int
|
||||
*/
|
||||
public function getLoggedOutTimestamp() {
|
||||
return $this->backend->getLoggedOutTimestamp();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the "logged out" timestamp
|
||||
* @param int $ts
|
||||
*/
|
||||
public function setLoggedOutTimestamp( $ts ) {
|
||||
$this->backend->setLoggedOutTimestamp( $ts );
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch provider metadata
|
||||
* @protected For use by SessionProvider subclasses only
|
||||
* @return mixed
|
||||
*/
|
||||
public function getProviderMetadata() {
|
||||
return $this->backend->getProviderMetadata();
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete all session data and clear the user (if possible)
|
||||
*/
|
||||
public function clear() {
|
||||
$data = &$this->backend->getData();
|
||||
if ( $data ) {
|
||||
$data = array();
|
||||
$this->backend->dirty();
|
||||
}
|
||||
if ( $this->backend->canSetUser() ) {
|
||||
$this->backend->setUser( new User );
|
||||
}
|
||||
$this->backend->save();
|
||||
}
|
||||
|
||||
/**
|
||||
* Renew the session
|
||||
*
|
||||
* Resets the TTL in the backend store if the session is near expiring, and
|
||||
* re-persists the session to any active WebRequests if persistent.
|
||||
*/
|
||||
public function renew() {
|
||||
$this->backend->renew();
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch a copy of this session attached to an alternative WebRequest
|
||||
*
|
||||
* Actions on the copy will affect this session too, and vice versa.
|
||||
*
|
||||
* @param WebRequest $request Any existing session associated with this
|
||||
* WebRequest object will be overwritten.
|
||||
* @return Session
|
||||
*/
|
||||
public function sessionWithRequest( WebRequest $request ) {
|
||||
$request->setSessionId( $this->backend->getSessionId() );
|
||||
return $this->backend->getSession( $request );
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch a value from the session
|
||||
* @param string|int $key
|
||||
* @param mixed $default
|
||||
* @return mixed
|
||||
*/
|
||||
public function get( $key, $default = null ) {
|
||||
$data = &$this->backend->getData();
|
||||
return array_key_exists( $key, $data ) ? $data[$key] : $default;
|
||||
}
|
||||
|
||||
/**
|
||||
* Test if a value exists in the session
|
||||
* @param string|int $key
|
||||
* @return bool
|
||||
*/
|
||||
public function exists( $key ) {
|
||||
$data = &$this->backend->getData();
|
||||
return array_key_exists( $key, $data );
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a value in the session
|
||||
* @param string|int $key
|
||||
* @param mixed $value
|
||||
*/
|
||||
public function set( $key, $value ) {
|
||||
$data = &$this->backend->getData();
|
||||
if ( !array_key_exists( $key, $data ) || $data[$key] !== $value ) {
|
||||
$data[$key] = $value;
|
||||
$this->backend->dirty();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a value from the session
|
||||
* @param string|int $key
|
||||
*/
|
||||
public function remove( $key ) {
|
||||
$data = &$this->backend->getData();
|
||||
if ( array_key_exists( $key, $data ) ) {
|
||||
unset( $data[$key] );
|
||||
$this->backend->dirty();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delay automatic saving while multiple updates are being made
|
||||
*
|
||||
* Calls to save() or clear() will not be delayed.
|
||||
*
|
||||
* @return \ScopedCallback When this goes out of scope, a save will be triggered
|
||||
*/
|
||||
public function delaySave() {
|
||||
return $this->backend->delaySave();
|
||||
}
|
||||
|
||||
/**
|
||||
* Save the session
|
||||
*/
|
||||
public function save() {
|
||||
$this->backend->save();
|
||||
}
|
||||
|
||||
/**
|
||||
* @name Interface methods
|
||||
* @{
|
||||
*/
|
||||
|
||||
public function count() {
|
||||
$data = &$this->backend->getData();
|
||||
return count( $data );
|
||||
}
|
||||
|
||||
public function current() {
|
||||
$data = &$this->backend->getData();
|
||||
return current( $data );
|
||||
}
|
||||
|
||||
public function key() {
|
||||
$data = &$this->backend->getData();
|
||||
return key( $data );
|
||||
}
|
||||
|
||||
public function next() {
|
||||
$data = &$this->backend->getData();
|
||||
next( $data );
|
||||
}
|
||||
|
||||
public function rewind() {
|
||||
$data = &$this->backend->getData();
|
||||
reset( $data );
|
||||
}
|
||||
|
||||
public function valid() {
|
||||
$data = &$this->backend->getData();
|
||||
return key( $data ) !== null;
|
||||
}
|
||||
|
||||
/**@}*/
|
||||
|
||||
}
|
||||
624
includes/session/SessionBackend.php
Normal file
624
includes/session/SessionBackend.php
Normal file
|
|
@ -0,0 +1,624 @@
|
|||
<?php
|
||||
/**
|
||||
* MediaWiki session backend
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program; if not, write to the Free Software Foundation, Inc.,
|
||||
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
* http://www.gnu.org/copyleft/gpl.html
|
||||
*
|
||||
* @file
|
||||
* @ingroup Session
|
||||
*/
|
||||
|
||||
namespace MediaWiki\Session;
|
||||
|
||||
use BagOStuff;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use User;
|
||||
use WebRequest;
|
||||
|
||||
/**
|
||||
* This is the actual workhorse for Session.
|
||||
*
|
||||
* Most code does not need to use this class, you want \\MediaWiki\\Session\\Session.
|
||||
* The exceptions are SessionProviders and SessionMetadata hook functions,
|
||||
* which get an instance of this class rather than Session.
|
||||
*
|
||||
* The reasons for this split are:
|
||||
* 1. A session can be attached to multiple requests, but we want the Session
|
||||
* object to have some features that correspond to just one of those
|
||||
* requests.
|
||||
* 2. We want reasonable garbage collection behavior, but we also want the
|
||||
* SessionManager to hold a reference to every active session so it can be
|
||||
* saved when the request ends.
|
||||
*
|
||||
* @ingroup Session
|
||||
* @since 1.27
|
||||
*/
|
||||
final class SessionBackend {
|
||||
/** @var SessionId */
|
||||
private $id;
|
||||
|
||||
private $persist = false;
|
||||
private $remember = false;
|
||||
private $forceHTTPS = false;
|
||||
|
||||
/** @var array|null */
|
||||
private $data = null;
|
||||
|
||||
private $forcePersist = false;
|
||||
private $metaDirty = false;
|
||||
private $dataDirty = false;
|
||||
|
||||
/** @var string Used to detect subarray modifications */
|
||||
private $dataHash = null;
|
||||
|
||||
/** @var BagOStuff */
|
||||
private $store;
|
||||
|
||||
/** @var LoggerInterface */
|
||||
private $logger;
|
||||
|
||||
/** @var int */
|
||||
private $lifetime;
|
||||
|
||||
/** @var User */
|
||||
private $user;
|
||||
|
||||
private $curIndex = 0;
|
||||
|
||||
/** @var WebRequest[] Session requests */
|
||||
private $requests = array();
|
||||
|
||||
/** @var SessionProvider provider */
|
||||
private $provider;
|
||||
|
||||
/** @var array|null provider-specified metadata */
|
||||
private $providerMetadata = null;
|
||||
|
||||
private $expires = 0;
|
||||
private $loggedOut = 0;
|
||||
private $delaySave = 0;
|
||||
|
||||
private $usePhpSessionHandling = true;
|
||||
private $checkPHPSessionRecursionGuard = false;
|
||||
|
||||
/**
|
||||
* @param SessionId $id Session ID object
|
||||
* @param SessionInfo $info Session info to populate from
|
||||
* @param BagOStuff $store Backend data store
|
||||
* @param LoggerInterface $logger
|
||||
* @param int $lifetime Session data lifetime in seconds
|
||||
*/
|
||||
public function __construct(
|
||||
SessionId $id, SessionInfo $info, BagOStuff $store, LoggerInterface $logger, $lifetime
|
||||
) {
|
||||
$phpSessionHandling = \RequestContext::getMain()->getConfig()->get( 'PHPSessionHandling' );
|
||||
$this->usePhpSessionHandling = $phpSessionHandling !== 'disable';
|
||||
|
||||
if ( $info->getUserInfo() && !$info->getUserInfo()->isVerified() ) {
|
||||
throw new \InvalidArgumentException(
|
||||
"Refusing to create session for unverified user {$info->getUserInfo()}"
|
||||
);
|
||||
}
|
||||
if ( $info->getProvider() === null ) {
|
||||
throw new \InvalidArgumentException( 'Cannot create session without a provider' );
|
||||
}
|
||||
if ( $info->getId() !== $id->getId() ) {
|
||||
throw new \InvalidArgumentException( 'SessionId and SessionInfo don\'t match' );
|
||||
}
|
||||
|
||||
$this->id = $id;
|
||||
$this->user = $info->getUserInfo() ? $info->getUserInfo()->getUser() : new User;
|
||||
$this->store = $store;
|
||||
$this->logger = $logger;
|
||||
$this->lifetime = $lifetime;
|
||||
$this->provider = $info->getProvider();
|
||||
$this->persist = $info->wasPersisted();
|
||||
$this->remember = $info->wasRemembered();
|
||||
$this->forceHTTPS = $info->forceHTTPS();
|
||||
$this->providerMetadata = $info->getProviderMetadata();
|
||||
|
||||
$blob = $store->get( wfMemcKey( 'MWSession', (string)$this->id ) );
|
||||
if ( !is_array( $blob ) ||
|
||||
!isset( $blob['metadata'] ) || !is_array( $blob['metadata'] ) ||
|
||||
!isset( $blob['data'] ) || !is_array( $blob['data'] )
|
||||
) {
|
||||
$this->data = array();
|
||||
$this->dataDirty = true;
|
||||
$this->metaDirty = true;
|
||||
$this->logger->debug( "SessionBackend $this->id is unsaved, marking dirty in constructor" );
|
||||
} else {
|
||||
$this->data = $blob['data'];
|
||||
if ( isset( $blob['metadata']['loggedOut'] ) ) {
|
||||
$this->loggedOut = (int)$blob['metadata']['loggedOut'];
|
||||
}
|
||||
if ( isset( $blob['metadata']['expires'] ) ) {
|
||||
$this->expires = (int)$blob['metadata']['expires'];
|
||||
} else {
|
||||
$this->metaDirty = true;
|
||||
$this->logger->debug(
|
||||
"SessionBackend $this->id metadata dirty due to missing expiration timestamp"
|
||||
);
|
||||
}
|
||||
}
|
||||
$this->dataHash = md5( serialize( $this->data ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a new Session for this backend
|
||||
* @param WebRequest $request
|
||||
* @return Session
|
||||
*/
|
||||
public function getSession( WebRequest $request ) {
|
||||
$index = ++$this->curIndex;
|
||||
$this->requests[$index] = $request;
|
||||
$session = new Session( $this, $index );
|
||||
return $session;
|
||||
}
|
||||
|
||||
/**
|
||||
* Deregister a Session
|
||||
* @private For use by \\MediaWiki\\Session\\Session::__destruct() only
|
||||
* @param int $index
|
||||
*/
|
||||
public function deregisterSession( $index ) {
|
||||
unset( $this->requests[$index] );
|
||||
if ( !count( $this->requests ) ) {
|
||||
$this->save( true );
|
||||
$this->provider->getManager()->deregisterSessionBackend( $this );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the session ID.
|
||||
* @return string
|
||||
*/
|
||||
public function getId() {
|
||||
return (string)$this->id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch the SessionId object
|
||||
* @private For internal use by WebRequest
|
||||
* @return SessionId
|
||||
*/
|
||||
public function getSessionId() {
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Changes the session ID
|
||||
* @return string New ID (might be the same as the old)
|
||||
*/
|
||||
public function resetId() {
|
||||
if ( $this->provider->persistsSessionId() ) {
|
||||
$oldId = (string)$this->id;
|
||||
$restart = $this->usePhpSessionHandling && $oldId === session_id() &&
|
||||
PHPSessionHandler::isEnabled();
|
||||
|
||||
if ( $restart ) {
|
||||
// If this session is the one behind PHP's $_SESSION, we need
|
||||
// to close then reopen it.
|
||||
session_write_close();
|
||||
}
|
||||
|
||||
$this->provider->getManager()->changeBackendId( $this );
|
||||
$this->provider->sessionIdWasReset( $this, $oldId );
|
||||
$this->metaDirty = true;
|
||||
$this->logger->debug(
|
||||
"SessionBackend $this->id metadata dirty due to ID reset (formerly $oldId)"
|
||||
);
|
||||
|
||||
if ( $restart ) {
|
||||
session_id( (string)$this->id );
|
||||
\MediaWiki\quietCall( 'session_start' );
|
||||
}
|
||||
|
||||
$this->autosave();
|
||||
|
||||
// Delete the data for the old session ID now
|
||||
$this->store->delete( wfMemcKey( 'MWSession', $oldId ) );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch the SessionProvider for this session
|
||||
* @return SessionProviderInterface
|
||||
*/
|
||||
public function getProvider() {
|
||||
return $this->provider;
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicate whether this session is persisted across requests
|
||||
*
|
||||
* For example, if cookies are set.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isPersistent() {
|
||||
return $this->persist;
|
||||
}
|
||||
|
||||
/**
|
||||
* Make this session persisted across requests
|
||||
*
|
||||
* If the session is already persistent, equivalent to calling
|
||||
* $this->renew().
|
||||
*/
|
||||
public function persist() {
|
||||
if ( !$this->persist ) {
|
||||
$this->persist = true;
|
||||
$this->forcePersist = true;
|
||||
$this->logger->debug( "SessionBackend $this->id force-persist due to persist()" );
|
||||
$this->autosave();
|
||||
} else {
|
||||
$this->renew();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicate whether the user should be remembered independently of the
|
||||
* session ID.
|
||||
* @return bool
|
||||
*/
|
||||
public function shouldRememberUser() {
|
||||
return $this->remember;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set whether the user should be remembered independently of the session
|
||||
* ID.
|
||||
* @param bool $remember
|
||||
*/
|
||||
public function setRememberUser( $remember ) {
|
||||
if ( $this->remember !== (bool)$remember ) {
|
||||
$this->remember = (bool)$remember;
|
||||
$this->metaDirty = true;
|
||||
$this->logger->debug( "SessionBackend $this->id metadata dirty due to remember-user change" );
|
||||
$this->autosave();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the request associated with a Session
|
||||
* @param int $index Session index
|
||||
* @return WebRequest
|
||||
*/
|
||||
public function getRequest( $index ) {
|
||||
if ( !isset( $this->requests[$index] ) ) {
|
||||
throw new \InvalidArgumentException( 'Invalid session index' );
|
||||
}
|
||||
return $this->requests[$index];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the authenticated user for this session
|
||||
* @return User
|
||||
*/
|
||||
public function getUser() {
|
||||
return $this->user;
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicate whether the session user info can be changed
|
||||
* @return bool
|
||||
*/
|
||||
public function canSetUser() {
|
||||
return $this->provider->canChangeUser();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a new user for this session
|
||||
* @note This should only be called when the user has been authenticated via a login process
|
||||
* @param User $user User to set on the session.
|
||||
* This may become a "UserValue" in the future, or User may be refactored
|
||||
* into such.
|
||||
*/
|
||||
public function setUser( $user ) {
|
||||
if ( !$this->canSetUser() ) {
|
||||
throw new \BadMethodCallException(
|
||||
'Cannot set user on this session; check $session->canSetUser() first'
|
||||
);
|
||||
}
|
||||
|
||||
$this->user = $user;
|
||||
$this->metaDirty = true;
|
||||
$this->logger->debug( "SessionBackend $this->id metadata dirty due to user change" );
|
||||
$this->autosave();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a suggested username for the login form
|
||||
* @param int $index Session index
|
||||
* @return string|null
|
||||
*/
|
||||
public function suggestLoginUsername( $index ) {
|
||||
if ( !isset( $this->requests[$index] ) ) {
|
||||
throw new \InvalidArgumentException( 'Invalid session index' );
|
||||
}
|
||||
return $this->provider->suggestLoginUsername( $this->requests[$index] );
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether HTTPS should be forced
|
||||
* @return bool
|
||||
*/
|
||||
public function shouldForceHTTPS() {
|
||||
return $this->forceHTTPS;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set whether HTTPS should be forced
|
||||
* @param bool $force
|
||||
*/
|
||||
public function setForceHTTPS( $force ) {
|
||||
if ( $this->forceHTTPS !== (bool)$force ) {
|
||||
$this->forceHTTPS = (bool)$force;
|
||||
$this->metaDirty = true;
|
||||
$this->logger->debug( "SessionBackend $this->id metadata dirty due to force-HTTPS change" );
|
||||
$this->autosave();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch the "logged out" timestamp
|
||||
* @return int
|
||||
*/
|
||||
public function getLoggedOutTimestamp() {
|
||||
return $this->loggedOut;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the "logged out" timestamp
|
||||
* @param int $ts
|
||||
*/
|
||||
public function setLoggedOutTimestamp( $ts = null ) {
|
||||
$ts = (int)$ts;
|
||||
if ( $this->loggedOut !== $ts ) {
|
||||
$this->loggedOut = $ts;
|
||||
$this->metaDirty = true;
|
||||
$this->logger->debug(
|
||||
"SessionBackend $this->id metadata dirty due to logged-out-timestamp change"
|
||||
);
|
||||
$this->autosave();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch provider metadata
|
||||
* @protected For use by SessionProvider subclasses only
|
||||
* @return mixed
|
||||
*/
|
||||
public function getProviderMetadata() {
|
||||
return $this->providerMetadata;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch the session data array
|
||||
*
|
||||
* Note the caller is responsible for calling $this->dirty() if anything in
|
||||
* the array is changed.
|
||||
*
|
||||
* @private For use by \\MediaWiki\\Session\\Session only.
|
||||
* @return array
|
||||
*/
|
||||
public function &getData() {
|
||||
return $this->data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add data to the session.
|
||||
*
|
||||
* Overwrites any existing data under the same keys.
|
||||
*
|
||||
* @param array $newData Key-value pairs to add to the session
|
||||
*/
|
||||
public function addData( array $newData ) {
|
||||
$data = &$this->getData();
|
||||
foreach ( $newData as $key => $value ) {
|
||||
if ( !array_key_exists( $key, $data ) || $data[$key] !== $value ) {
|
||||
$data[$key] = $value;
|
||||
$this->dataDirty = true;
|
||||
$this->logger->debug(
|
||||
"SessionBackend $this->id data dirty due to addData(): " . wfGetAllCallers( 5 )
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark data as dirty
|
||||
* @private For use by \\MediaWiki\\Session\\Session only.
|
||||
*/
|
||||
public function dirty() {
|
||||
$this->dataDirty = true;
|
||||
$this->logger->debug(
|
||||
"SessionBackend $this->id data dirty due to dirty(): " . wfGetAllCallers( 5 )
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Renew the session by resaving everything
|
||||
*
|
||||
* Resets the TTL in the backend store if the session is near expiring, and
|
||||
* re-persists the session to any active WebRequests if persistent.
|
||||
*/
|
||||
public function renew() {
|
||||
if ( time() + $this->lifetime / 2 > $this->expires ) {
|
||||
$this->metaDirty = true;
|
||||
$this->logger->debug(
|
||||
"SessionBackend $this->id metadata dirty for renew(): " . wfGetAllCallers( 5 )
|
||||
);
|
||||
if ( $this->persist ) {
|
||||
$this->forcePersist = true;
|
||||
$this->logger->debug(
|
||||
"SessionBackend $this->id force-persist for renew(): " . wfGetAllCallers( 5 )
|
||||
);
|
||||
}
|
||||
}
|
||||
$this->autosave();
|
||||
}
|
||||
|
||||
/**
|
||||
* Delay automatic saving while multiple updates are being made
|
||||
*
|
||||
* Calls to save() will not be delayed.
|
||||
*
|
||||
* @return \ScopedCallback When this goes out of scope, a save will be triggered
|
||||
*/
|
||||
public function delaySave() {
|
||||
$that = $this;
|
||||
$this->delaySave++;
|
||||
$ref = &$this->delaySave;
|
||||
return new \ScopedCallback( function () use ( $that, &$ref ) {
|
||||
if ( --$ref <= 0 ) {
|
||||
$ref = 0;
|
||||
$that->save();
|
||||
}
|
||||
} );
|
||||
}
|
||||
|
||||
/**
|
||||
* Save and persist session data, unless delayed
|
||||
*/
|
||||
private function autosave() {
|
||||
if ( $this->delaySave <= 0 ) {
|
||||
$this->save();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Save and persist session data
|
||||
* @param bool $closing Whether the session is being closed
|
||||
*/
|
||||
public function save( $closing = false ) {
|
||||
if ( $this->provider->getManager()->isUserSessionPrevented( $this->user->getName() ) ) {
|
||||
$this->logger->debug(
|
||||
"SessionBackend $this->id not saving, " .
|
||||
"user {$this->user} was passed to SessionManager::preventSessionsForUser"
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Ensure the user has a token
|
||||
// @codeCoverageIgnoreStart
|
||||
$anon = $this->user->isAnon();
|
||||
if ( !$anon && !$this->user->getToken() ) {
|
||||
$this->logger->debug(
|
||||
"SessionBackend $this->id creating token for user {$this->user} on save"
|
||||
);
|
||||
$this->user->setToken();
|
||||
if ( !wfReadOnly() ) {
|
||||
$this->user->saveSettings();
|
||||
}
|
||||
$this->metaDirty = true;
|
||||
}
|
||||
// @codeCoverageIgnoreEnd
|
||||
|
||||
if ( !$this->metaDirty && !$this->dataDirty &&
|
||||
$this->dataHash !== md5( serialize( $this->data ) )
|
||||
) {
|
||||
$this->logger->debug( "SessionBackend $this->id data dirty due to hash mismatch, " .
|
||||
"$this->dataHash !== " . md5( serialize( $this->data ) ) );
|
||||
$this->dataDirty = true;
|
||||
}
|
||||
|
||||
if ( !$this->metaDirty && !$this->dataDirty && !$this->forcePersist ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->logger->debug( "SessionBackend $this->id save: " .
|
||||
'dataDirty=' . (int)$this->dataDirty . ' ' .
|
||||
'metaDirty=' . (int)$this->metaDirty . ' ' .
|
||||
'forcePersist=' . (int)$this->forcePersist
|
||||
);
|
||||
|
||||
// Persist to the provider, if flagged
|
||||
if ( $this->persist && ( $this->metaDirty || $this->forcePersist ) ) {
|
||||
foreach ( $this->requests as $request ) {
|
||||
$request->setSessionId( $this->getSessionId() );
|
||||
$this->provider->persistSession( $this, $request );
|
||||
}
|
||||
if ( !$closing ) {
|
||||
$this->checkPHPSession();
|
||||
}
|
||||
}
|
||||
|
||||
$this->forcePersist = false;
|
||||
|
||||
if ( !$this->metaDirty && !$this->dataDirty ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Save session data to store, if necessary
|
||||
$metadata = $origMetadata = array(
|
||||
'provider' => (string)$this->provider,
|
||||
'providerMetadata' => $this->providerMetadata,
|
||||
'userId' => $anon ? 0 : $this->user->getId(),
|
||||
'userName' => $anon ? null : $this->user->getName(),
|
||||
'userToken' => $anon ? null : $this->user->getToken(),
|
||||
'remember' => !$anon && $this->remember,
|
||||
'forceHTTPS' => $this->forceHTTPS,
|
||||
'expires' => time() + $this->lifetime,
|
||||
'loggedOut' => $this->loggedOut,
|
||||
);
|
||||
|
||||
\Hooks::run( 'SessionMetadata', array( $this, &$metadata, $this->requests ) );
|
||||
|
||||
foreach ( $origMetadata as $k => $v ) {
|
||||
if ( $metadata[$k] !== $v ) {
|
||||
throw new \UnexpectedValueException( "SessionMetadata hook changed metadata key \"$k\"" );
|
||||
}
|
||||
}
|
||||
|
||||
$this->store->set(
|
||||
wfMemcKey( 'MWSession', (string)$this->id ),
|
||||
array(
|
||||
'data' => $this->data,
|
||||
'metadata' => $metadata,
|
||||
),
|
||||
$metadata['expires']
|
||||
);
|
||||
|
||||
$this->metaDirty = false;
|
||||
$this->dataDirty = false;
|
||||
$this->dataHash = md5( serialize( $this->data ) );
|
||||
$this->expires = $metadata['expires'];
|
||||
}
|
||||
|
||||
/**
|
||||
* For backwards compatibility, open the PHP session when the global
|
||||
* session is persisted
|
||||
*/
|
||||
private function checkPHPSession() {
|
||||
if ( !$this->checkPHPSessionRecursionGuard ) {
|
||||
$this->checkPHPSessionRecursionGuard = true;
|
||||
$ref = &$this->checkPHPSessionRecursionGuard;
|
||||
$reset = new \ScopedCallback( function () use ( &$ref ) {
|
||||
$ref = false;
|
||||
} );
|
||||
|
||||
if ( $this->usePhpSessionHandling && session_id() === '' && PHPSessionHandler::isEnabled() &&
|
||||
SessionManager::getGlobalSession()->getId() === (string)$this->id
|
||||
) {
|
||||
$this->logger->debug( "SessionBackend $this->id: Taking over PHP session" );
|
||||
session_id( (string)$this->id );
|
||||
\MediaWiki\quietCall( 'session_start' );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
70
includes/session/SessionId.php
Normal file
70
includes/session/SessionId.php
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
<?php
|
||||
/**
|
||||
* MediaWiki session ID holder
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program; if not, write to the Free Software Foundation, Inc.,
|
||||
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
* http://www.gnu.org/copyleft/gpl.html
|
||||
*
|
||||
* @file
|
||||
* @ingroup Session
|
||||
*/
|
||||
|
||||
namespace MediaWiki\Session;
|
||||
|
||||
/**
|
||||
* Value object holding the session ID in a manner that can be globally
|
||||
* updated.
|
||||
*
|
||||
* This class exists because we want WebRequest to refer to the session, but it
|
||||
* can't hold the Session itself due to issues with circular references and it
|
||||
* can't just hold the ID as a string because we need to be able to update the
|
||||
* ID when SessionBackend::resetId() is called.
|
||||
*
|
||||
* @ingroup Session
|
||||
* @since 1.27
|
||||
*/
|
||||
final class SessionId {
|
||||
/** @var string */
|
||||
private $id;
|
||||
|
||||
/**
|
||||
* @param string $id
|
||||
*/
|
||||
public function __construct( $id ) {
|
||||
$this->id = $id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the ID
|
||||
* @return string
|
||||
*/
|
||||
public function getId() {
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the ID
|
||||
* @private For use by \\MediaWiki\\Session\\SessionManager only
|
||||
* @param string $id
|
||||
*/
|
||||
public function setId( $id ) {
|
||||
$this->id = $id;
|
||||
}
|
||||
|
||||
public function __toString() {
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
}
|
||||
270
includes/session/SessionInfo.php
Normal file
270
includes/session/SessionInfo.php
Normal file
|
|
@ -0,0 +1,270 @@
|
|||
<?php
|
||||
/**
|
||||
* MediaWiki session info
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program; if not, write to the Free Software Foundation, Inc.,
|
||||
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
* http://www.gnu.org/copyleft/gpl.html
|
||||
*
|
||||
* @file
|
||||
* @ingroup Session
|
||||
*/
|
||||
|
||||
namespace MediaWiki\Session;
|
||||
|
||||
use Psr\Log\LoggerInterface;
|
||||
use BagOStuff;
|
||||
use WebRequest;
|
||||
|
||||
/**
|
||||
* Value object returned by SessionProvider
|
||||
*
|
||||
* This holds the data necessary to construct a Session.
|
||||
*
|
||||
* @ingroup Session
|
||||
* @since 1.27
|
||||
*/
|
||||
class SessionInfo {
|
||||
/** Minimum allowed priority */
|
||||
const MIN_PRIORITY = 1;
|
||||
|
||||
/** Maximum allowed priority */
|
||||
const MAX_PRIORITY = 100;
|
||||
|
||||
/** @var SessionProvider|null */
|
||||
private $provider;
|
||||
|
||||
/** @var string */
|
||||
private $id;
|
||||
|
||||
/** @var int */
|
||||
private $priority;
|
||||
|
||||
/** @var UserInfo|null */
|
||||
private $userInfo = null;
|
||||
|
||||
private $persisted = false;
|
||||
private $remembered = false;
|
||||
private $forceHTTPS = false;
|
||||
private $idIsSafe = false;
|
||||
|
||||
/** @var array|null */
|
||||
private $providerMetadata = null;
|
||||
|
||||
/**
|
||||
* @param int $priority Session priority
|
||||
* @param array $data
|
||||
* - provider: (SessionProvider|null) If not given, the provider will be
|
||||
* determined from the saved session data.
|
||||
* - id: (string|null) Session ID
|
||||
* - userInfo: (UserInfo|null) User known from the request. If
|
||||
* $provider->canChangeUser() is false, a verified user
|
||||
* must be provided.
|
||||
* - persisted: (bool) Whether this session was persisted
|
||||
* - remembered: (bool) Whether the verified user was remembered.
|
||||
* Defaults to true.
|
||||
* - forceHTTPS: (bool) Whether to force HTTPS for this session
|
||||
* - metadata: (array) Provider metadata, to be returned by
|
||||
* Session::getProviderMetadata().
|
||||
* - idIsSafe: (bool) Set true if the 'id' did not come from the user.
|
||||
* Generally you'll use this from SessionProvider::newEmptySession(),
|
||||
* and not from any other method.
|
||||
* - copyFrom: (SessionInfo) SessionInfo to copy other data items from.
|
||||
*/
|
||||
public function __construct( $priority, array $data ) {
|
||||
if ( $priority < self::MIN_PRIORITY || $priority > self::MAX_PRIORITY ) {
|
||||
throw new \InvalidArgumentException( 'Invalid priority' );
|
||||
}
|
||||
|
||||
if ( isset( $data['copyFrom'] ) ) {
|
||||
$from = $data['copyFrom'];
|
||||
if ( !$from instanceof SessionInfo ) {
|
||||
throw new \InvalidArgumentException( 'Invalid copyFrom' );
|
||||
}
|
||||
$data += array(
|
||||
'provider' => $from->provider,
|
||||
'id' => $from->id,
|
||||
'userInfo' => $from->userInfo,
|
||||
'persisted' => $from->persisted,
|
||||
'remembered' => $from->remembered,
|
||||
'forceHTTPS' => $from->forceHTTPS,
|
||||
'metadata' => $from->providerMetadata,
|
||||
'idIsSafe' => $from->idIsSafe,
|
||||
// @codeCoverageIgnoreStart
|
||||
);
|
||||
// @codeCoverageIgnoreEnd
|
||||
} else {
|
||||
$data += array(
|
||||
'provider' => null,
|
||||
'id' => null,
|
||||
'userInfo' => null,
|
||||
'persisted' => false,
|
||||
'remembered' => true,
|
||||
'forceHTTPS' => false,
|
||||
'metadata' => null,
|
||||
'idIsSafe' => false,
|
||||
// @codeCoverageIgnoreStart
|
||||
);
|
||||
// @codeCoverageIgnoreEnd
|
||||
}
|
||||
|
||||
if ( $data['id'] !== null && !SessionManager::validateSessionId( $data['id'] ) ) {
|
||||
throw new \InvalidArgumentException( 'Invalid session ID' );
|
||||
}
|
||||
|
||||
if ( $data['userInfo'] !== null && !$data['userInfo'] instanceof UserInfo ) {
|
||||
throw new \InvalidArgumentException( 'Invalid userInfo' );
|
||||
}
|
||||
|
||||
if ( !$data['provider'] && $data['id'] === null ) {
|
||||
throw new \InvalidArgumentException(
|
||||
'Must supply an ID when no provider is given'
|
||||
);
|
||||
}
|
||||
|
||||
if ( $data['metadata'] !== null && !is_array( $data['metadata'] ) ) {
|
||||
throw new \InvalidArgumentException( 'Invalid metadata' );
|
||||
}
|
||||
|
||||
$this->provider = $data['provider'];
|
||||
if ( $data['id'] !== null ) {
|
||||
$this->id = $data['id'];
|
||||
$this->idIsSafe = $data['idIsSafe'];
|
||||
} else {
|
||||
$this->id = $this->provider->getManager()->generateSessionId();
|
||||
$this->idIsSafe = true;
|
||||
}
|
||||
$this->priority = (int)$priority;
|
||||
$this->userInfo = $data['userInfo'];
|
||||
$this->persisted = (bool)$data['persisted'];
|
||||
if ( $data['provider'] !== null ) {
|
||||
if ( $this->userInfo !== null && !$this->userInfo->isAnon() && $this->userInfo->isVerified() ) {
|
||||
$this->remembered = (bool)$data['remembered'];
|
||||
}
|
||||
$this->providerMetadata = $data['metadata'];
|
||||
}
|
||||
$this->forceHTTPS = (bool)$data['forceHTTPS'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the provider
|
||||
* @return SessionProvider|null
|
||||
*/
|
||||
final public function getProvider() {
|
||||
return $this->provider;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the session ID
|
||||
* @return string
|
||||
*/
|
||||
final public function getId() {
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicate whether the ID is "safe"
|
||||
*
|
||||
* The ID is safe in the following cases:
|
||||
* - The ID was randomly generated by the constructor.
|
||||
* - The ID was found in the backend data store.
|
||||
* - $this->getProvider()->persistsSessionId() is false.
|
||||
* - The constructor was explicitly told it's safe using the 'idIsSafe'
|
||||
* parameter.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
final public function isIdSafe() {
|
||||
return $this->idIsSafe;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the priority
|
||||
* @return int
|
||||
*/
|
||||
final public function getPriority() {
|
||||
return $this->priority;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the user
|
||||
* @return UserInfo|null
|
||||
*/
|
||||
final public function getUserInfo() {
|
||||
return $this->userInfo;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return whether the session is persisted
|
||||
*
|
||||
* i.e. a session ID was given to the constuctor
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
final public function wasPersisted() {
|
||||
return $this->persisted;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return provider metadata
|
||||
* @return array|null
|
||||
*/
|
||||
final public function getProviderMetadata() {
|
||||
return $this->providerMetadata;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return whether the user was remembered
|
||||
*
|
||||
* For providers that can persist the user separately from the session,
|
||||
* the human using it may not actually *want* that to be done. For example,
|
||||
* a cookie-based provider can set cookies that are longer-lived than the
|
||||
* backend session data, but on a public terminal the human likely doesn't
|
||||
* want those cookies set.
|
||||
*
|
||||
* This is false unless a non-anonymous verified user was passed to
|
||||
* the SessionInfo constructor by the provider, and the provider didn't
|
||||
* pass false for the 'remembered' data item.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
final public function wasRemembered() {
|
||||
return $this->remembered;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether this session should only be used over HTTPS
|
||||
* @return bool
|
||||
*/
|
||||
final public function forceHTTPS() {
|
||||
return $this->forceHTTPS;
|
||||
}
|
||||
|
||||
public function __toString() {
|
||||
return '[' . $this->getPriority() . ']' .
|
||||
( $this->getProvider() ?: 'null' ) .
|
||||
( $this->userInfo ?: '<null>' ) . $this->getId();
|
||||
}
|
||||
|
||||
/**
|
||||
* Compare two SessionInfo objects by priority
|
||||
* @param SessionInfo $a
|
||||
* @param SessionInfo $b
|
||||
* @return int Negative if $a < $b, positive if $a > $b, zero if equal
|
||||
*/
|
||||
public static function compare( $a, $b ) {
|
||||
return $a->getPriority() - $b->getPriority();
|
||||
}
|
||||
|
||||
}
|
||||
997
includes/session/SessionManager.php
Normal file
997
includes/session/SessionManager.php
Normal file
|
|
@ -0,0 +1,997 @@
|
|||
<?php
|
||||
/**
|
||||
* MediaWiki\Session entry point
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program; if not, write to the Free Software Foundation, Inc.,
|
||||
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
* http://www.gnu.org/copyleft/gpl.html
|
||||
*
|
||||
* @file
|
||||
* @ingroup Session
|
||||
*/
|
||||
|
||||
namespace MediaWiki\Session;
|
||||
|
||||
use Psr\Log\LoggerInterface;
|
||||
use BagOStuff;
|
||||
use Config;
|
||||
use FauxRequest;
|
||||
use Language;
|
||||
use Message;
|
||||
use User;
|
||||
use WebRequest;
|
||||
|
||||
/**
|
||||
* This serves as the entry point to the MediaWiki session handling system.
|
||||
*
|
||||
* @ingroup Session
|
||||
* @since 1.27
|
||||
*/
|
||||
final class SessionManager implements SessionManagerInterface {
|
||||
/** @var SessionManager|null */
|
||||
private static $instance = null;
|
||||
|
||||
/** @var Session|null */
|
||||
private static $globalSession = null;
|
||||
|
||||
/** @var WebRequest|null */
|
||||
private static $globalSessionRequest = null;
|
||||
|
||||
/** @var LoggerInterface */
|
||||
private $logger;
|
||||
|
||||
/** @var Config */
|
||||
private $config;
|
||||
|
||||
/** @var BagOStuff|null */
|
||||
private $store;
|
||||
|
||||
/** @var SessionProvider[] */
|
||||
private $sessionProviders = null;
|
||||
|
||||
/** @var string[] */
|
||||
private $varyCookies = null;
|
||||
|
||||
/** @var array */
|
||||
private $varyHeaders = null;
|
||||
|
||||
/** @var SessionBackend[] */
|
||||
private $allSessionBackends = array();
|
||||
|
||||
/** @var SessionId[] */
|
||||
private $allSessionIds = array();
|
||||
|
||||
/** @var string[] */
|
||||
private $preventUsers = array();
|
||||
|
||||
/**
|
||||
* Get the global SessionManager
|
||||
* @return SessionManagerInterface
|
||||
* (really a SessionManager, but this is to make IDEs less confused)
|
||||
*/
|
||||
public static function singleton() {
|
||||
if ( self::$instance === null ) {
|
||||
self::$instance = new self();
|
||||
}
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the "global" session
|
||||
*
|
||||
* If PHP's session_id() has been set, returns that session. Otherwise
|
||||
* returns the session for RequestContext::getMain()->getRequest().
|
||||
*
|
||||
* @return Session
|
||||
*/
|
||||
public static function getGlobalSession() {
|
||||
if ( !PHPSessionHandler::isEnabled() ) {
|
||||
$id = '';
|
||||
} else {
|
||||
$id = session_id();
|
||||
}
|
||||
|
||||
$request = \RequestContext::getMain()->getRequest();
|
||||
if (
|
||||
!self::$globalSession // No global session is set up yet
|
||||
|| self::$globalSessionRequest !== $request // The global WebRequest changed
|
||||
|| $id !== '' && self::$globalSession->getId() !== $id // Someone messed with session_id()
|
||||
) {
|
||||
self::$globalSessionRequest = $request;
|
||||
if ( $id === '' ) {
|
||||
// session_id() wasn't used, so fetch the Session from the WebRequest.
|
||||
// We use $request->getSession() instead of $singleton->getSessionForRequest()
|
||||
// because doing the latter would require a public
|
||||
// "$request->getSessionId()" method that would confuse end
|
||||
// users by returning SessionId|null where they'd expect it to
|
||||
// be short for $request->getSession()->getId(), and would
|
||||
// wind up being a duplicate of the code in
|
||||
// $request->getSession() anyway.
|
||||
self::$globalSession = $request->getSession();
|
||||
} else {
|
||||
// Someone used session_id(), so we need to follow suit.
|
||||
// Note this overwrites whatever session might already be
|
||||
// associated with $request with the one for $id.
|
||||
self::$globalSession = self::singleton()->getSessionById( $id, false, $request );
|
||||
}
|
||||
}
|
||||
return self::$globalSession;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $options
|
||||
* - config: Config to fetch configuration from. Defaults to the default 'main' config.
|
||||
* - logger: LoggerInterface to use for logging. Defaults to the 'session' channel.
|
||||
* - store: BagOStuff to store session data in.
|
||||
*/
|
||||
public function __construct( $options = array() ) {
|
||||
if ( isset( $options['config'] ) ) {
|
||||
$this->config = $options['config'];
|
||||
if ( !$this->config instanceof Config ) {
|
||||
throw new \InvalidArgumentException(
|
||||
'$options[\'config\'] must be an instance of Config'
|
||||
);
|
||||
}
|
||||
} else {
|
||||
$this->config = \ConfigFactory::getDefaultInstance()->makeConfig( 'main' );
|
||||
}
|
||||
|
||||
if ( isset( $options['logger'] ) ) {
|
||||
if ( !$options['logger'] instanceof LoggerInterface ) {
|
||||
throw new \InvalidArgumentException(
|
||||
'$options[\'logger\'] must be an instance of LoggerInterface'
|
||||
);
|
||||
}
|
||||
$this->setLogger( $options['logger'] );
|
||||
} else {
|
||||
$this->setLogger( \MediaWiki\Logger\LoggerFactory::getInstance( 'session' ) );
|
||||
}
|
||||
|
||||
if ( isset( $options['store'] ) ) {
|
||||
if ( !$options['store'] instanceof BagOStuff ) {
|
||||
throw new \InvalidArgumentException(
|
||||
'$options[\'store\'] must be an instance of BagOStuff'
|
||||
);
|
||||
}
|
||||
$this->store = $options['store'];
|
||||
} else {
|
||||
$this->store = \ObjectCache::getInstance( $this->config->get( 'SessionCacheType' ) );
|
||||
$this->store->setLogger( $this->logger );
|
||||
}
|
||||
|
||||
register_shutdown_function( array( $this, 'shutdown' ) );
|
||||
}
|
||||
|
||||
public function setLogger( LoggerInterface $logger ) {
|
||||
$this->logger = $logger;
|
||||
}
|
||||
|
||||
public function getPersistedSessionId( WebRequest $request ) {
|
||||
$info = $this->getSessionInfoForRequest( $request );
|
||||
if ( $info && $info->wasPersisted() ) {
|
||||
return $info->getId();
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public function getSessionForRequest( WebRequest $request ) {
|
||||
$info = $this->getSessionInfoForRequest( $request );
|
||||
|
||||
if ( !$info ) {
|
||||
$session = $this->getEmptySession( $request );
|
||||
} else {
|
||||
$session = $this->getSessionFromInfo( $info, $request );
|
||||
}
|
||||
return $session;
|
||||
}
|
||||
|
||||
public function getSessionById( $id, $noEmpty = false, WebRequest $request = null ) {
|
||||
if ( !self::validateSessionId( $id ) ) {
|
||||
throw new \InvalidArgumentException( 'Invalid session ID' );
|
||||
}
|
||||
if ( !$request ) {
|
||||
$request = new FauxRequest;
|
||||
}
|
||||
|
||||
$session = null;
|
||||
|
||||
// Test this here to provide a better log message for the common case
|
||||
// of "no such ID"
|
||||
$key = wfMemcKey( 'MWSession', $id );
|
||||
if ( is_array( $this->store->get( $key ) ) ) {
|
||||
$info = new SessionInfo( SessionInfo::MIN_PRIORITY, array( 'id' => $id, 'idIsSafe' => true ) );
|
||||
if ( $this->loadSessionInfoFromStore( $info, $request ) ) {
|
||||
$session = $this->getSessionFromInfo( $info, $request );
|
||||
}
|
||||
}
|
||||
|
||||
if ( !$noEmpty && $session === null ) {
|
||||
$ex = null;
|
||||
try {
|
||||
$session = $this->getEmptySessionInternal( $request, $id );
|
||||
} catch ( \Exception $ex ) {
|
||||
$this->logger->error( __METHOD__ . ': failed to create empty session: ' .
|
||||
$ex->getMessage() );
|
||||
$session = null;
|
||||
}
|
||||
if ( $session === null ) {
|
||||
throw new \UnexpectedValueException(
|
||||
'Can neither load the session nor create an empty session', 0, $ex
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return $session;
|
||||
}
|
||||
|
||||
public function getEmptySession( WebRequest $request = null ) {
|
||||
return $this->getEmptySessionInternal( $request );
|
||||
}
|
||||
|
||||
/**
|
||||
* @see SessionManagerInterface::getEmptySession
|
||||
* @param WebRequest|null $request
|
||||
* @param string|null $id ID to force on the new session
|
||||
* @return Session
|
||||
*/
|
||||
private function getEmptySessionInternal( WebRequest $request = null, $id = null ) {
|
||||
if ( $id !== null ) {
|
||||
if ( !self::validateSessionId( $id ) ) {
|
||||
throw new \InvalidArgumentException( 'Invalid session ID' );
|
||||
}
|
||||
|
||||
$key = wfMemcKey( 'MWSession', $id );
|
||||
if ( is_array( $this->store->get( $key ) ) ) {
|
||||
throw new \InvalidArgumentException( 'Session ID already exists' );
|
||||
}
|
||||
}
|
||||
if ( !$request ) {
|
||||
$request = new FauxRequest;
|
||||
}
|
||||
|
||||
$infos = array();
|
||||
foreach ( $this->getProviders() as $provider ) {
|
||||
$info = $provider->newSessionInfo( $id );
|
||||
if ( !$info ) {
|
||||
continue;
|
||||
}
|
||||
if ( $info->getProvider() !== $provider ) {
|
||||
throw new \UnexpectedValueException(
|
||||
"$provider returned an empty session info for a different provider: $info"
|
||||
);
|
||||
}
|
||||
if ( $id !== null && $info->getId() !== $id ) {
|
||||
throw new \UnexpectedValueException(
|
||||
"$provider returned empty session info with a wrong id: " .
|
||||
$info->getId() . ' != ' . $id
|
||||
);
|
||||
}
|
||||
if ( !$info->isIdSafe() ) {
|
||||
throw new \UnexpectedValueException(
|
||||
"$provider returned empty session info with id flagged unsafe"
|
||||
);
|
||||
}
|
||||
$compare = $infos ? SessionInfo::compare( $infos[0], $info ) : -1;
|
||||
if ( $compare > 0 ) {
|
||||
continue;
|
||||
}
|
||||
if ( $compare === 0 ) {
|
||||
$infos[] = $info;
|
||||
} else {
|
||||
$infos = array( $info );
|
||||
}
|
||||
}
|
||||
|
||||
// Make sure there's exactly one
|
||||
if ( count( $infos ) > 1 ) {
|
||||
throw new \UnexpectedValueException(
|
||||
'Multiple empty sessions tied for top priority: ' . join( ', ', $infos )
|
||||
);
|
||||
} elseif ( count( $infos ) < 1 ) {
|
||||
throw new \UnexpectedValueException( 'No provider could provide an empty session!' );
|
||||
}
|
||||
|
||||
return $this->getSessionFromInfo( $infos[0], $request );
|
||||
}
|
||||
|
||||
public function getVaryHeaders() {
|
||||
if ( $this->varyHeaders === null ) {
|
||||
$headers = array();
|
||||
foreach ( $this->getProviders() as $provider ) {
|
||||
foreach ( $provider->getVaryHeaders() as $header => $options ) {
|
||||
if ( !isset( $headers[$header] ) ) {
|
||||
$headers[$header] = array();
|
||||
}
|
||||
if ( is_array( $options ) ) {
|
||||
$headers[$header] = array_unique( array_merge( $headers[$header], $options ) );
|
||||
}
|
||||
}
|
||||
}
|
||||
$this->varyHeaders = $headers;
|
||||
}
|
||||
return $this->varyHeaders;
|
||||
}
|
||||
|
||||
public function getVaryCookies() {
|
||||
if ( $this->varyCookies === null ) {
|
||||
$cookies = array();
|
||||
foreach ( $this->getProviders() as $provider ) {
|
||||
$cookies = array_merge( $cookies, $provider->getVaryCookies() );
|
||||
}
|
||||
$this->varyCookies = array_values( array_unique( $cookies ) );
|
||||
}
|
||||
return $this->varyCookies;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate a session ID
|
||||
* @param string $id
|
||||
* @return bool
|
||||
*/
|
||||
public static function validateSessionId( $id ) {
|
||||
return is_string( $id ) && preg_match( '/^[a-zA-Z0-9_-]{32,}$/', $id );
|
||||
}
|
||||
|
||||
/**
|
||||
* @name Internal methods
|
||||
* @{
|
||||
*/
|
||||
|
||||
/**
|
||||
* Auto-create the given user, if necessary
|
||||
* @private Don't call this yourself. Let Setup.php do it for you at the right time.
|
||||
* @note This more properly belongs in AuthManager, but we need it now.
|
||||
* When AuthManager comes, this will be deprecated and will pass-through
|
||||
* to the corresponding AuthManager method.
|
||||
* @param User $user User to auto-create
|
||||
* @return bool Success
|
||||
*/
|
||||
public static function autoCreateUser( User $user ) {
|
||||
global $wgAuth;
|
||||
|
||||
$logger = self::singleton()->logger;
|
||||
|
||||
// Much of this code is based on that in CentralAuth
|
||||
|
||||
// Try the local user from the slave DB
|
||||
$localId = User::idFromName( $user->getName() );
|
||||
|
||||
// Fetch the user ID from the master, so that we don't try to create the user
|
||||
// when they already exist, due to replication lag
|
||||
// @codeCoverageIgnoreStart
|
||||
if ( !$localId && wfGetLB()->getReaderIndex() != 0 ) {
|
||||
$localId = User::idFromName( $user->getName(), User::READ_LATEST );
|
||||
}
|
||||
// @codeCoverageIgnoreEnd
|
||||
|
||||
if ( $localId ) {
|
||||
// User exists after all.
|
||||
$user->setId( $localId );
|
||||
$user->loadFromId();
|
||||
return false;
|
||||
}
|
||||
|
||||
// Denied by AuthPlugin? But ignore AuthPlugin itself.
|
||||
if ( get_class( $wgAuth ) !== 'AuthPlugin' && !$wgAuth->autoCreate() ) {
|
||||
$logger->debug( __METHOD__ . ': denied by AuthPlugin' );
|
||||
$user->setId( 0 );
|
||||
$user->loadFromId();
|
||||
return false;
|
||||
}
|
||||
|
||||
// Wiki is read-only?
|
||||
if ( wfReadOnly() ) {
|
||||
$logger->debug( __METHOD__ . ': denied by wfReadOnly()' );
|
||||
$user->setId( 0 );
|
||||
$user->loadFromId();
|
||||
return false;
|
||||
}
|
||||
|
||||
$userName = $user->getName();
|
||||
|
||||
// Check the session, if we tried to create this user already there's
|
||||
// no point in retrying.
|
||||
$session = self::getGlobalSession();
|
||||
$reason = $session->get( 'MWSession::AutoCreateBlacklist' );
|
||||
if ( $reason ) {
|
||||
$logger->debug( __METHOD__ . ": blacklisted in session ($reason)" );
|
||||
$user->setId( 0 );
|
||||
$user->loadFromId();
|
||||
return false;
|
||||
}
|
||||
|
||||
// Is the IP user able to create accounts?
|
||||
$anon = new User;
|
||||
if ( !$anon->isAllowedAny( 'createaccount', 'autocreateaccount' )
|
||||
|| $anon->isBlockedFromCreateAccount()
|
||||
) {
|
||||
// Blacklist the user to avoid repeated DB queries subsequently
|
||||
$logger->debug( __METHOD__ . ': user is blocked from this wiki, blacklisting' );
|
||||
$session->set( 'MWSession::AutoCreateBlacklist', 'blocked', 600 );
|
||||
$session->persist();
|
||||
$user->setId( 0 );
|
||||
$user->loadFromId();
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check for validity of username
|
||||
if ( !User::isCreatableName( $userName ) ) {
|
||||
$logger->debug( __METHOD__ . ': Invalid username, blacklisting' );
|
||||
$session->set( 'MWSession::AutoCreateBlacklist', 'invalid username', 600 );
|
||||
$session->persist();
|
||||
$user->setId( 0 );
|
||||
$user->loadFromId();
|
||||
return false;
|
||||
}
|
||||
|
||||
// Give other extensions a chance to stop auto creation.
|
||||
$user->loadDefaults( $userName );
|
||||
$abortMessage = '';
|
||||
if ( !\Hooks::run( 'AbortAutoAccount', array( $user, &$abortMessage ) ) ) {
|
||||
// In this case we have no way to return the message to the user,
|
||||
// but we can log it.
|
||||
$logger->debug( __METHOD__ . ": denied by hook: $abortMessage" );
|
||||
$session->set( 'MWSession::AutoCreateBlacklist', "hook aborted: $abortMessage", 600 );
|
||||
$session->persist();
|
||||
$user->setId( 0 );
|
||||
$user->loadFromId();
|
||||
return false;
|
||||
}
|
||||
|
||||
// Make sure the name has not been changed
|
||||
if ( $user->getName() !== $userName ) {
|
||||
$user->setId( 0 );
|
||||
$user->loadFromId();
|
||||
throw new \UnexpectedValueException(
|
||||
'AbortAutoAccount hook tried to change the user name'
|
||||
);
|
||||
}
|
||||
|
||||
// Ignore warnings about master connections/writes...hard to avoid here
|
||||
\Profiler::instance()->getTransactionProfiler()->resetExpectations();
|
||||
|
||||
$cache = \ObjectCache::getLocalClusterInstance();
|
||||
$backoffKey = wfMemcKey( 'MWSession', 'autocreate-failed', md5( $userName ) );
|
||||
if ( $cache->get( $backoffKey ) ) {
|
||||
$logger->debug( __METHOD__ . ': denied by prior creation attempt failures' );
|
||||
$user->setId( 0 );
|
||||
$user->loadFromId();
|
||||
return false;
|
||||
}
|
||||
|
||||
// Checks passed, create the user...
|
||||
$from = isset( $_SERVER['REQUEST_URI'] ) ? $_SERVER['REQUEST_URI'] : 'CLI';
|
||||
$logger->info( __METHOD__ . ": creating new user ($userName) - from: $from" );
|
||||
|
||||
try {
|
||||
// Insert the user into the local DB master
|
||||
$status = $user->addToDatabase();
|
||||
if ( !$status->isOK() ) {
|
||||
// @codeCoverageIgnoreStart
|
||||
$logger->error( __METHOD__ . ': failed with message ' . $status->getWikiText() );
|
||||
$user->setId( 0 );
|
||||
$user->loadFromId();
|
||||
return false;
|
||||
// @codeCoverageIgnoreEnd
|
||||
}
|
||||
} catch ( \Exception $ex ) {
|
||||
// @codeCoverageIgnoreStart
|
||||
$logger->error( __METHOD__ . ': failed with exception ' . $ex->getMessage() );
|
||||
// Do not keep throwing errors for a while
|
||||
$cache->set( $backoffKey, 1, 600 );
|
||||
// Bubble up error; which should normally trigger DB rollbacks
|
||||
throw $ex;
|
||||
// @codeCoverageIgnoreEnd
|
||||
}
|
||||
|
||||
# Notify hooks (e.g. Newuserlog)
|
||||
\Hooks::run( 'AuthPluginAutoCreate', array( $user ) );
|
||||
\Hooks::run( 'LocalUserCreated', array( $user, true ) );
|
||||
|
||||
# Update user count
|
||||
\DeferredUpdates::addUpdate( new \SiteStatsUpdate( 0, 0, 0, 0, 1 ) );
|
||||
|
||||
# Watch user's userpage and talk page
|
||||
$user->addWatch( $user->getUserPage(), \WatchedItem::IGNORE_USER_RIGHTS );
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prevent future sessions for the user
|
||||
*
|
||||
* The intention is that the named account will never again be usable for
|
||||
* normal login (i.e. there is no way to undo the prevention of access).
|
||||
*
|
||||
* @private For use from \\User::newSystemUser only
|
||||
* @param string $username
|
||||
*/
|
||||
public function preventSessionsForUser( $username ) {
|
||||
$this->preventUsers[$username] = true;
|
||||
|
||||
// Reset the user's token to kill existing sessions
|
||||
$user = User::newFromName( $username );
|
||||
if ( $user && $user->getToken() ) {
|
||||
$user->setToken( true );
|
||||
$user->saveSettings();
|
||||
}
|
||||
|
||||
// Instruct the session providers to kill any other sessions too.
|
||||
foreach ( $this->getProviders() as $provider ) {
|
||||
$provider->preventSessionsForUser( $username );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test if a user is prevented
|
||||
* @private For use from SessionBackend only
|
||||
* @param string $username
|
||||
* @return bool
|
||||
*/
|
||||
public function isUserSessionPrevented( $username ) {
|
||||
return !empty( $this->preventUsers[$username] );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the available SessionProviders
|
||||
* @return SessionProvider[]
|
||||
*/
|
||||
protected function getProviders() {
|
||||
if ( $this->sessionProviders === null ) {
|
||||
$this->sessionProviders = array();
|
||||
foreach ( $this->config->get( 'SessionProviders' ) as $spec ) {
|
||||
$provider = \ObjectFactory::getObjectFromSpec( $spec );
|
||||
$provider->setLogger( $this->logger );
|
||||
$provider->setConfig( $this->config );
|
||||
$provider->setManager( $this );
|
||||
if ( isset( $this->sessionProviders[(string)$provider] ) ) {
|
||||
throw new \UnexpectedValueException( "Duplicate provider name \"$provider\"" );
|
||||
}
|
||||
$this->sessionProviders[(string)$provider] = $provider;
|
||||
}
|
||||
}
|
||||
return $this->sessionProviders;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a session provider by name
|
||||
*
|
||||
* Generally, this will only be used by internal implementation of some
|
||||
* special session-providing mechanism. General purpose code, if it needs
|
||||
* to access a SessionProvider at all, will use Session::getProvider().
|
||||
*
|
||||
* @param string $name
|
||||
* @return SessionProvider|null
|
||||
*/
|
||||
public function getProvider( $name ) {
|
||||
$providers = $this->getProviders();
|
||||
return isset( $providers[$name] ) ? $providers[$name] : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Save all active sessions on shutdown
|
||||
* @private For internal use with register_shutdown_function()
|
||||
*/
|
||||
public function shutdown() {
|
||||
if ( $this->allSessionBackends ) {
|
||||
$this->logger->debug( 'Saving all sessions on shutdown' );
|
||||
if ( session_id() !== '' ) {
|
||||
// @codeCoverageIgnoreStart
|
||||
session_write_close();
|
||||
}
|
||||
// @codeCoverageIgnoreEnd
|
||||
foreach ( $this->allSessionBackends as $backend ) {
|
||||
$backend->save( true );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch the SessionInfo(s) for a request
|
||||
* @param WebRequest $request
|
||||
* @return SessionInfo|null
|
||||
*/
|
||||
private function getSessionInfoForRequest( WebRequest $request ) {
|
||||
// Call all providers to fetch "the" session
|
||||
$infos = array();
|
||||
foreach ( $this->getProviders() as $provider ) {
|
||||
$info = $provider->provideSessionInfo( $request );
|
||||
if ( !$info ) {
|
||||
continue;
|
||||
}
|
||||
if ( $info->getProvider() !== $provider ) {
|
||||
throw new \UnexpectedValueException(
|
||||
"$provider returned session info for a different provider: $info"
|
||||
);
|
||||
}
|
||||
$infos[] = $info;
|
||||
}
|
||||
|
||||
// Sort the SessionInfos. Then find the first one that can be
|
||||
// successfully loaded, and then all the ones after it with the same
|
||||
// priority.
|
||||
usort( $infos, 'MediaWiki\\Session\\SessionInfo::compare' );
|
||||
$retInfos = array();
|
||||
while ( $infos ) {
|
||||
$info = array_pop( $infos );
|
||||
if ( $this->loadSessionInfoFromStore( $info, $request ) ) {
|
||||
$retInfos[] = $info;
|
||||
while ( $infos ) {
|
||||
$info = array_pop( $infos );
|
||||
if ( SessionInfo::compare( $retInfos[0], $info ) ) {
|
||||
// We hit a lower priority, stop checking.
|
||||
break;
|
||||
}
|
||||
if ( $this->loadSessionInfoFromStore( $info, $request ) ) {
|
||||
// This is going to error out below, but we want to
|
||||
// provide a complete list.
|
||||
$retInfos[] = $info;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ( count( $retInfos ) > 1 ) {
|
||||
$ex = new \OverflowException(
|
||||
'Multiple sessions for this request tied for top priority: ' . join( ', ', $retInfos )
|
||||
);
|
||||
$ex->sessionInfos = $retInfos;
|
||||
throw $ex;
|
||||
}
|
||||
|
||||
return $retInfos ? $retInfos[0] : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load and verify the session info against the store
|
||||
*
|
||||
* @param SessionInfo &$info Will likely be replaced with an updated SessionInfo instance
|
||||
* @param WebRequest $request
|
||||
* @return bool Whether the session info matches the stored data (if any)
|
||||
*/
|
||||
private function loadSessionInfoFromStore( SessionInfo &$info, WebRequest $request ) {
|
||||
$blob = $this->store->get( wfMemcKey( 'MWSession', $info->getId() ) );
|
||||
|
||||
$newParams = array();
|
||||
|
||||
if ( $blob !== false ) {
|
||||
// Sanity check: blob must be an array, if it's saved at all
|
||||
if ( !is_array( $blob ) ) {
|
||||
$this->logger->warning( "Session $info: Bad data" );
|
||||
return false;
|
||||
}
|
||||
|
||||
// Sanity check: blob has data and metadata arrays
|
||||
if ( !isset( $blob['data'] ) || !is_array( $blob['data'] ) ||
|
||||
!isset( $blob['metadata'] ) || !is_array( $blob['metadata'] )
|
||||
) {
|
||||
$this->logger->warning( "Session $info: Bad data structure" );
|
||||
return false;
|
||||
}
|
||||
|
||||
$data = $blob['data'];
|
||||
$metadata = $blob['metadata'];
|
||||
|
||||
// Sanity check: metadata must be an array and must contain certain
|
||||
// keys, if it's saved at all
|
||||
if ( !array_key_exists( 'userId', $metadata ) ||
|
||||
!array_key_exists( 'userName', $metadata ) ||
|
||||
!array_key_exists( 'userToken', $metadata ) ||
|
||||
!array_key_exists( 'provider', $metadata )
|
||||
) {
|
||||
$this->logger->warning( "Session $info: Bad metadata" );
|
||||
return false;
|
||||
}
|
||||
|
||||
// First, load the provider from metadata, or validate it against the metadata.
|
||||
$provider = $info->getProvider();
|
||||
if ( $provider === null ) {
|
||||
$newParams['provider'] = $provider = $this->getProvider( $metadata['provider'] );
|
||||
if ( !$provider ) {
|
||||
$this->logger->warning( "Session $info: Unknown provider, " . $metadata['provider'] );
|
||||
return false;
|
||||
}
|
||||
} elseif ( $metadata['provider'] !== (string)$provider ) {
|
||||
$this->logger->warning( "Session $info: Wrong provider, " .
|
||||
$metadata['provider'] . ' !== ' . $provider );
|
||||
return false;
|
||||
}
|
||||
|
||||
// Load provider metadata from metadata, or validate it against the metadata
|
||||
$providerMetadata = $info->getProviderMetadata();
|
||||
if ( isset( $metadata['providerMetadata'] ) ) {
|
||||
if ( $providerMetadata === null ) {
|
||||
$newParams['metadata'] = $metadata['providerMetadata'];
|
||||
} else {
|
||||
try {
|
||||
$newProviderMetadata = $provider->mergeMetadata(
|
||||
$metadata['providerMetadata'], $providerMetadata
|
||||
);
|
||||
if ( $newProviderMetadata !== $providerMetadata ) {
|
||||
$newParams['metadata'] = $newProviderMetadata;
|
||||
}
|
||||
} catch ( \UnexpectedValueException $ex ) {
|
||||
$this->logger->warning( "Session $info: Metadata merge failed: " . $ex->getMessage() );
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Next, load the user from metadata, or validate it against the metadata.
|
||||
$userInfo = $info->getUserInfo();
|
||||
if ( !$userInfo ) {
|
||||
// For loading, id is preferred to name.
|
||||
try {
|
||||
if ( $metadata['userId'] ) {
|
||||
$userInfo = UserInfo::newFromId( $metadata['userId'] );
|
||||
} elseif ( $metadata['userName'] !== null ) { // Shouldn't happen, but just in case
|
||||
$userInfo = UserInfo::newFromName( $metadata['userName'] );
|
||||
} else {
|
||||
$userInfo = UserInfo::newAnonymous();
|
||||
}
|
||||
} catch ( \InvalidArgumentException $ex ) {
|
||||
$this->logger->error( "Session $info: " . $ex->getMessage() );
|
||||
return false;
|
||||
}
|
||||
$newParams['userInfo'] = $userInfo;
|
||||
} else {
|
||||
// User validation passes if user ID matches, or if there
|
||||
// is no saved ID and the names match.
|
||||
if ( $metadata['userId'] ) {
|
||||
if ( $metadata['userId'] !== $userInfo->getId() ) {
|
||||
$this->logger->warning( "Session $info: User ID mismatch, " .
|
||||
$metadata['userId'] . ' !== ' . $userInfo->getId() );
|
||||
return false;
|
||||
}
|
||||
|
||||
// If the user was renamed, probably best to fail here.
|
||||
if ( $metadata['userName'] !== null &&
|
||||
$userInfo->getName() !== $metadata['userName']
|
||||
) {
|
||||
$this->logger->warning( "Session $info: User ID matched but name didn't (rename?), " .
|
||||
$metadata['userName'] . ' !== ' . $userInfo->getName() );
|
||||
return false;
|
||||
}
|
||||
|
||||
} elseif ( $metadata['userName'] !== null ) { // Shouldn't happen, but just in case
|
||||
if ( $metadata['userName'] !== $userInfo->getName() ) {
|
||||
$this->logger->warning( "Session $info: User name mismatch, " .
|
||||
$metadata['userName'] . ' !== ' . $userInfo->getName() );
|
||||
return false;
|
||||
}
|
||||
} elseif ( !$userInfo->isAnon() ) {
|
||||
// Metadata specifies an anonymous user, but the passed-in
|
||||
// user isn't anonymous.
|
||||
$this->logger->warning(
|
||||
"Session $info: Metadata has an anonymous user, " .
|
||||
'but a non-anon user was provided'
|
||||
);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// And if we have a token in the metadata, it must match the loaded/provided user.
|
||||
if ( $metadata['userToken'] !== null &&
|
||||
$userInfo->getToken() !== $metadata['userToken']
|
||||
) {
|
||||
$this->logger->warning( "Session $info: User token mismatch" );
|
||||
return false;
|
||||
}
|
||||
if ( !$userInfo->isVerified() ) {
|
||||
$newParams['userInfo'] = $userInfo->verified();
|
||||
}
|
||||
|
||||
if ( !empty( $metadata['remember'] ) && !$info->wasRemembered() ) {
|
||||
$newParams['remembered'] = true;
|
||||
}
|
||||
if ( !empty( $metadata['forceHTTPS'] ) && !$info->forceHTTPS() ) {
|
||||
$newParams['forceHTTPS'] = true;
|
||||
}
|
||||
|
||||
if ( !$info->isIdSafe() ) {
|
||||
$newParams['idIsSafe'] = true;
|
||||
}
|
||||
} else {
|
||||
// No metadata, so we can't load the provider if one wasn't given.
|
||||
if ( $info->getProvider() === null ) {
|
||||
$this->logger->warning( "Session $info: Null provider and no metadata" );
|
||||
return false;
|
||||
}
|
||||
|
||||
// If no user was provided and no metadata, it must be anon.
|
||||
if ( !$info->getUserInfo() ) {
|
||||
if ( $info->getProvider()->canChangeUser() ) {
|
||||
$newParams['userInfo'] = UserInfo::newAnonymous();
|
||||
} else {
|
||||
$this->logger->info(
|
||||
"Session $info: No user provided and provider cannot set user"
|
||||
);
|
||||
return false;
|
||||
}
|
||||
} elseif ( !$info->getUserInfo()->isVerified() ) {
|
||||
$this->logger->warning(
|
||||
"Session $info: Unverified user provided and no metadata to auth it"
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
$data = false;
|
||||
$metadata = false;
|
||||
|
||||
if ( !$info->getProvider()->persistsSessionId() && !$info->isIdSafe() ) {
|
||||
// The ID doesn't come from the user, so it should be safe
|
||||
// (and if not, nothing we can do about it anyway)
|
||||
$newParams['idIsSafe'] = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Construct the replacement SessionInfo, if necessary
|
||||
if ( $newParams ) {
|
||||
$newParams['copyFrom'] = $info;
|
||||
$info = new SessionInfo( $info->getPriority(), $newParams );
|
||||
}
|
||||
|
||||
// Allow the provider to check the loaded SessionInfo
|
||||
$providerMetadata = $info->getProviderMetadata();
|
||||
if ( !$info->getProvider()->refreshSessionInfo( $info, $request, $providerMetadata ) ) {
|
||||
return false;
|
||||
}
|
||||
if ( $providerMetadata !== $info->getProviderMetadata() ) {
|
||||
$info = new SessionInfo( $info->getPriority(), array(
|
||||
'metadata' => $providerMetadata,
|
||||
'copyFrom' => $info,
|
||||
) );
|
||||
}
|
||||
|
||||
// Give hooks a chance to abort. Combined with the SessionMetadata
|
||||
// hook, this can allow for tying a session to an IP address or the
|
||||
// like.
|
||||
$reason = 'Hook aborted';
|
||||
if ( !\Hooks::run(
|
||||
'SessionCheckInfo',
|
||||
array( &$reason, $info, $request, $metadata, $data )
|
||||
) ) {
|
||||
$this->logger->warning( "Session $info: $reason" );
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a session corresponding to the passed SessionInfo
|
||||
* @private For use by a SessionProvider that needs to specially create its
|
||||
* own session.
|
||||
* @param SessionInfo $info
|
||||
* @param WebRequest $request
|
||||
* @return Session
|
||||
*/
|
||||
public function getSessionFromInfo( SessionInfo $info, WebRequest $request ) {
|
||||
$id = $info->getId();
|
||||
|
||||
if ( !isset( $this->allSessionBackends[$id] ) ) {
|
||||
if ( !isset( $this->allSessionIds[$id] ) ) {
|
||||
$this->allSessionIds[$id] = new SessionId( $id );
|
||||
}
|
||||
$backend = new SessionBackend(
|
||||
$this->allSessionIds[$id],
|
||||
$info,
|
||||
$this->store,
|
||||
$this->logger,
|
||||
$this->config->get( 'ObjectCacheSessionExpiry' )
|
||||
);
|
||||
$this->allSessionBackends[$id] = $backend;
|
||||
$delay = $backend->delaySave();
|
||||
} else {
|
||||
$backend = $this->allSessionBackends[$id];
|
||||
$delay = $backend->delaySave();
|
||||
if ( $info->wasPersisted() ) {
|
||||
$backend->persist();
|
||||
}
|
||||
if ( $info->wasRemembered() ) {
|
||||
$backend->setRememberUser( true );
|
||||
}
|
||||
}
|
||||
|
||||
$request->setSessionId( $backend->getSessionId() );
|
||||
$session = $backend->getSession( $request );
|
||||
|
||||
if ( !$info->isIdSafe() ) {
|
||||
$session->resetId();
|
||||
}
|
||||
|
||||
\ScopedCallback::consume( $delay );
|
||||
return $session;
|
||||
}
|
||||
|
||||
/**
|
||||
* Deregister a SessionBackend
|
||||
* @private For use from \\MediaWiki\\Session\\SessionBackend only
|
||||
* @param SessionBackend $backend
|
||||
*/
|
||||
public function deregisterSessionBackend( SessionBackend $backend ) {
|
||||
$id = $backend->getId();
|
||||
if ( !isset( $this->allSessionBackends[$id] ) || !isset( $this->allSessionIds[$id] ) ||
|
||||
$this->allSessionBackends[$id] !== $backend ||
|
||||
$this->allSessionIds[$id] !== $backend->getSessionId()
|
||||
) {
|
||||
throw new \InvalidArgumentException( 'Backend was not registered with this SessionManager' );
|
||||
}
|
||||
|
||||
unset( $this->allSessionBackends[$id] );
|
||||
// Explicitly do not unset $this->allSessionIds[$id]
|
||||
}
|
||||
|
||||
/**
|
||||
* Change a SessionBackend's ID
|
||||
* @private For use from \\MediaWiki\\Session\\SessionBackend only
|
||||
* @param SessionBackend $backend
|
||||
*/
|
||||
public function changeBackendId( SessionBackend $backend ) {
|
||||
$sessionId = $backend->getSessionId();
|
||||
$oldId = (string)$sessionId;
|
||||
if ( !isset( $this->allSessionBackends[$oldId] ) || !isset( $this->allSessionIds[$oldId] ) ||
|
||||
$this->allSessionBackends[$oldId] !== $backend ||
|
||||
$this->allSessionIds[$oldId] !== $sessionId
|
||||
) {
|
||||
throw new \InvalidArgumentException( 'Backend was not registered with this SessionManager' );
|
||||
}
|
||||
|
||||
$newId = $this->generateSessionId();
|
||||
|
||||
unset( $this->allSessionBackends[$oldId], $this->allSessionIds[$oldId] );
|
||||
$sessionId->setId( $newId );
|
||||
$this->allSessionBackends[$newId] = $backend;
|
||||
$this->allSessionIds[$newId] = $sessionId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a new random session ID
|
||||
* @return string
|
||||
*/
|
||||
public function generateSessionId() {
|
||||
do {
|
||||
$id = wfBaseConvert( \MWCryptRand::generateHex( 40 ), 16, 32, 32 );
|
||||
$key = wfMemcKey( 'MWSession', $id );
|
||||
} while ( isset( $this->allSessionIds[$id] ) || is_array( $this->store->get( $key ) ) );
|
||||
return $id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Call setters on a PHPSessionHandler
|
||||
* @private Use PhpSessionHandler::install()
|
||||
* @param PHPSessionHandler $handler
|
||||
*/
|
||||
public function setupPHPSessionHandler( PHPSessionHandler $handler ) {
|
||||
$handler->setManager( $this, $this->store, $this->logger );
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset the internal caching for unit testing
|
||||
*/
|
||||
public static function resetCache() {
|
||||
if ( !defined( 'MW_PHPUNIT_TEST' ) ) {
|
||||
// @codeCoverageIgnoreStart
|
||||
throw new MWException( __METHOD__ . ' may only be called from unit tests!' );
|
||||
// @codeCoverageIgnoreEnd
|
||||
}
|
||||
|
||||
self::$globalSession = null;
|
||||
self::$globalSessionRequest = null;
|
||||
}
|
||||
|
||||
/**@}*/
|
||||
|
||||
}
|
||||
109
includes/session/SessionManagerInterface.php
Normal file
109
includes/session/SessionManagerInterface.php
Normal file
|
|
@ -0,0 +1,109 @@
|
|||
<?php
|
||||
/**
|
||||
* MediaWiki\Session entry point interface
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program; if not, write to the Free Software Foundation, Inc.,
|
||||
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
* http://www.gnu.org/copyleft/gpl.html
|
||||
*
|
||||
* @file
|
||||
* @ingroup Session
|
||||
*/
|
||||
|
||||
namespace MediaWiki\Session;
|
||||
|
||||
use Psr\Log\LoggerAwareInterface;
|
||||
use WebRequest;
|
||||
|
||||
/**
|
||||
* This exists to make IDEs happy, so they don't see the
|
||||
* internal-but-required-to-be-public methods on SessionManager.
|
||||
*
|
||||
* @ingroup Session
|
||||
* @since 1.27
|
||||
*/
|
||||
interface SessionManagerInterface extends LoggerAwareInterface {
|
||||
/**
|
||||
* Fetch the persisted session ID in a request.
|
||||
*
|
||||
* Note this is not the same thing as whether the session associated with
|
||||
* the request is currently persistent, as the session might have been
|
||||
* first made persistent during this request.
|
||||
*
|
||||
* @param WebRequest $request
|
||||
* @return string|null
|
||||
* @throws \\OverflowException if there are multiple sessions tied for top
|
||||
* priority in the request. Exception has a property "sessionInfos"
|
||||
* holding the SessionInfo objects for the sessions involved.
|
||||
*/
|
||||
public function getPersistedSessionId( WebRequest $request );
|
||||
|
||||
/**
|
||||
* Fetch the session for a request
|
||||
*
|
||||
* @note You probably want to use $request->getSession() instead. It's more
|
||||
* efficient and doesn't break FauxRequests or sessions that were changed
|
||||
* by $this->getSessionById() or $this->getEmptySession().
|
||||
* @param WebRequest $request Any existing associated session will be reset
|
||||
* to the session corresponding to the data in the request itself.
|
||||
* @return Session
|
||||
* @throws \\OverflowException if there are multiple sessions tied for top
|
||||
* priority in the request. Exception has a property "sessionInfos"
|
||||
* holding the SessionInfo objects for the sessions involved.
|
||||
*/
|
||||
public function getSessionForRequest( WebRequest $request );
|
||||
|
||||
/**
|
||||
* Fetch a session by ID
|
||||
* @param string $id
|
||||
* @param bool $noEmpty Don't return an empty session
|
||||
* @param WebRequest|null $request Corresponding request. Any existing
|
||||
* session associated with this WebRequest object will be overwritten.
|
||||
* @return Session|null
|
||||
*/
|
||||
public function getSessionById( $id, $noEmpty = false, WebRequest $request = null );
|
||||
|
||||
/**
|
||||
* Fetch a new, empty session
|
||||
*
|
||||
* The first provider configured that is able to provide an empty session
|
||||
* will be used.
|
||||
*
|
||||
* @param WebRequest|null $request Corresponding request. Any existing
|
||||
* session associated with this WebRequest object will be overwritten.
|
||||
* @return Session
|
||||
*/
|
||||
public function getEmptySession( WebRequest $request = null );
|
||||
|
||||
/**
|
||||
* Return the HTTP headers that need varying on.
|
||||
*
|
||||
* The return value is such that someone could theoretically do this:
|
||||
* @code
|
||||
* foreach ( $provider->getVaryHeaders() as $header => $options ) {
|
||||
* $outputPage->addVaryHeader( $header, $options );
|
||||
* }
|
||||
* @endcode
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getVaryHeaders();
|
||||
|
||||
/**
|
||||
* Return the list of cookies that need varying on.
|
||||
* @return string[]
|
||||
*/
|
||||
public function getVaryCookies();
|
||||
|
||||
}
|
||||
473
includes/session/SessionProvider.php
Normal file
473
includes/session/SessionProvider.php
Normal file
|
|
@ -0,0 +1,473 @@
|
|||
<?php
|
||||
/**
|
||||
* MediaWiki session provider base class
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program; if not, write to the Free Software Foundation, Inc.,
|
||||
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
* http://www.gnu.org/copyleft/gpl.html
|
||||
*
|
||||
* @file
|
||||
* @ingroup Session
|
||||
*/
|
||||
|
||||
namespace MediaWiki\Session;
|
||||
|
||||
use Psr\Log\LoggerAwareInterface;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Config;
|
||||
use Language;
|
||||
use WebRequest;
|
||||
|
||||
/**
|
||||
* A SessionProvider provides SessionInfo and support for Session
|
||||
*
|
||||
* A SessionProvider is responsible for taking a WebRequest and determining
|
||||
* the authenticated session that it's a part of. It does this by returning an
|
||||
* SessionInfo object with basic information about the session it thinks is
|
||||
* associated with the request, namely the session ID and possibly the
|
||||
* authenticated user the session belongs to.
|
||||
*
|
||||
* The SessionProvider also provides for updating the WebResponse with
|
||||
* information necessary to provide the client with data that the client will
|
||||
* send with later requests, and for populating the Vary and Key headers with
|
||||
* the data necessary to correctly vary the cache on these client requests.
|
||||
*
|
||||
* An important part of the latter is indicating whether it even *can* tell the
|
||||
* client to include such data in future requests, via the persistsSessionId()
|
||||
* and canChangeUser() methods. The cases are (in order of decreasing
|
||||
* commonness):
|
||||
* - Cannot persist ID, no changing User: The request identifies and
|
||||
* authenticates a particular local user, and the client cannot be
|
||||
* instructed to include an arbitrary session ID with future requests. For
|
||||
* example, OAuth or SSL certificate auth.
|
||||
* - Can persist ID and can change User: The client can be instructed to
|
||||
* return at least one piece of arbitrary data, that being the session ID.
|
||||
* The user identity might also be given to the client, otherwise it's saved
|
||||
* in the session data. For example, cookie-based sessions.
|
||||
* - Can persist ID but no changing User: The request uniquely identifies and
|
||||
* authenticates a local user, and the client can be instructed to return an
|
||||
* arbitrary session ID with future requests. For example, HTTP Digest
|
||||
* authentication might somehow use the 'opaque' field as a session ID
|
||||
* (although getting MediaWiki to return 401 responses without breaking
|
||||
* other stuff might be a challenge).
|
||||
* - Cannot persist ID but can change User: I can't think of a way this
|
||||
* would make sense.
|
||||
*
|
||||
* Note that many methods that are technically "cannot persist ID" could be
|
||||
* turned into "can persist ID but not changing User" using a session cookie,
|
||||
* as implemented by ImmutableSessionProviderWithCookie. If doing so, different
|
||||
* session cookie names should be used for different providers to avoid
|
||||
* collisions.
|
||||
*
|
||||
* @ingroup Session
|
||||
* @since 1.27
|
||||
*/
|
||||
abstract class SessionProvider implements SessionProviderInterface, LoggerAwareInterface {
|
||||
|
||||
/** @var LoggerInterface */
|
||||
protected $logger;
|
||||
|
||||
/** @var Config */
|
||||
protected $config;
|
||||
|
||||
/** @var SessionManager */
|
||||
protected $manager;
|
||||
|
||||
/** @var int Session priority. Used for the default newSessionInfo(), but
|
||||
* could be used by subclasses too.
|
||||
*/
|
||||
protected $priority;
|
||||
|
||||
/**
|
||||
* @note To fully initialize a SessionProvider, the setLogger(),
|
||||
* setConfig(), and setManager() methods must be called (and should be
|
||||
* called in that order). Failure to do so is liable to cause things to
|
||||
* fail unexpectedly.
|
||||
*/
|
||||
public function __construct() {
|
||||
$this->priority = SessionInfo::MIN_PRIORITY + 10;
|
||||
}
|
||||
|
||||
public function setLogger( LoggerInterface $logger ) {
|
||||
$this->logger = $logger;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set configuration
|
||||
* @param Config $config
|
||||
*/
|
||||
public function setConfig( Config $config ) {
|
||||
$this->config = $config;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the session manager
|
||||
* @param SessionManager $manager
|
||||
*/
|
||||
public function setManager( SessionManager $manager ) {
|
||||
$this->manager = $manager;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the session manager
|
||||
* @return SessionManager
|
||||
*/
|
||||
public function getManager() {
|
||||
return $this->manager;
|
||||
}
|
||||
|
||||
/**
|
||||
* Provide session info for a request
|
||||
*
|
||||
* If no session exists for the request, return null. Otherwise return an
|
||||
* SessionInfo object identifying the session.
|
||||
*
|
||||
* If multiple SessionProviders provide sessions, the one with highest
|
||||
* priority wins. In case of a tie, an exception is thrown.
|
||||
* SessionProviders are encouraged to make priorities user-configurable
|
||||
* unless only max-priority makes sense.
|
||||
*
|
||||
* @warning This will be called early in the MediaWiki setup process,
|
||||
* before $wgUser, $wgLang, $wgOut, $wgParser, $wgTitle, and corresponding
|
||||
* pieces of the main RequestContext are set up! If you try to use these,
|
||||
* things *will* break.
|
||||
* @note The SessionProvider must not attempt to auto-create users.
|
||||
* MediaWiki will do this later (when it's safe) if the chosen session has
|
||||
* a user with a valid name but no ID.
|
||||
* @protected For use by \\MediaWiki\\Session\\SessionManager only
|
||||
* @param WebRequest $request
|
||||
* @return SessionInfo|null
|
||||
*/
|
||||
abstract public function provideSessionInfo( WebRequest $request );
|
||||
|
||||
/**
|
||||
* Provide session info for a new, empty session
|
||||
*
|
||||
* Return null if such a session cannot be created. This base
|
||||
* implementation assumes that it only makes sense if a session ID can be
|
||||
* persisted and changing users is allowed.
|
||||
*
|
||||
* @protected For use by \\MediaWiki\\Session\\SessionManager only
|
||||
* @param string|null $id ID to force for the new session
|
||||
* @return SessionInfo|null
|
||||
* If non-null, must return true for $info->isIdSafe(); pass true for
|
||||
* $data['idIsSafe'] to ensure this.
|
||||
*/
|
||||
public function newSessionInfo( $id = null ) {
|
||||
if ( $this->canChangeUser() && $this->persistsSessionId() ) {
|
||||
return new SessionInfo( $this->priority, array(
|
||||
'id' => $id,
|
||||
'provider' => $this,
|
||||
'persisted' => false,
|
||||
'idIsSafe' => true,
|
||||
) );
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge saved session provider metadata
|
||||
*
|
||||
* The default implementation checks that anything in both arrays is
|
||||
* identical, then returns $providedMetadata.
|
||||
*
|
||||
* @protected For use by \\MediaWiki\\Session\\SessionManager only
|
||||
* @param array $savedMetadata Saved provider metadata
|
||||
* @param array $providedMetadata Provided provider metadata
|
||||
* @return array Resulting metadata
|
||||
* @throws \UnexpectedValueException If the metadata cannot be merged
|
||||
*/
|
||||
public function mergeMetadata( array $savedMetadata, array $providedMetadata ) {
|
||||
foreach ( $providedMetadata as $k => $v ) {
|
||||
if ( array_key_exists( $k, $savedMetadata ) && $savedMetadata[$k] !== $v ) {
|
||||
throw new \UnexpectedValueException( "Key \"$k\" changed" );
|
||||
}
|
||||
}
|
||||
return $providedMetadata;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate a loaded SessionInfo and refresh provider metadata
|
||||
*
|
||||
* This is similar in purpose to the 'SessionCheckInfo' hook, and also
|
||||
* allows for updating the provider metadata. On failure, the provider is
|
||||
* expected to write an appropriate message to its logger.
|
||||
*
|
||||
* @protected For use by \\MediaWiki\\Session\\SessionManager only
|
||||
* @param SessionInfo $info
|
||||
* @param WebRequest $request
|
||||
* @param array|null &$metadata Provider metadata, may be altered.
|
||||
* @return bool Return false to reject the SessionInfo after all.
|
||||
*/
|
||||
public function refreshSessionInfo( SessionInfo $info, WebRequest $request, &$metadata ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicate whether self::persistSession() can save arbitrary session IDs
|
||||
*
|
||||
* If false, any session passed to self::persistSession() will have an ID
|
||||
* that was originally provided by self::provideSessionInfo().
|
||||
*
|
||||
* If true, the provider may be passed sessions with arbitrary session IDs,
|
||||
* and will be expected to manipulate the request in such a way that future
|
||||
* requests will cause self::provideSessionInfo() to provide a SessionInfo
|
||||
* with that ID.
|
||||
*
|
||||
* For example, a session provider for OAuth would function by matching the
|
||||
* OAuth headers to a particular user, and then would use self::hashToSessionId()
|
||||
* to turn the user and OAuth client ID (and maybe also the user token and
|
||||
* client secret) into a session ID, and therefore can't easily assign that
|
||||
* user+client a different ID. Similarly, a session provider for SSL client
|
||||
* certificates would function by matching the certificate to a particular
|
||||
* user, and then would use self::hashToSessionId() to turn the user and
|
||||
* certificate fingerprint into a session ID, and therefore can't easily
|
||||
* assign a different ID either. On the other hand, a provider that saves
|
||||
* the session ID into a cookie can easily just set the cookie to a
|
||||
* different value.
|
||||
*
|
||||
* @protected For use by \\MediaWiki\\Session\\SessionBackend only
|
||||
* @return bool
|
||||
*/
|
||||
abstract public function persistsSessionId();
|
||||
|
||||
/**
|
||||
* Indicate whether the user associated with the request can be changed
|
||||
*
|
||||
* If false, any session passed to self::persistSession() will have a user
|
||||
* that was originally provided by self::provideSessionInfo(). Further,
|
||||
* self::provideSessionInfo() may only provide sessions that have a user
|
||||
* already set.
|
||||
*
|
||||
* If true, the provider may be passed sessions with arbitrary users, and
|
||||
* will be expected to manipulate the request in such a way that future
|
||||
* requests will cause self::provideSessionInfo() to provide a SessionInfo
|
||||
* with that ID. This can be as simple as not passing any 'userInfo' into
|
||||
* SessionInfo's constructor, in which case SessionInfo will load the user
|
||||
* from the saved session's metadata.
|
||||
*
|
||||
* For example, a session provider for OAuth or SSL client certificates
|
||||
* would function by matching the OAuth headers or certificate to a
|
||||
* particular user, and thus would return false here since it can't
|
||||
* arbitrarily assign those OAuth credentials or that certificate to a
|
||||
* different user. A session provider that shoves information into cookies,
|
||||
* on the other hand, could easily do so.
|
||||
*
|
||||
* @protected For use by \\MediaWiki\\Session\\SessionBackend only
|
||||
* @return bool
|
||||
*/
|
||||
abstract public function canChangeUser();
|
||||
|
||||
/**
|
||||
* Notification that the session ID was reset
|
||||
*
|
||||
* No need to persist here, persistSession() will be called if appropriate.
|
||||
*
|
||||
* @protected For use by \\MediaWiki\\Session\\SessionBackend only
|
||||
* @param SessionBackend $session Session to persist
|
||||
* @param string $oldId Old session ID
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
public function sessionIdWasReset( SessionBackend $session, $oldId ) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Persist a session into a request/response
|
||||
*
|
||||
* For example, you might set cookies for the session's ID, user ID, user
|
||||
* name, and user token on the passed request.
|
||||
*
|
||||
* To correctly persist a user independently of the session ID, the
|
||||
* provider should persist both the user ID (or name, but preferably the
|
||||
* ID) and the user token. When reading the data from the request, it
|
||||
* should construct a User object from the ID/name and then verify that the
|
||||
* User object's token matches the token included in the request. Should
|
||||
* the tokens not match, an anonymous user *must* be passed to
|
||||
* SessionInfo::__construct().
|
||||
*
|
||||
* When persisting a user independently of the session ID,
|
||||
* $session->shouldRememberUser() should be checked first. If this returns
|
||||
* false, the user token *must not* be saved to cookies. The user name
|
||||
* and/or ID may be persisted, and should be used to construct an
|
||||
* unverified UserInfo to pass to SessionInfo::__construct().
|
||||
*
|
||||
* A backend that cannot persist sesison ID or user info should implement
|
||||
* this as a no-op.
|
||||
*
|
||||
* @protected For use by \\MediaWiki\\Session\\SessionBackend only
|
||||
* @param SessionBackend $session Session to persist
|
||||
* @param WebRequest $request Request into which to persist the session
|
||||
*/
|
||||
abstract public function persistSession( SessionBackend $session, WebRequest $request );
|
||||
|
||||
/**
|
||||
* Remove any persisted session from a request/response
|
||||
*
|
||||
* For example, blank and expire any cookies set by self::persistSession().
|
||||
*
|
||||
* A backend that cannot persist sesison ID or user info should implement
|
||||
* this as a no-op.
|
||||
*
|
||||
* @protected For use by \\MediaWiki\\Session\\SessionManager only
|
||||
* @param WebRequest $request Request from which to remove any session data
|
||||
*/
|
||||
abstract public function unpersistSession( WebRequest $request );
|
||||
|
||||
/**
|
||||
* Prevent future sessions for the user
|
||||
*
|
||||
* If the provider is capable of returning a SessionInfo with a verified
|
||||
* UserInfo for the named user in some manner other than by validating
|
||||
* against $user->getToken(), steps must be taken to prevent that from
|
||||
* occurring in the future. This might add the username to a blacklist, or
|
||||
* it might just delete whatever authentication credentials would allow
|
||||
* such a session in the first place (e.g. remove all OAuth grants or
|
||||
* delete record of the SSL client certificate).
|
||||
*
|
||||
* The intention is that the named account will never again be usable for
|
||||
* normal login (i.e. there is no way to undo the prevention of access).
|
||||
*
|
||||
* Note that the passed user name might not exist locally (i.e.
|
||||
* User::idFromName( $username ) === 0); the name should still be
|
||||
* prevented, if applicable.
|
||||
*
|
||||
* @protected For use by \\MediaWiki\\Session\\SessionManager only
|
||||
* @param string $username
|
||||
*/
|
||||
public function preventSessionsForUser( $username ) {
|
||||
if ( !$this->canChangeUser() ) {
|
||||
throw new \BadMethodCallException(
|
||||
__METHOD__ . ' must be implmented when canChangeUser() is false'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the HTTP headers that need varying on.
|
||||
*
|
||||
* The return value is such that someone could theoretically do this:
|
||||
* @code
|
||||
* foreach ( $provider->getVaryHeaders() as $header => $options ) {
|
||||
* $outputPage->addVaryHeader( $header, $options );
|
||||
* }
|
||||
* @endcode
|
||||
*
|
||||
* @protected For use by \\MediaWiki\\Session\\SessionManager only
|
||||
* @return array
|
||||
*/
|
||||
public function getVaryHeaders() {
|
||||
return array();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the list of cookies that need varying on.
|
||||
* @protected For use by \\MediaWiki\\Session\\SessionManager only
|
||||
* @return string[]
|
||||
*/
|
||||
public function getVaryCookies() {
|
||||
return array();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a suggested username for the login form
|
||||
* @protected For use by \\MediaWiki\\Session\\SessionBackend only
|
||||
* @param WebRequest $request
|
||||
* @return string|null
|
||||
*/
|
||||
public function suggestLoginUsername( WebRequest $request ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @note Only override this if it makes sense to instantiate multiple
|
||||
* instances of the provider. Value returned must be unique across
|
||||
* configured providers. If you override this, you'll likely need to
|
||||
* override self::describeMessage() as well.
|
||||
* @return string
|
||||
*/
|
||||
public function __toString() {
|
||||
return get_class( $this );
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a Message identifying this session type
|
||||
*
|
||||
* This default implementation takes the class name, lowercases it,
|
||||
* replaces backslashes with dashes, and prefixes 'sessionprovider-' to
|
||||
* determine the message key. For example, MediaWiki\\Session\\CookieSessionProvider
|
||||
* produces 'sessionprovider-mediawiki-session-cookiesessionprovider'.
|
||||
*
|
||||
* @note If self::__toString() is overridden, this will likely need to be
|
||||
* overridden as well.
|
||||
* @warning This will be called early during MediaWiki startup. Do not
|
||||
* use $wgUser, $wgLang, $wgOut, $wgParser, or their equivalents via
|
||||
* RequestContext from this method!
|
||||
* @return Message
|
||||
*/
|
||||
protected function describeMessage() {
|
||||
return wfMessage(
|
||||
'sessionprovider-' . str_replace( '\\', '-', strtolower( get_class( $this ) ) )
|
||||
);
|
||||
}
|
||||
|
||||
public function describe( Language $lang ) {
|
||||
$msg = $this->describeMessage();
|
||||
$msg->inLanguage( $lang );
|
||||
if ( $msg->isDisabled() ) {
|
||||
$msg = wfMessage( 'sessionprovider-generic', (string)$this )->inLanguage( $lang );
|
||||
}
|
||||
return $msg->plain();
|
||||
}
|
||||
|
||||
public function whyNoSession() {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hash data as a session ID
|
||||
*
|
||||
* Generally this will only be used when self::persistsSessionId() is false and
|
||||
* the provider has to base the session ID on the verified user's identity
|
||||
* or other static data.
|
||||
*
|
||||
* @param string $data
|
||||
* @param string|null $key Defaults to $this->config->get( 'SecretKey' )
|
||||
* @return string
|
||||
*/
|
||||
final protected function hashToSessionId( $data, $key = null ) {
|
||||
if ( !is_string( $data ) ) {
|
||||
throw new \InvalidArgumentException(
|
||||
'$data must be a string, ' . gettype( $data ) . ' was passed'
|
||||
);
|
||||
}
|
||||
if ( $key !== null && !is_string( $key ) ) {
|
||||
throw new \InvalidArgumentException(
|
||||
'$key must be a string or null, ' . gettype( $key ) . ' was passed'
|
||||
);
|
||||
}
|
||||
|
||||
$hash = \MWCryptHash::hmac( "$this\n$data", $key ?: $this->config->get( 'SecretKey' ), false );
|
||||
if ( strlen( $hash ) < 32 ) {
|
||||
// Should never happen, even md5 is 128 bits
|
||||
// @codeCoverageIgnoreStart
|
||||
throw new \UnexpectedValueException( 'Hash fuction returned less than 128 bits' );
|
||||
// @codeCoverageIgnoreEnd
|
||||
}
|
||||
if ( strlen( $hash ) >= 40 ) {
|
||||
$hash = wfBaseConvert( $hash, 16, 32, 32 );
|
||||
}
|
||||
return substr( $hash, -32 );
|
||||
}
|
||||
|
||||
}
|
||||
54
includes/session/SessionProviderInterface.php
Normal file
54
includes/session/SessionProviderInterface.php
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
<?php
|
||||
/**
|
||||
* MediaWiki\Session\Provider interface
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program; if not, write to the Free Software Foundation, Inc.,
|
||||
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
* http://www.gnu.org/copyleft/gpl.html
|
||||
*
|
||||
* @file
|
||||
* @ingroup Session
|
||||
*/
|
||||
|
||||
namespace MediaWiki\Session;
|
||||
|
||||
use Language;
|
||||
|
||||
/**
|
||||
* This exists to make IDEs happy, so they don't see the
|
||||
* internal-but-required-to-be-public methods on SessionProvider.
|
||||
*
|
||||
* @ingroup Session
|
||||
* @since 1.27
|
||||
*/
|
||||
interface SessionProviderInterface {
|
||||
|
||||
/**
|
||||
* Return an identifier for this session type
|
||||
*
|
||||
* @param Language $lang Language to use.
|
||||
* @return string
|
||||
*/
|
||||
public function describe( Language $lang );
|
||||
|
||||
/**
|
||||
* Return a Message for why sessions might not be being persisted.
|
||||
*
|
||||
* For example, "check whether you're blocking our cookies".
|
||||
*
|
||||
* @return Message|null
|
||||
*/
|
||||
public function whyNoSession();
|
||||
|
||||
}
|
||||
187
includes/session/UserInfo.php
Normal file
187
includes/session/UserInfo.php
Normal file
|
|
@ -0,0 +1,187 @@
|
|||
<?php
|
||||
/**
|
||||
* MediaWiki session user info
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program; if not, write to the Free Software Foundation, Inc.,
|
||||
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
* http://www.gnu.org/copyleft/gpl.html
|
||||
*
|
||||
* @file
|
||||
* @ingroup Session
|
||||
*/
|
||||
|
||||
namespace MediaWiki\Session;
|
||||
|
||||
use User;
|
||||
|
||||
/**
|
||||
* Object holding data about a session's user
|
||||
*
|
||||
* In general, this class exists for two purposes:
|
||||
* - User doesn't distinguish between "anonymous user" and "non-anonymous user
|
||||
* that doesn't exist locally", while we do need to.
|
||||
* - We also need the "verified" property described below; tracking it via
|
||||
* another data item to SessionInfo's constructor makes things much more
|
||||
* confusing.
|
||||
*
|
||||
* A UserInfo may be "verified". This indicates that the creator knows that the
|
||||
* request really comes from that user, whether that's by validating OAuth
|
||||
* credentials, SSL client certificates, or by having both the user ID and
|
||||
* token available from cookies.
|
||||
*
|
||||
* An "unverified" UserInfo should be used when it's not possible to
|
||||
* authenticate the user, e.g. the user ID cookie is set but the user Token
|
||||
* cookie isn't. If the Token is available but doesn't match, don't return a
|
||||
* UserInfo at all.
|
||||
*
|
||||
* @ingroup Session
|
||||
* @since 1.27
|
||||
*/
|
||||
final class UserInfo {
|
||||
private $verified = false;
|
||||
|
||||
/** @var User|null */
|
||||
private $user = null;
|
||||
|
||||
private function __construct( User $user = null, $verified ) {
|
||||
if ( $user && $user->isAnon() && !User::isUsableName( $user->getName() ) ) {
|
||||
$this->verified = true;
|
||||
$this->user = null;
|
||||
} else {
|
||||
$this->verified = $verified;
|
||||
$this->user = $user;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an instance for an anonymous (i.e. not logged in) user
|
||||
*
|
||||
* Logged-out users are always "verified".
|
||||
*
|
||||
* @return UserInfo
|
||||
*/
|
||||
public static function newAnonymous() {
|
||||
return new self( null, true );
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an instance for a logged-in user by ID
|
||||
* @param int $id User ID
|
||||
* @param bool $verified True if the user is verified
|
||||
* @return UserInfo
|
||||
*/
|
||||
public static function newFromId( $id, $verified = false ) {
|
||||
$user = User::newFromId( $id );
|
||||
|
||||
// Ensure the ID actually exists
|
||||
$user->load();
|
||||
if ( $user->isAnon() ) {
|
||||
throw new \InvalidArgumentException( 'Invalid ID' );
|
||||
}
|
||||
|
||||
return new self( $user, $verified );
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an instance for a logged-in user by name
|
||||
* @param string $name User name (need not exist locally)
|
||||
* @param bool $verified True if the user is verified
|
||||
* @return UserInfo
|
||||
*/
|
||||
public static function newFromName( $name, $verified = false ) {
|
||||
$user = User::newFromName( $name, 'usable' );
|
||||
if ( !$user ) {
|
||||
throw new \InvalidArgumentException( 'Invalid user name' );
|
||||
}
|
||||
return new self( $user, $verified );
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an instance from an existing User object
|
||||
* @param User $user (need not exist locally)
|
||||
* @param bool $verified True if the user is verified
|
||||
* @return UserInfo
|
||||
*/
|
||||
public static function newFromUser( User $user, $verified = false ) {
|
||||
return new self( $user, $verified );
|
||||
}
|
||||
|
||||
/**
|
||||
* Return whether this is an anonymous user
|
||||
* @return bool
|
||||
*/
|
||||
public function isAnon() {
|
||||
return $this->user === null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return whether this represents a verified user
|
||||
* @return bool
|
||||
*/
|
||||
public function isVerified() {
|
||||
return $this->verified;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the user ID
|
||||
* @note Do not use this to test for anonymous users!
|
||||
* @return int
|
||||
*/
|
||||
public function getId() {
|
||||
return $this->user === null ? 0 : $this->user->getId();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the user name
|
||||
* @return string|null
|
||||
*/
|
||||
public function getName() {
|
||||
return $this->user === null ? null : $this->user->getName();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the user token
|
||||
* @return string|null
|
||||
*/
|
||||
public function getToken() {
|
||||
return $this->user === null || $this->user->getId() === 0 ? null : $this->user->getToken( true );
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a User object
|
||||
* @return User
|
||||
*/
|
||||
public function getUser() {
|
||||
return $this->user === null ? new User : $this->user;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a verified version of this object
|
||||
* @return UserInfo
|
||||
*/
|
||||
public function verified() {
|
||||
return $this->verified ? $this : new self( $this->user, true );
|
||||
}
|
||||
|
||||
public function __toString() {
|
||||
if ( $this->user === null ) {
|
||||
return '<anon>';
|
||||
}
|
||||
return '<' .
|
||||
( $this->verified ? '+' : '-' ) . ':' .
|
||||
$this->getId() . ':' . $this->getName() .
|
||||
'>';
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -21,6 +21,7 @@
|
|||
* @ingroup SpecialPage
|
||||
*/
|
||||
use MediaWiki\Logger\LoggerFactory;
|
||||
use MediaWiki\Session\SessionManager;
|
||||
|
||||
/**
|
||||
* Implements Special:UserLogin
|
||||
|
|
@ -263,9 +264,9 @@ class LoginForm extends SpecialPage {
|
|||
* @param string|null $subPage
|
||||
*/
|
||||
public function execute( $subPage ) {
|
||||
if ( session_id() == '' ) {
|
||||
wfSetupSession();
|
||||
}
|
||||
// Make sure session is persisted
|
||||
$session = MediaWiki\Session\SessionManager::getGlobalSession();
|
||||
$session->persist();
|
||||
|
||||
$this->load();
|
||||
|
||||
|
|
@ -276,6 +277,17 @@ class LoginForm extends SpecialPage {
|
|||
}
|
||||
$this->setHeaders();
|
||||
|
||||
// Make sure it's possible to log in
|
||||
if ( $this->mType !== 'signup' && !$session->canSetUser() ) {
|
||||
throw new ErrorPageError(
|
||||
'cannotloginnow-title',
|
||||
'cannotloginnow-text',
|
||||
array(
|
||||
$session->getProvider()->describe( RequestContext::getMain()->getLanguage() )
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* In the case where the user is already logged in, and was redirected to
|
||||
* the login form from a page that requires login, do not show the login
|
||||
|
|
@ -1376,7 +1388,7 @@ class LoginForm extends SpecialPage {
|
|||
if ( $user->isLoggedIn() ) {
|
||||
$this->mUsername = $user->getName();
|
||||
} else {
|
||||
$this->mUsername = $this->getRequest()->getCookie( 'UserName' );
|
||||
$this->mUsername = $this->getRequest()->getSession()->suggestLoginUsername();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1552,7 +1564,8 @@ class LoginForm extends SpecialPage {
|
|||
function hasSessionCookie() {
|
||||
global $wgDisableCookieCheck;
|
||||
|
||||
return $wgDisableCookieCheck ? true : $this->getRequest()->checkSessionCookie();
|
||||
return $wgDisableCookieCheck ||
|
||||
SessionManager::singleton()->getPersistedSessionId( $this->getRequest() ) !== null;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -1571,7 +1584,7 @@ class LoginForm extends SpecialPage {
|
|||
public static function setLoginToken() {
|
||||
global $wgRequest;
|
||||
// Generate a token directly instead of using $user->getEditToken()
|
||||
// because the latter reuses $_SESSION['wsEditToken']
|
||||
// because the latter reuses wsEditToken in the session
|
||||
$wgRequest->setSessionData( 'wsLoginToken', MWCryptRand::generateHex( 32 ) );
|
||||
}
|
||||
|
||||
|
|
@ -1617,7 +1630,7 @@ class LoginForm extends SpecialPage {
|
|||
$wgCookieSecure = false;
|
||||
}
|
||||
|
||||
wfResetSessionID();
|
||||
MediaWiki\Session\SessionManager::getGlobalSession()->resetId();
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -44,6 +44,18 @@ class SpecialUserlogout extends UnlistedSpecialPage {
|
|||
$this->setHeaders();
|
||||
$this->outputHeader();
|
||||
|
||||
// Make sure it's possible to log out
|
||||
$session = MediaWiki\Session\SessionManager::getGlobalSession();
|
||||
if ( !$session->canSetUser() ) {
|
||||
throw new ErrorPageError(
|
||||
'cannotlogoutnow-title',
|
||||
'cannotlogoutnow-text',
|
||||
array(
|
||||
$session->getProvider()->describe( RequestContext::getMain()->getLanguage() )
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
$user = $this->getUser();
|
||||
$oldName = $user->getName();
|
||||
$user->logout();
|
||||
|
|
|
|||
|
|
@ -390,7 +390,7 @@ class UploadFromUrl extends UploadBase {
|
|||
'userName' => $user->getName(),
|
||||
'leaveMessage' => $this->mAsync == 'async-leavemessage',
|
||||
'ignoreWarnings' => $this->mIgnoreWarnings,
|
||||
'sessionId' => session_id(),
|
||||
'sessionId' => MediaWiki\Session\SessionManager::getGlobalSession()->getId(),
|
||||
'sessionKey' => $sessionKey,
|
||||
) );
|
||||
$job->initializeSessionData();
|
||||
|
|
|
|||
|
|
@ -20,6 +20,8 @@
|
|||
* @file
|
||||
*/
|
||||
|
||||
use MediaWiki\Session\SessionManager;
|
||||
|
||||
/**
|
||||
* String Some punctuation to prevent editing from broken text-mangling proxies.
|
||||
* @ingroup Constants
|
||||
|
|
@ -99,6 +101,7 @@ class User implements IDBAccessObject {
|
|||
'apihighlimits',
|
||||
'applychangetags',
|
||||
'autoconfirmed',
|
||||
'autocreateaccount',
|
||||
'autopatrol',
|
||||
'bigdelete',
|
||||
'block',
|
||||
|
|
@ -227,7 +230,7 @@ class User implements IDBAccessObject {
|
|||
* - 'defaults' anonymous user initialised from class defaults
|
||||
* - 'name' initialise from mName
|
||||
* - 'id' initialise from mId
|
||||
* - 'session' log in from cookies or session if possible
|
||||
* - 'session' log in from session if possible
|
||||
*
|
||||
* Use the User::newFrom*() family of functions to set this.
|
||||
*/
|
||||
|
|
@ -311,14 +314,26 @@ class User implements IDBAccessObject {
|
|||
* @param integer $flags User::READ_* constant bitfield
|
||||
*/
|
||||
public function load( $flags = self::READ_NORMAL ) {
|
||||
global $wgFullyInitialised;
|
||||
|
||||
if ( $this->mLoadedItems === true ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Set it now to avoid infinite recursion in accessors
|
||||
$oldLoadedItems = $this->mLoadedItems;
|
||||
$this->mLoadedItems = true;
|
||||
$this->queryFlagsUsed = $flags;
|
||||
|
||||
// If this is called too early, things are likely to break.
|
||||
if ( $this->mFrom === 'session' && empty( $wgFullyInitialised ) ) {
|
||||
\MediaWiki\Logger\LoggerFactory::getInstance( 'session' )
|
||||
->warning( 'User::loadFromSession called before the end of Setup.php' );
|
||||
$this->loadDefaults();
|
||||
$this->mLoadedItems = $oldLoadedItems;
|
||||
return;
|
||||
}
|
||||
|
||||
switch ( $this->mFrom ) {
|
||||
case 'defaults':
|
||||
$this->loadDefaults();
|
||||
|
|
@ -541,8 +556,8 @@ class User implements IDBAccessObject {
|
|||
}
|
||||
|
||||
/**
|
||||
* Create a new user object using data from session or cookies. If the
|
||||
* login credentials are invalid, the result is an anonymous user.
|
||||
* Create a new user object using data from session. If the login
|
||||
* credentials are invalid, the result is an anonymous user.
|
||||
*
|
||||
* @param WebRequest|null $request Object to use; $wgRequest will be used if omitted.
|
||||
* @return User
|
||||
|
|
@ -662,6 +677,8 @@ class User implements IDBAccessObject {
|
|||
$user->saveSettings();
|
||||
}
|
||||
|
||||
SessionManager::singleton()->preventSessionsForUser( $user->getName() );
|
||||
|
||||
return $user;
|
||||
}
|
||||
|
||||
|
|
@ -1069,8 +1086,9 @@ class User implements IDBAccessObject {
|
|||
$this->mOptionOverrides = null;
|
||||
$this->mOptionsLoaded = false;
|
||||
|
||||
$loggedOut = $this->getRequest()->getCookie( 'LoggedOut' );
|
||||
if ( $loggedOut !== null ) {
|
||||
$request = $this->getRequest();
|
||||
$loggedOut = $request ? $request->getSession()->getLoggedOutTimestamp() : 0;
|
||||
if ( $loggedOut !== 0 ) {
|
||||
$this->mTouched = wfTimestamp( TS_MW, $loggedOut );
|
||||
} else {
|
||||
$this->mTouched = '1'; # Allow any pages to be cached
|
||||
|
|
@ -1115,84 +1133,32 @@ class User implements IDBAccessObject {
|
|||
}
|
||||
|
||||
/**
|
||||
* Load user data from the session or login cookie.
|
||||
* Load user data from the session.
|
||||
*
|
||||
* @return bool True if the user is logged in, false otherwise.
|
||||
*/
|
||||
private function loadFromSession() {
|
||||
// Deprecated hook
|
||||
$result = null;
|
||||
Hooks::run( 'UserLoadFromSession', array( $this, &$result ) );
|
||||
Hooks::run( 'UserLoadFromSession', array( $this, &$result ), '1.27' );
|
||||
if ( $result !== null ) {
|
||||
return $result;
|
||||
}
|
||||
|
||||
$request = $this->getRequest();
|
||||
|
||||
$cookieId = $request->getCookie( 'UserID' );
|
||||
$sessId = $request->getSessionData( 'wsUserID' );
|
||||
|
||||
if ( $cookieId !== null ) {
|
||||
$sId = intval( $cookieId );
|
||||
if ( $sessId !== null && $cookieId != $sessId ) {
|
||||
wfDebugLog( 'loginSessions', "Session user ID ($sessId) and
|
||||
cookie user ID ($sId) don't match!" );
|
||||
return false;
|
||||
}
|
||||
$request->setSessionData( 'wsUserID', $sId );
|
||||
} elseif ( $sessId !== null && $sessId != 0 ) {
|
||||
$sId = $sessId;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ( $request->getSessionData( 'wsUserName' ) !== null ) {
|
||||
$sName = $request->getSessionData( 'wsUserName' );
|
||||
} elseif ( $request->getCookie( 'UserName' ) !== null ) {
|
||||
$sName = $request->getCookie( 'UserName' );
|
||||
$request->setSessionData( 'wsUserName', $sName );
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
|
||||
$proposedUser = User::newFromId( $sId );
|
||||
if ( !$proposedUser->isLoggedIn() ) {
|
||||
// Not a valid ID
|
||||
return false;
|
||||
}
|
||||
|
||||
global $wgBlockDisablesLogin;
|
||||
if ( $wgBlockDisablesLogin && $proposedUser->isBlocked() ) {
|
||||
// User blocked and we've disabled blocked user logins
|
||||
return false;
|
||||
}
|
||||
|
||||
if ( $request->getSessionData( 'wsToken' ) ) {
|
||||
$passwordCorrect =
|
||||
( $proposedUser->getToken( false ) === $request->getSessionData( 'wsToken' ) );
|
||||
$from = 'session';
|
||||
} elseif ( $request->getCookie( 'Token' ) ) {
|
||||
# Get the token from DB/cache and clean it up to remove garbage padding.
|
||||
# This deals with historical problems with bugs and the default column value.
|
||||
$token = rtrim( $proposedUser->getToken( false ) ); // correct token
|
||||
// Make comparison in constant time (bug 61346)
|
||||
$passwordCorrect = strlen( $token )
|
||||
&& hash_equals( $token, $request->getCookie( 'Token' ) );
|
||||
$from = 'cookie';
|
||||
} else {
|
||||
// No session or persistent login cookie
|
||||
return false;
|
||||
}
|
||||
|
||||
if ( ( $sName === $proposedUser->getName() ) && $passwordCorrect ) {
|
||||
$this->loadFromUserObject( $proposedUser );
|
||||
$request->setSessionData( 'wsToken', $this->mToken );
|
||||
wfDebug( "User: logged in from $from\n" );
|
||||
// MediaWiki\Session\Session already did the necessary authentication of the user
|
||||
// returned here, so just use it if applicable.
|
||||
$session = $this->getRequest()->getSession();
|
||||
$user = $session->getUser();
|
||||
if ( $user->isLoggedIn() ) {
|
||||
$this->loadFromUserObject( $user );
|
||||
// Other code expects these to be set in the session, so set them.
|
||||
$session->set( 'wsUserID', $this->getId() );
|
||||
$session->set( 'wsUserName', $this->getName() );
|
||||
$session->set( 'wsToken', $this->mToken );
|
||||
return true;
|
||||
} else {
|
||||
// Invalid credentials
|
||||
wfDebug( "User: can't log in from $from, invalid credentials\n" );
|
||||
return false;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -3502,6 +3468,7 @@ class User implements IDBAccessObject {
|
|||
/**
|
||||
* Set a cookie on the user's client. Wrapper for
|
||||
* WebResponse::setCookie
|
||||
* @deprecated since 1.27
|
||||
* @param string $name Name of the cookie to set
|
||||
* @param string $value Value to set
|
||||
* @param int $exp Expiration time, as a UNIX time value;
|
||||
|
|
@ -3517,6 +3484,7 @@ class User implements IDBAccessObject {
|
|||
protected function setCookie(
|
||||
$name, $value, $exp = 0, $secure = null, $params = array(), $request = null
|
||||
) {
|
||||
wfDeprecated( __METHOD__, '1.27' );
|
||||
if ( $request === null ) {
|
||||
$request = $this->getRequest();
|
||||
}
|
||||
|
|
@ -3526,6 +3494,7 @@ class User implements IDBAccessObject {
|
|||
|
||||
/**
|
||||
* Clear a cookie on the user's client
|
||||
* @deprecated since 1.27
|
||||
* @param string $name Name of the cookie to clear
|
||||
* @param bool $secure
|
||||
* true: Force setting the secure attribute when setting the cookie
|
||||
|
|
@ -3534,6 +3503,7 @@ class User implements IDBAccessObject {
|
|||
* @param array $params Array of options sent passed to WebResponse::setcookie()
|
||||
*/
|
||||
protected function clearCookie( $name, $secure = null, $params = array() ) {
|
||||
wfDeprecated( __METHOD__, '1.27' );
|
||||
$this->setCookie( $name, '', time() - 86400, $secure, $params );
|
||||
}
|
||||
|
||||
|
|
@ -3544,6 +3514,7 @@ class User implements IDBAccessObject {
|
|||
*
|
||||
* @see User::setCookie
|
||||
*
|
||||
* @deprecated since 1.27
|
||||
* @param string $name Name of the cookie to set
|
||||
* @param string $value Value to set
|
||||
* @param bool $secure
|
||||
|
|
@ -3554,6 +3525,8 @@ class User implements IDBAccessObject {
|
|||
protected function setExtendedLoginCookie( $name, $value, $secure ) {
|
||||
global $wgExtendedLoginCookieExpiration, $wgCookieExpiration;
|
||||
|
||||
wfDeprecated( __METHOD__, '1.27' );
|
||||
|
||||
$exp = time();
|
||||
$exp += $wgExtendedLoginCookieExpiration !== null
|
||||
? $wgExtendedLoginCookieExpiration
|
||||
|
|
@ -3563,7 +3536,7 @@ class User implements IDBAccessObject {
|
|||
}
|
||||
|
||||
/**
|
||||
* Set the default cookies for this session on the user's client.
|
||||
* Persist this user's session (e.g. set cookies)
|
||||
*
|
||||
* @param WebRequest|null $request WebRequest object to use; $wgRequest will be used if null
|
||||
* is passed.
|
||||
|
|
@ -3571,72 +3544,36 @@ class User implements IDBAccessObject {
|
|||
* @param bool $rememberMe Whether to add a Token cookie for elongated sessions
|
||||
*/
|
||||
public function setCookies( $request = null, $secure = null, $rememberMe = false ) {
|
||||
global $wgExtendedLoginCookies;
|
||||
|
||||
if ( $request === null ) {
|
||||
$request = $this->getRequest();
|
||||
}
|
||||
|
||||
$this->load();
|
||||
if ( 0 == $this->mId ) {
|
||||
return;
|
||||
}
|
||||
if ( !$this->mToken ) {
|
||||
// When token is empty or NULL generate a new one and then save it to the database
|
||||
// This allows a wiki to re-secure itself after a leak of it's user table or $wgSecretKey
|
||||
// Simply by setting every cell in the user_token column to NULL and letting them be
|
||||
// regenerated as users log back into the wiki.
|
||||
$this->setToken();
|
||||
if ( !wfReadOnly() ) {
|
||||
$this->saveSettings();
|
||||
|
||||
$session = $this->getRequest()->getSession();
|
||||
if ( $request && $session->getRequest() !== $request ) {
|
||||
$session = $session->sessionWithRequest( $request );
|
||||
}
|
||||
$delay = $session->delaySave();
|
||||
|
||||
if ( !$session->getUser()->equals( $this ) ) {
|
||||
if ( !$session->canSetUser() ) {
|
||||
\MediaWiki\Logger\LoggerFactory::getInstance( 'session' )
|
||||
->warning( __METHOD__ .
|
||||
": Cannot save user \"$this\" to a user \"{$session->getUser()}\"'s immutable session"
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
$session = array(
|
||||
'wsUserID' => $this->mId,
|
||||
'wsToken' => $this->mToken,
|
||||
'wsUserName' => $this->getName()
|
||||
);
|
||||
$cookies = array(
|
||||
'UserID' => $this->mId,
|
||||
'UserName' => $this->getName(),
|
||||
);
|
||||
if ( $rememberMe ) {
|
||||
$cookies['Token'] = $this->mToken;
|
||||
} else {
|
||||
$cookies['Token'] = false;
|
||||
$session->setUser( $this );
|
||||
}
|
||||
|
||||
Hooks::run( 'UserSetCookies', array( $this, &$session, &$cookies ) );
|
||||
|
||||
foreach ( $session as $name => $value ) {
|
||||
$request->setSessionData( $name, $value );
|
||||
}
|
||||
foreach ( $cookies as $name => $value ) {
|
||||
if ( $value === false ) {
|
||||
$this->clearCookie( $name );
|
||||
} elseif ( $rememberMe && in_array( $name, $wgExtendedLoginCookies ) ) {
|
||||
$this->setExtendedLoginCookie( $name, $value, $secure );
|
||||
} else {
|
||||
$this->setCookie( $name, $value, 0, $secure, array(), $request );
|
||||
}
|
||||
$session->setRememberUser( $rememberMe );
|
||||
if ( $secure !== null ) {
|
||||
$session->setForceHTTPS( $secure );
|
||||
}
|
||||
|
||||
/**
|
||||
* If wpStickHTTPS was selected, also set an insecure cookie that
|
||||
* will cause the site to redirect the user to HTTPS, if they access
|
||||
* it over HTTP. Bug 29898. Use an un-prefixed cookie, so it's the same
|
||||
* as the one set by centralauth (bug 53538). Also set it to session, or
|
||||
* standard time setting, based on if rememberme was set.
|
||||
*/
|
||||
if ( $request->getCheck( 'wpStickHTTPS' ) || $this->requiresHTTPS() ) {
|
||||
$this->setCookie(
|
||||
'forceHTTPS',
|
||||
'true',
|
||||
$rememberMe ? 0 : null,
|
||||
false,
|
||||
array( 'prefix' => '' ) // no prefix
|
||||
);
|
||||
}
|
||||
$session->persist();
|
||||
|
||||
ScopedCallback::consume( $delay );
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -3649,20 +3586,29 @@ class User implements IDBAccessObject {
|
|||
}
|
||||
|
||||
/**
|
||||
* Clear the user's cookies and session, and reset the instance cache.
|
||||
* Clear the user's session, and reset the instance cache.
|
||||
* @see logout()
|
||||
*/
|
||||
public function doLogout() {
|
||||
$this->clearInstanceCache( 'defaults' );
|
||||
|
||||
$this->getRequest()->setSessionData( 'wsUserID', 0 );
|
||||
|
||||
$this->clearCookie( 'UserID' );
|
||||
$this->clearCookie( 'Token' );
|
||||
$this->clearCookie( 'forceHTTPS', false, array( 'prefix' => '' ) );
|
||||
|
||||
// Remember when user logged out, to prevent seeing cached pages
|
||||
$this->setCookie( 'LoggedOut', time(), time() + 86400 );
|
||||
$session = $this->getRequest()->getSession();
|
||||
if ( !$session->canSetUser() ) {
|
||||
\MediaWiki\Logger\LoggerFactory::getInstance( 'session' )
|
||||
->warning( __METHOD__ . ": Cannot log out of an immutable session" );
|
||||
} elseif ( !$session->getUser()->equals( $this ) ) {
|
||||
\MediaWiki\Logger\LoggerFactory::getInstance( 'session' )
|
||||
->warning( __METHOD__ .
|
||||
": Cannot log user \"$this\" out of a user \"{$session->getUser()}\"'s session"
|
||||
);
|
||||
// But we still may as well make this user object anon
|
||||
$this->clearInstanceCache( 'defaults' );
|
||||
} else {
|
||||
$this->clearInstanceCache( 'defaults' );
|
||||
$delay = $session->delaySave();
|
||||
$session->setLoggedOutTimestamp( time() );
|
||||
$session->setUser( new User );
|
||||
$session->set( 'wsUserID', 0 ); // Other code expects this
|
||||
ScopedCallback::consume( $delay );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -389,6 +389,8 @@
|
|||
"virus-scanfailed": "scan failed (code $1)",
|
||||
"virus-unknownscanner": "unknown antivirus:",
|
||||
"logouttext": "<strong>You are now logged out.</strong>\n\nNote that some pages may continue to be displayed as if you were still logged in, until you clear your browser cache.",
|
||||
"cannotlogoutnow-title": "Cannot log out now",
|
||||
"cannotlogoutnow-text": "Logging out is not possible when using $1.",
|
||||
"welcomeuser": "Welcome, $1!",
|
||||
"welcomecreation-msg": "Your account has been created.\nYou can change your {{SITENAME}} [[Special:Preferences|preferences]] if you wish.",
|
||||
"yourname": "Username:",
|
||||
|
|
@ -406,6 +408,8 @@
|
|||
"remembermypassword": "Remember my login on this browser (for a maximum of $1 {{PLURAL:$1|day|days}})",
|
||||
"userlogin-remembermypassword": "Keep me logged in",
|
||||
"userlogin-signwithsecure": "Use secure connection",
|
||||
"cannotloginnow-title": "Cannot log in now",
|
||||
"cannotloginnow-text": "Logging in is not possible when using $1.",
|
||||
"yourdomainname": "Your domain:",
|
||||
"password-change-forbidden": "You cannot change passwords on this wiki.",
|
||||
"externaldberror": "There was either an authentication database error or you are not allowed to update your external account.",
|
||||
|
|
@ -1112,6 +1116,7 @@
|
|||
"right-createpage": "Create pages (which are not discussion pages)",
|
||||
"right-createtalk": "Create discussion pages",
|
||||
"right-createaccount": "Create new user accounts",
|
||||
"right-autocreateaccount": "Automatically log in with an external user account",
|
||||
"right-minoredit": "Mark edits as minor",
|
||||
"right-move": "Move pages",
|
||||
"right-move-subpages": "Move pages with their subpages",
|
||||
|
|
@ -1190,6 +1195,7 @@
|
|||
"action-createpage": "create pages",
|
||||
"action-createtalk": "create discussion pages",
|
||||
"action-createaccount": "create this user account",
|
||||
"action-autocreateaccount": "automatically create this external user account",
|
||||
"action-history": "view the history of this page",
|
||||
"action-minoredit": "mark this edit as minor",
|
||||
"action-move": "move this page",
|
||||
|
|
@ -3924,5 +3930,9 @@
|
|||
"mw-widgets-dateinput-placeholder-month": "YYYY-MM",
|
||||
"mw-widgets-titleinput-description-new-page": "page does not exist yet",
|
||||
"mw-widgets-titleinput-description-redirect": "redirect to $1",
|
||||
"api-error-blacklisted": "Please choose a different, descriptive title."
|
||||
"api-error-blacklisted": "Please choose a different, descriptive title.",
|
||||
"sessionmanager-tie": "Cannot combine multiple request authentication types: $1.",
|
||||
"sessionprovider-generic": "$1 sessions",
|
||||
"sessionprovider-mediawiki-session-cookiesessionprovider": "cookie-based sessions",
|
||||
"sessionprovider-nocookies": "Cookies may be disabled. Ensure you have cookies enabled and start again."
|
||||
}
|
||||
|
|
|
|||
|
|
@ -564,6 +564,8 @@
|
|||
"virus-scanfailed": "Used as error message. \"scan\" stands for \"virus scan\". Parameters:\n* $1 - exit code of virus scanner",
|
||||
"virus-unknownscanner": "Used as error message. This message is followed by the virus scanner name.",
|
||||
"logouttext": "Log out message. Parameters:\n* $1 - (Unused) an URL to [[Special:Userlogin]] containing <code>returnto</code> and <code>returntoquery</code> parameters",
|
||||
"cannotlogoutnow-title": "Error page title shown when logging out is not possible.",
|
||||
"cannotlogoutnow-text": "Error page text shown when logging out is not possible. Parameters:\n* $1 - Session type in use that makes it not possible to log out, from a message like {{msg-mw|sessionprovider-mediawiki-session-cookiesessionprovider}}.",
|
||||
"welcomeuser": "Text for a welcome heading that users see after registering a user account.\n\nParameters:\n* $1 - the username of the new user. See [[phab:T44215]]",
|
||||
"welcomecreation-msg": "A welcome message users see after registering a user account, following a welcomeuser heading.\n\nParameters:\n* $1 - (Unused) the username of the new user.\n\nReplaces [[MediaWiki:welcomecreation|welcomecreation]] in 1.21wmf5, see [[phab:T44215]]",
|
||||
"yourname": "Since 1.22 no longer used in core, but used by some extensions.\n{{Identical|Username}}",
|
||||
|
|
@ -581,6 +583,8 @@
|
|||
"remembermypassword": "Used as checkbox label on [[Special:ChangePassword]]. Parameters:\n* $1 - number of days\n{{Identical|Remember my login on this computer}}",
|
||||
"userlogin-remembermypassword": "The text for a check box in [[Special:UserLogin]].",
|
||||
"userlogin-signwithsecure": "Text of link to HTTPS login form.\n\nSee example: [[Special:UserLogin]]",
|
||||
"cannotloginnow-title": "Error page title shown when logging in is not possible.",
|
||||
"cannotloginnow-text": "Error page text shown when logging in is not possible. Parameters:\n* $1 - Session type in use that makes it not possible to log in, from a message like {{msg-mw|sessionprovider-mediawiki-session-cookiesessionprovider}}.",
|
||||
"yourdomainname": "Used as label for listbox.",
|
||||
"password-change-forbidden": "Error message shown when an external authentication source does not allow the password to be changed.",
|
||||
"externaldberror": "This message is thrown when a valid attempt to change the wiki password for a user fails because of a database error or an error from an external system.",
|
||||
|
|
@ -1287,6 +1291,7 @@
|
|||
"right-createpage": "{{doc-right|createpage}}\nBasic right to create pages. The right to edit discussion/talk pages is {{msg-mw|right-createtalk}}.",
|
||||
"right-createtalk": "{{doc-right|createtalk}}\nBasic right to create discussion/talk pages. The right to edit other pages is {{msg-mw|right-createpage}}.",
|
||||
"right-createaccount": "{{doc-right|createaccount}}\nThe right to [[Special:CreateAccount|create a user account]].",
|
||||
"right-autocreateaccount": "{{doc-right|autocreateaccount}}\nThe right to automatically create an account from an external source (e.g. CentralAuth).",
|
||||
"right-minoredit": "{{doc-right|minoredit}}\nThe right to use the \"This is a minor edit\" checkbox. See {{msg-mw|minoredit}} for the message used for that checkbox.",
|
||||
"right-move": "{{doc-right|move}}\nThe right to move any page that is not protected from moving.\n{{Identical|Move page}}",
|
||||
"right-move-subpages": "{{doc-right|move-subpages}}",
|
||||
|
|
@ -1365,6 +1370,7 @@
|
|||
"action-createpage": "{{Doc-action|createpage}}\n{{Identical|Create page}}",
|
||||
"action-createtalk": "{{Doc-action|createtalk}}",
|
||||
"action-createaccount": "{{Doc-action|createaccount}}",
|
||||
"action-autocreateaccount": "{{Doc-action|autocreateaccount}}",
|
||||
"action-history": "{{Doc-action|history}}",
|
||||
"action-minoredit": "{{Doc-action|minoredit}}",
|
||||
"action-move": "{{Doc-action|move}}",
|
||||
|
|
@ -4099,5 +4105,9 @@
|
|||
"mw-widgets-dateinput-placeholder-month": "Placeholder displayed in a date input field when it's empty, representing a date format with 4 digits for year and 2 digits for month, separated with hyphens (without a day). This should be uppercase, if possible, and must not include any additional explanations. If there is no good way to translate it, make this message blank.",
|
||||
"mw-widgets-titleinput-description-new-page": "Description label for a new page in the title input widget.",
|
||||
"mw-widgets-titleinput-description-redirect": "Description label for a redirect in the title input widget.",
|
||||
"api-error-blacklisted": "Used as error message.\n\nFollowed by the link {{msg-mw|Mwe-upwiz-feedback-blacklist-info-prompt}}."
|
||||
"api-error-blacklisted": "Used as error message.\n\nFollowed by the link {{msg-mw|Mwe-upwiz-feedback-blacklist-info-prompt}}.",
|
||||
"sessionmanager-tie": "Used as an error message when multiple session sources are tied in priority.\n\nParameters:\n* $1 - List of dession type descriptions, from messages like {{msg-mw|sessionprovider-mediawiki-session-cookiesessionprovider}}.",
|
||||
"sessionprovider-generic": "Used to create a generic session type description when one isn't provided via the proper message. Should be phrased to make sense when added to a message such as {{msg-mw|cannotloginnow-text}}.\n\nParameters:\n* $1 - PHP classname.",
|
||||
"sessionprovider-mediawiki-session-cookiesessionprovider": "Description of the sessions provided by the CookieSessionProvider class, which use HTTP cookies. Should be phrased to make sense when added to a message such as {{msg-mw|cannotloginnow-text}}.",
|
||||
"sessionprovider-nocookies": "Used to inform the user that sessions may be missing due to lack of cookies."
|
||||
}
|
||||
|
|
|
|||
|
|
@ -49,6 +49,7 @@ $wgAutoloadClasses += array(
|
|||
|
||||
# tests/phpunit/includes
|
||||
'TestingAccessWrapper' => "$testDir/phpunit/includes/TestingAccessWrapper.php",
|
||||
'TestLogger' => "$testDir/phpunit/includes/TestLogger.php",
|
||||
|
||||
# tests/phpunit/includes/api
|
||||
'ApiFormatTestBase' => "$testDir/phpunit/includes/api/format/ApiFormatTestBase.php",
|
||||
|
|
@ -94,6 +95,10 @@ $wgAutoloadClasses += array(
|
|||
'ResourceLoaderImageModuleTestable' =>
|
||||
"$testDir/phpunit/includes/resourceloader/ResourceLoaderImageModuleTest.php",
|
||||
|
||||
# tests/phpunit/includes/session
|
||||
'MediaWiki\\Session\\TestBagOStuff' => "$testDir/phpunit/includes/session/TestBagOStuff.php",
|
||||
'MediaWiki\\Session\\TestUtils' => "$testDir/phpunit/includes/session/TestUtils.php",
|
||||
|
||||
# tests/phpunit/includes/specials
|
||||
'SpecialPageTestBase' => "$testDir/phpunit/includes/specials/SpecialPageTestBase.php",
|
||||
|
||||
|
|
@ -118,6 +123,9 @@ $wgAutoloadClasses += array(
|
|||
'MockSvgHandler' => "$testDir/phpunit/mocks/media/MockSvgHandler.php",
|
||||
'MockDjVuHandler' => "$testDir/phpunit/mocks/media/MockDjVuHandler.php",
|
||||
'MockWebRequest' => "$testDir/phpunit/mocks/MockWebRequest.php",
|
||||
'MediaWiki\\Session\\DummySessionBackend'
|
||||
=> "$testDir/phpunit/mocks/session/DummySessionBackend.php",
|
||||
'DummySessionProvider' => "$testDir/phpunit/mocks/session/DummySessionProvider.php",
|
||||
|
||||
# tests/parser
|
||||
'NewParserTest' => "$testDir/phpunit/includes/parser/NewParserTest.php",
|
||||
|
|
|
|||
|
|
@ -221,6 +221,8 @@ abstract class MediaWikiTestCase extends PHPUnit_Framework_TestCase {
|
|||
}
|
||||
|
||||
protected function tearDown() {
|
||||
global $wgRequest;
|
||||
|
||||
$status = ob_get_status();
|
||||
if ( isset( $status['name'] ) &&
|
||||
$status['name'] === 'MediaWikiTestCase::wfResetOutputBuffersBarrier'
|
||||
|
|
@ -252,6 +254,12 @@ abstract class MediaWikiTestCase extends PHPUnit_Framework_TestCase {
|
|||
$this->mwGlobals = array();
|
||||
RequestContext::resetMain();
|
||||
MediaHandler::resetCache();
|
||||
if ( session_id() !== '' ) {
|
||||
session_write_close();
|
||||
session_id( '' );
|
||||
}
|
||||
$wgRequest = new FauxRequest();
|
||||
MediaWiki\Session\SessionManager::resetCache();
|
||||
|
||||
$phpErrorLevel = intval( ini_get( 'error_reporting' ) );
|
||||
|
||||
|
|
@ -509,6 +517,13 @@ abstract class MediaWikiTestCase extends PHPUnit_Framework_TestCase {
|
|||
false,
|
||||
$user
|
||||
);
|
||||
|
||||
// doEditContent() probably started the session via
|
||||
// User::loadFromSession(). Close it now.
|
||||
if ( session_id() !== '' ) {
|
||||
session_write_close();
|
||||
session_id( '' );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
105
tests/phpunit/includes/TestLogger.php
Normal file
105
tests/phpunit/includes/TestLogger.php
Normal file
|
|
@ -0,0 +1,105 @@
|
|||
<?php
|
||||
/**
|
||||
* Testing logger
|
||||
*
|
||||
* Copyright (C) 2015 Brad Jorsch <bjorsch@wikimedia.org>
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program; if not, write to the Free Software Foundation, Inc.,
|
||||
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
* http://www.gnu.org/copyleft/gpl.html
|
||||
*
|
||||
* @file
|
||||
* @author Brad Jorsch <bjorsch@wikimedia.org>
|
||||
*/
|
||||
|
||||
use Psr\Log\LogLevel;
|
||||
|
||||
/**
|
||||
* A logger that may be configured to either buffer logs or to print them to
|
||||
* the output where PHPUnit will complain about them.
|
||||
*
|
||||
* @since 1.27
|
||||
*/
|
||||
class TestLogger extends \Psr\Log\AbstractLogger {
|
||||
private $collect = false;
|
||||
private $buffer = array();
|
||||
private $filter = null;
|
||||
|
||||
/**
|
||||
* @param bool $collect Whether to collect logs
|
||||
* @param callable $filter Filter logs before collecting/printing. Signature is
|
||||
* string|null function ( string $message, string $level );
|
||||
*/
|
||||
public function __construct( $collect = false, $filter = null ) {
|
||||
$this->collect = $collect;
|
||||
$this->filter = $filter;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the "collect" flag
|
||||
* @param bool $collect
|
||||
*/
|
||||
public function setCollect( $collect ) {
|
||||
$this->collect = $collect;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the collected logs
|
||||
* @return array Array of array( string $level, string $message )
|
||||
*/
|
||||
public function getBuffer() {
|
||||
return $this->buffer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear the collected log buffer
|
||||
*/
|
||||
public function clearBuffer() {
|
||||
$this->buffer = array();
|
||||
}
|
||||
|
||||
public function log( $level, $message, array $context = array() ) {
|
||||
$message = trim( $message );
|
||||
|
||||
if ( $this->filter ) {
|
||||
$message = call_user_func( $this->filter, $message, $level );
|
||||
if ( $message === null ) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if ( $this->collect ) {
|
||||
$this->buffer[] = array( $level, $message );
|
||||
} else {
|
||||
switch ( $level ) {
|
||||
case LogLevel::DEBUG:
|
||||
case LogLevel::INFO:
|
||||
case LogLevel::NOTICE:
|
||||
trigger_error( "LOG[$level]: $message", E_USER_NOTICE );
|
||||
break;
|
||||
|
||||
case LogLevel::WARNING:
|
||||
trigger_error( "LOG[$level]: $message", E_USER_WARNING );
|
||||
break;
|
||||
|
||||
case LogLevel::ERROR:
|
||||
case LogLevel::CRITICAL:
|
||||
case LogLevel::ALERT:
|
||||
case LogLevel::EMERGENCY:
|
||||
trigger_error( "LOG[$level]: $message", E_USER_ERROR );
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -97,7 +97,8 @@ class ApiMainTest extends ApiTestCase {
|
|||
$request->setHeaders( $headers );
|
||||
$request->response()->statusHeader( 200 ); // Why doesn't it default?
|
||||
|
||||
$api = new ApiMain( $request );
|
||||
$context = $this->apiContext->newTestContext( $request, null );
|
||||
$api = new ApiMain( $context );
|
||||
$priv = TestingAccessWrapper::newFromObject( $api );
|
||||
$priv->mInternalMode = false;
|
||||
|
||||
|
|
|
|||
|
|
@ -47,11 +47,7 @@ abstract class ApiTestCase extends MediaWikiLangTestCase {
|
|||
|
||||
protected function tearDown() {
|
||||
// Avoid leaking session over tests
|
||||
if ( session_id() != '' ) {
|
||||
global $wgUser;
|
||||
$wgUser->logout();
|
||||
session_destroy();
|
||||
}
|
||||
MediaWiki\Session\SessionManager::getGlobalSession()->clear();
|
||||
|
||||
parent::tearDown();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,8 +15,6 @@ abstract class ApiTestCaseUpload extends ApiTestCase {
|
|||
'wgEnableAPI' => true,
|
||||
) );
|
||||
|
||||
wfSetupSession();
|
||||
|
||||
$this->clearFakeUploads();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -37,6 +37,14 @@ class RequestContextTest extends MediaWikiTestCase {
|
|||
* @covers RequestContext::importScopedSession
|
||||
*/
|
||||
public function testImportScopedSession() {
|
||||
// Make sure session handling is started
|
||||
if ( !MediaWiki\Session\PHPSessionHandler::isInstalled() ) {
|
||||
MediaWiki\Session\PHPSessionHandler::install(
|
||||
MediaWiki\Session\SessionManager::singleton()
|
||||
);
|
||||
}
|
||||
$oldSessionId = session_id();
|
||||
|
||||
$context = RequestContext::getMain();
|
||||
|
||||
$oInfo = $context->exportSession();
|
||||
|
|
@ -76,7 +84,16 @@ class RequestContextTest extends MediaWikiTestCase {
|
|||
$context->getRequest()->getAllHeaders(),
|
||||
"Correct context headers."
|
||||
);
|
||||
$this->assertEquals( $sinfo['sessionId'], session_id(), "Correct context session ID." );
|
||||
$this->assertEquals(
|
||||
$sinfo['sessionId'],
|
||||
MediaWiki\Session\SessionManager::getGlobalSession()->getId(),
|
||||
"Correct context session ID."
|
||||
);
|
||||
if ( \MediaWiki\Session\PhpSessionHandler::isEnabled() ) {
|
||||
$this->assertEquals( $sinfo['sessionId'], session_id(), "Correct context session ID." );
|
||||
} else {
|
||||
$this->assertEquals( $oldSessionId, session_id(), "Unchanged PHP session ID." );
|
||||
}
|
||||
$this->assertEquals( true, $context->getUser()->isLoggedIn(), "Correct context user." );
|
||||
$this->assertEquals( $sinfo['userId'], $context->getUser()->getId(), "Correct context user ID." );
|
||||
$this->assertEquals(
|
||||
|
|
|
|||
726
tests/phpunit/includes/session/CookieSessionProviderTest.php
Normal file
726
tests/phpunit/includes/session/CookieSessionProviderTest.php
Normal file
|
|
@ -0,0 +1,726 @@
|
|||
<?php
|
||||
|
||||
namespace MediaWiki\Session;
|
||||
|
||||
use MediaWikiTestCase;
|
||||
use User;
|
||||
|
||||
/**
|
||||
* @group Session
|
||||
* @group Database
|
||||
* @covers MediaWiki\Session\CookieSessionProvider
|
||||
*/
|
||||
class CookieSessionProviderTest extends MediaWikiTestCase {
|
||||
|
||||
private function getConfig() {
|
||||
global $wgCookieExpiration;
|
||||
return new \HashConfig( array(
|
||||
'CookiePrefix' => 'CookiePrefix',
|
||||
'CookiePath' => 'CookiePath',
|
||||
'CookieDomain' => 'CookieDomain',
|
||||
'CookieSecure' => true,
|
||||
'CookieHttpOnly' => true,
|
||||
'SessionName' => false,
|
||||
'ExtendedLoginCookies' => array( 'UserID', 'Token' ),
|
||||
'ExtendedLoginCookieExpiration' => $wgCookieExpiration * 2,
|
||||
) );
|
||||
}
|
||||
|
||||
public function testConstructor() {
|
||||
try {
|
||||
new CookieSessionProvider();
|
||||
$this->fail( 'Expected exception not thrown' );
|
||||
} catch ( \InvalidArgumentException $ex ) {
|
||||
$this->assertSame(
|
||||
'MediaWiki\\Session\\CookieSessionProvider::__construct: priority must be specified',
|
||||
$ex->getMessage()
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
new CookieSessionProvider( array( 'priority' => 'foo' ) );
|
||||
$this->fail( 'Expected exception not thrown' );
|
||||
} catch ( \InvalidArgumentException $ex ) {
|
||||
$this->assertSame(
|
||||
'MediaWiki\\Session\\CookieSessionProvider::__construct: Invalid priority',
|
||||
$ex->getMessage()
|
||||
);
|
||||
}
|
||||
try {
|
||||
new CookieSessionProvider( array( 'priority' => SessionInfo::MIN_PRIORITY - 1 ) );
|
||||
$this->fail( 'Expected exception not thrown' );
|
||||
} catch ( \InvalidArgumentException $ex ) {
|
||||
$this->assertSame(
|
||||
'MediaWiki\\Session\\CookieSessionProvider::__construct: Invalid priority',
|
||||
$ex->getMessage()
|
||||
);
|
||||
}
|
||||
try {
|
||||
new CookieSessionProvider( array( 'priority' => SessionInfo::MAX_PRIORITY + 1 ) );
|
||||
$this->fail( 'Expected exception not thrown' );
|
||||
} catch ( \InvalidArgumentException $ex ) {
|
||||
$this->assertSame(
|
||||
'MediaWiki\\Session\\CookieSessionProvider::__construct: Invalid priority',
|
||||
$ex->getMessage()
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
new CookieSessionProvider( array( 'priority' => 1, 'cookieOptions' => null ) );
|
||||
$this->fail( 'Expected exception not thrown' );
|
||||
} catch ( \InvalidArgumentException $ex ) {
|
||||
$this->assertSame(
|
||||
'MediaWiki\\Session\\CookieSessionProvider::__construct: cookieOptions must be an array',
|
||||
$ex->getMessage()
|
||||
);
|
||||
}
|
||||
|
||||
$config = $this->getConfig();
|
||||
$p = \TestingAccessWrapper::newFromObject(
|
||||
new CookieSessionProvider( array( 'priority' => 1 ) )
|
||||
);
|
||||
$p->setLogger( new \TestLogger() );
|
||||
$p->setConfig( $config );
|
||||
$this->assertEquals( 1, $p->priority );
|
||||
$this->assertEquals( array(
|
||||
'callUserSetCookiesHook' => false,
|
||||
'sessionName' => 'CookiePrefix_session',
|
||||
), $p->params );
|
||||
$this->assertEquals( array(
|
||||
'prefix' => 'CookiePrefix',
|
||||
'path' => 'CookiePath',
|
||||
'domain' => 'CookieDomain',
|
||||
'secure' => true,
|
||||
'httpOnly' => true,
|
||||
), $p->cookieOptions );
|
||||
|
||||
$config->set( 'SessionName', 'SessionName' );
|
||||
$p = \TestingAccessWrapper::newFromObject(
|
||||
new CookieSessionProvider( array( 'priority' => 3 ) )
|
||||
);
|
||||
$p->setLogger( new \TestLogger() );
|
||||
$p->setConfig( $config );
|
||||
$this->assertEquals( 3, $p->priority );
|
||||
$this->assertEquals( array(
|
||||
'callUserSetCookiesHook' => false,
|
||||
'sessionName' => 'SessionName',
|
||||
), $p->params );
|
||||
$this->assertEquals( array(
|
||||
'prefix' => 'CookiePrefix',
|
||||
'path' => 'CookiePath',
|
||||
'domain' => 'CookieDomain',
|
||||
'secure' => true,
|
||||
'httpOnly' => true,
|
||||
), $p->cookieOptions );
|
||||
|
||||
$p = \TestingAccessWrapper::newFromObject( new CookieSessionProvider( array(
|
||||
'priority' => 10,
|
||||
'callUserSetCookiesHook' => true,
|
||||
'cookieOptions' => array(
|
||||
'prefix' => 'XPrefix',
|
||||
'path' => 'XPath',
|
||||
'domain' => 'XDomain',
|
||||
'secure' => 'XSecure',
|
||||
'httpOnly' => 'XHttpOnly',
|
||||
),
|
||||
'sessionName' => 'XSession',
|
||||
) ) );
|
||||
$p->setLogger( new \TestLogger() );
|
||||
$p->setConfig( $config );
|
||||
$this->assertEquals( 10, $p->priority );
|
||||
$this->assertEquals( array(
|
||||
'callUserSetCookiesHook' => true,
|
||||
'sessionName' => 'XSession',
|
||||
), $p->params );
|
||||
$this->assertEquals( array(
|
||||
'prefix' => 'XPrefix',
|
||||
'path' => 'XPath',
|
||||
'domain' => 'XDomain',
|
||||
'secure' => 'XSecure',
|
||||
'httpOnly' => 'XHttpOnly',
|
||||
), $p->cookieOptions );
|
||||
}
|
||||
|
||||
public function testBasics() {
|
||||
$provider = new CookieSessionProvider( array( 'priority' => 10 ) );
|
||||
|
||||
$this->assertTrue( $provider->persistsSessionID() );
|
||||
$this->assertTrue( $provider->canChangeUser() );
|
||||
|
||||
$msg = $provider->whyNoSession();
|
||||
$this->assertInstanceOf( 'Message', $msg );
|
||||
$this->assertSame( 'sessionprovider-nocookies', $msg->getKey() );
|
||||
}
|
||||
|
||||
public function testProvideSessionInfo() {
|
||||
$params = array(
|
||||
'priority' => 20,
|
||||
'sessionName' => 'session',
|
||||
'cookieOptions' => array( 'prefix' => 'x' ),
|
||||
);
|
||||
$provider = new CookieSessionProvider( $params );
|
||||
$provider->setLogger( new \TestLogger() );
|
||||
$provider->setConfig( $this->getConfig() );
|
||||
$provider->setManager( new SessionManager() );
|
||||
|
||||
$user = User::newFromName( 'UTSysop' );
|
||||
$id = $user->getId();
|
||||
$name = $user->getName();
|
||||
$token = $user->getToken( true );
|
||||
|
||||
$sessionId = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa';
|
||||
|
||||
// No data
|
||||
$request = new \FauxRequest();
|
||||
$info = $provider->provideSessionInfo( $request );
|
||||
$this->assertNull( $info );
|
||||
|
||||
// Session key only
|
||||
$request = new \FauxRequest();
|
||||
$request->setCookies( array(
|
||||
'session' => $sessionId,
|
||||
), '' );
|
||||
$info = $provider->provideSessionInfo( $request );
|
||||
$this->assertNotNull( $info );
|
||||
$this->assertSame( $params['priority'], $info->getPriority() );
|
||||
$this->assertSame( $sessionId, $info->getId() );
|
||||
$this->assertNull( $info->getUserInfo() );
|
||||
$this->assertFalse( $info->forceHTTPS() );
|
||||
|
||||
// User, no session key
|
||||
$request = new \FauxRequest();
|
||||
$request->setCookies( array(
|
||||
'xUserID' => $id,
|
||||
'xToken' => $token,
|
||||
), '' );
|
||||
$info = $provider->provideSessionInfo( $request );
|
||||
$this->assertNotNull( $info );
|
||||
$this->assertSame( $params['priority'], $info->getPriority() );
|
||||
$this->assertNotSame( $sessionId, $info->getId() );
|
||||
$this->assertNotNull( $info->getUserInfo() );
|
||||
$this->assertSame( $id, $info->getUserInfo()->getId() );
|
||||
$this->assertSame( $name, $info->getUserInfo()->getName() );
|
||||
$this->assertFalse( $info->forceHTTPS() );
|
||||
|
||||
// User and session key
|
||||
$request = new \FauxRequest();
|
||||
$request->setCookies( array(
|
||||
'session' => $sessionId,
|
||||
'xUserID' => $id,
|
||||
'xToken' => $token,
|
||||
), '' );
|
||||
$info = $provider->provideSessionInfo( $request );
|
||||
$this->assertNotNull( $info );
|
||||
$this->assertSame( $params['priority'], $info->getPriority() );
|
||||
$this->assertSame( $sessionId, $info->getId() );
|
||||
$this->assertNotNull( $info->getUserInfo() );
|
||||
$this->assertSame( $id, $info->getUserInfo()->getId() );
|
||||
$this->assertSame( $name, $info->getUserInfo()->getName() );
|
||||
$this->assertFalse( $info->forceHTTPS() );
|
||||
|
||||
// User with bad token
|
||||
$request = new \FauxRequest();
|
||||
$request->setCookies( array(
|
||||
'session' => $sessionId,
|
||||
'xUserID' => $id,
|
||||
'xToken' => 'BADTOKEN',
|
||||
), '' );
|
||||
$info = $provider->provideSessionInfo( $request );
|
||||
$this->assertNull( $info );
|
||||
|
||||
// User id with no token
|
||||
$request = new \FauxRequest();
|
||||
$request->setCookies( array(
|
||||
'session' => $sessionId,
|
||||
'xUserID' => $id,
|
||||
), '' );
|
||||
$info = $provider->provideSessionInfo( $request );
|
||||
$this->assertNotNull( $info );
|
||||
$this->assertSame( $params['priority'], $info->getPriority() );
|
||||
$this->assertSame( $sessionId, $info->getId() );
|
||||
$this->assertNotNull( $info->getUserInfo() );
|
||||
$this->assertFalse( $info->getUserInfo()->isVerified() );
|
||||
$this->assertSame( $id, $info->getUserInfo()->getId() );
|
||||
$this->assertSame( $name, $info->getUserInfo()->getName() );
|
||||
$this->assertFalse( $info->forceHTTPS() );
|
||||
|
||||
$request = new \FauxRequest();
|
||||
$request->setCookies( array(
|
||||
'xUserID' => $id,
|
||||
), '' );
|
||||
$info = $provider->provideSessionInfo( $request );
|
||||
$this->assertNull( $info );
|
||||
|
||||
// User and session key, with forceHTTPS flag
|
||||
$request = new \FauxRequest();
|
||||
$request->setCookies( array(
|
||||
'session' => $sessionId,
|
||||
'xUserID' => $id,
|
||||
'xToken' => $token,
|
||||
'forceHTTPS' => true,
|
||||
), '' );
|
||||
$info = $provider->provideSessionInfo( $request );
|
||||
$this->assertNotNull( $info );
|
||||
$this->assertSame( $params['priority'], $info->getPriority() );
|
||||
$this->assertSame( $sessionId, $info->getId() );
|
||||
$this->assertNotNull( $info->getUserInfo() );
|
||||
$this->assertSame( $id, $info->getUserInfo()->getId() );
|
||||
$this->assertSame( $name, $info->getUserInfo()->getName() );
|
||||
$this->assertTrue( $info->forceHTTPS() );
|
||||
|
||||
// Invalid user id
|
||||
$request = new \FauxRequest();
|
||||
$request->setCookies( array(
|
||||
'session' => $sessionId,
|
||||
'xUserID' => '-1',
|
||||
), '' );
|
||||
$info = $provider->provideSessionInfo( $request );
|
||||
$this->assertNull( $info );
|
||||
|
||||
// User id with matching name
|
||||
$request = new \FauxRequest();
|
||||
$request->setCookies( array(
|
||||
'session' => $sessionId,
|
||||
'xUserID' => $id,
|
||||
'xUserName' => $name,
|
||||
), '' );
|
||||
$info = $provider->provideSessionInfo( $request );
|
||||
$this->assertNotNull( $info );
|
||||
$this->assertSame( $params['priority'], $info->getPriority() );
|
||||
$this->assertSame( $sessionId, $info->getId() );
|
||||
$this->assertNotNull( $info->getUserInfo() );
|
||||
$this->assertFalse( $info->getUserInfo()->isVerified() );
|
||||
$this->assertSame( $id, $info->getUserInfo()->getId() );
|
||||
$this->assertSame( $name, $info->getUserInfo()->getName() );
|
||||
$this->assertFalse( $info->forceHTTPS() );
|
||||
|
||||
// User id with wrong name
|
||||
$request = new \FauxRequest();
|
||||
$request->setCookies( array(
|
||||
'session' => $sessionId,
|
||||
'xUserID' => $id,
|
||||
'xUserName' => 'Wrong',
|
||||
), '' );
|
||||
$info = $provider->provideSessionInfo( $request );
|
||||
$this->assertNull( $info );
|
||||
}
|
||||
|
||||
public function testGetVaryCookies() {
|
||||
$provider = new CookieSessionProvider( array(
|
||||
'priority' => 1,
|
||||
'sessionName' => 'MySessionName',
|
||||
'cookieOptions' => array( 'prefix' => 'MyCookiePrefix' ),
|
||||
) );
|
||||
$this->assertArrayEquals( array(
|
||||
'MyCookiePrefixToken',
|
||||
'MyCookiePrefixLoggedOut',
|
||||
'MySessionName',
|
||||
'forceHTTPS',
|
||||
), $provider->getVaryCookies() );
|
||||
}
|
||||
|
||||
public function testSuggestLoginUsername() {
|
||||
$provider = new CookieSessionProvider( array(
|
||||
'priority' => 1,
|
||||
'sessionName' => 'MySessionName',
|
||||
'cookieOptions' => array( 'prefix' => 'x' ),
|
||||
) );
|
||||
|
||||
$request = new \FauxRequest();
|
||||
$this->assertEquals( null, $provider->suggestLoginUsername( $request ) );
|
||||
|
||||
$request->setCookies( array(
|
||||
'xUserName' => 'Example',
|
||||
), '' );
|
||||
$this->assertEquals( 'Example', $provider->suggestLoginUsername( $request ) );
|
||||
}
|
||||
|
||||
public function testPersistSession() {
|
||||
$this->setMwGlobals( array( 'wgCookieExpiration' => 100 ) );
|
||||
|
||||
$provider = new CookieSessionProvider( array(
|
||||
'priority' => 1,
|
||||
'sessionName' => 'MySessionName',
|
||||
'callUserSetCookiesHook' => false,
|
||||
'cookieOptions' => array( 'prefix' => 'x' ),
|
||||
) );
|
||||
$config = $this->getConfig();
|
||||
$provider->setLogger( new \TestLogger() );
|
||||
$provider->setConfig( $config );
|
||||
$provider->setManager( SessionManager::singleton() );
|
||||
|
||||
$sessionId = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa';
|
||||
$store = new \HashBagOStuff();
|
||||
$user = User::newFromName( 'UTSysop' );
|
||||
$anon = new User;
|
||||
|
||||
$backend = new SessionBackend(
|
||||
new SessionId( $sessionId ),
|
||||
new SessionInfo( SessionInfo::MIN_PRIORITY, array(
|
||||
'provider' => $provider,
|
||||
'id' => $sessionId,
|
||||
'persisted' => true,
|
||||
'idIsSafe' => true,
|
||||
) ),
|
||||
$store,
|
||||
new \Psr\Log\NullLogger(),
|
||||
10
|
||||
);
|
||||
\TestingAccessWrapper::newFromObject( $backend )->usePhpSessionHandling = false;
|
||||
|
||||
$mock = $this->getMock( 'stdClass', array( 'onUserSetCookies' ) );
|
||||
$mock->expects( $this->never() )->method( 'onUserSetCookies' );
|
||||
$this->mergeMwGlobalArrayValue( 'wgHooks', array( 'UserSetCookies' => array( $mock ) ) );
|
||||
|
||||
// Anonymous user
|
||||
$backend->setUser( $anon );
|
||||
$backend->setRememberUser( true );
|
||||
$backend->setForceHTTPS( false );
|
||||
$request = new \FauxRequest();
|
||||
$provider->persistSession( $backend, $request );
|
||||
$this->assertSame( $sessionId, $request->response()->getCookie( 'MySessionName' ) );
|
||||
$this->assertSame( '', $request->response()->getCookie( 'xUserID' ) );
|
||||
$this->assertSame( null, $request->response()->getCookie( 'xUserName' ) );
|
||||
$this->assertSame( '', $request->response()->getCookie( 'xToken' ) );
|
||||
$this->assertSame( null, $request->response()->getCookie( 'forceHTTPS' ) );
|
||||
$this->assertSame( array(), $backend->getData() );
|
||||
|
||||
// Logged-in user, no remember
|
||||
$backend->setUser( $user );
|
||||
$backend->setRememberUser( false );
|
||||
$backend->setForceHTTPS( false );
|
||||
$request = new \FauxRequest();
|
||||
$provider->persistSession( $backend, $request );
|
||||
$this->assertSame( $sessionId, $request->response()->getCookie( 'MySessionName' ) );
|
||||
$this->assertSame( (string)$user->getId(), $request->response()->getCookie( 'xUserID' ) );
|
||||
$this->assertSame( $user->getName(), $request->response()->getCookie( 'xUserName' ) );
|
||||
$this->assertSame( '', $request->response()->getCookie( 'xToken' ) );
|
||||
$this->assertSame( null, $request->response()->getCookie( 'forceHTTPS' ) );
|
||||
$this->assertSame( array(), $backend->getData() );
|
||||
|
||||
// Logged-in user, remember
|
||||
$backend->setUser( $user );
|
||||
$backend->setRememberUser( true );
|
||||
$backend->setForceHTTPS( true );
|
||||
$request = new \FauxRequest();
|
||||
$time = time();
|
||||
$provider->persistSession( $backend, $request );
|
||||
$this->assertSame( $sessionId, $request->response()->getCookie( 'MySessionName' ) );
|
||||
$this->assertSame( (string)$user->getId(), $request->response()->getCookie( 'xUserID' ) );
|
||||
$this->assertSame( $user->getName(), $request->response()->getCookie( 'xUserName' ) );
|
||||
$this->assertSame( $user->getToken(), $request->response()->getCookie( 'xToken' ) );
|
||||
$this->assertSame( 'true', $request->response()->getCookie( 'forceHTTPS' ) );
|
||||
$this->assertSame( array(), $backend->getData() );
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider provideCookieData
|
||||
* @param bool $secure
|
||||
* @param bool $remember
|
||||
*/
|
||||
public function testCookieData( $secure, $remember ) {
|
||||
$this->setMwGlobals( array(
|
||||
'wgCookieExpiration' => 100,
|
||||
'wgSecureLogin' => false,
|
||||
) );
|
||||
|
||||
$provider = new CookieSessionProvider( array(
|
||||
'priority' => 1,
|
||||
'sessionName' => 'MySessionName',
|
||||
'callUserSetCookiesHook' => false,
|
||||
'cookieOptions' => array( 'prefix' => 'x' ),
|
||||
) );
|
||||
$config = $this->getConfig();
|
||||
$config->set( 'CookieSecure', false );
|
||||
$provider->setLogger( new \TestLogger() );
|
||||
$provider->setConfig( $config );
|
||||
$provider->setManager( SessionManager::singleton() );
|
||||
|
||||
$sessionId = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa';
|
||||
$user = User::newFromName( 'UTSysop' );
|
||||
$this->assertFalse( $user->requiresHTTPS(), 'sanity check' );
|
||||
|
||||
$backend = new SessionBackend(
|
||||
new SessionId( $sessionId ),
|
||||
new SessionInfo( SessionInfo::MIN_PRIORITY, array(
|
||||
'provider' => $provider,
|
||||
'id' => $sessionId,
|
||||
'persisted' => true,
|
||||
'idIsSafe' => true,
|
||||
) ),
|
||||
new \EmptyBagOStuff(),
|
||||
new \Psr\Log\NullLogger(),
|
||||
10
|
||||
);
|
||||
\TestingAccessWrapper::newFromObject( $backend )->usePhpSessionHandling = false;
|
||||
$backend->setUser( $user );
|
||||
$backend->setRememberUser( $remember );
|
||||
$backend->setForceHTTPS( $secure );
|
||||
$request = new \FauxRequest();
|
||||
$time = time();
|
||||
$provider->persistSession( $backend, $request );
|
||||
|
||||
$defaults = array(
|
||||
'expire' => (int)100,
|
||||
'path' => $config->get( 'CookiePath' ),
|
||||
'domain' => $config->get( 'CookieDomain' ),
|
||||
'secure' => $secure,
|
||||
'httpOnly' => $config->get( 'CookieHttpOnly' ),
|
||||
'raw' => false,
|
||||
);
|
||||
$extendedExpiry = $config->get( 'ExtendedLoginCookieExpiration' );
|
||||
$extendedExpiry = (int)( $extendedExpiry === null ? 0 : $extendedExpiry );
|
||||
$this->assertEquals( array( 'UserID', 'Token' ), $config->get( 'ExtendedLoginCookies' ),
|
||||
'sanity check' );
|
||||
$expect = array(
|
||||
'MySessionName' => array(
|
||||
'value' => (string)$sessionId,
|
||||
'expire' => 0,
|
||||
) + $defaults,
|
||||
'xUserID' => array(
|
||||
'value' => (string)$user->getId(),
|
||||
'expire' => $extendedExpiry,
|
||||
) + $defaults,
|
||||
'xUserName' => array(
|
||||
'value' => $user->getName(),
|
||||
) + $defaults,
|
||||
'xToken' => array(
|
||||
'value' => $remember ? $user->getToken() : '',
|
||||
'expire' => $remember ? $extendedExpiry : -31536000,
|
||||
) + $defaults,
|
||||
'forceHTTPS' => !$secure ? null : array(
|
||||
'value' => 'true',
|
||||
'secure' => false,
|
||||
'expire' => $remember ? $defaults['expire'] : null,
|
||||
) + $defaults,
|
||||
);
|
||||
foreach ( $expect as $key => $value ) {
|
||||
$actual = $request->response()->getCookieData( $key );
|
||||
if ( $actual && $actual['expire'] > 0 ) {
|
||||
// Round expiry so we don't randomly fail if the seconds ticked during the test.
|
||||
$actual['expire'] = round( $actual['expire'] - $time, -2 );
|
||||
}
|
||||
$this->assertEquals( $value, $actual, "Cookie $key" );
|
||||
}
|
||||
}
|
||||
|
||||
public static function provideCookieData() {
|
||||
return array(
|
||||
array( false, false ),
|
||||
array( false, true ),
|
||||
array( true, false ),
|
||||
array( true, true ),
|
||||
);
|
||||
}
|
||||
|
||||
protected function getSentRequest() {
|
||||
$sentResponse = $this->getMock( 'FauxResponse', array( 'headersSent', 'setCookie', 'header' ) );
|
||||
$sentResponse->expects( $this->any() )->method( 'headersSent' )
|
||||
->will( $this->returnValue( true ) );
|
||||
$sentResponse->expects( $this->never() )->method( 'setCookie' );
|
||||
$sentResponse->expects( $this->never() )->method( 'header' );
|
||||
|
||||
$sentRequest = $this->getMock( 'FauxRequest', array( 'response' ) );
|
||||
$sentRequest->expects( $this->any() )->method( 'response' )
|
||||
->will( $this->returnValue( $sentResponse ) );
|
||||
return $sentRequest;
|
||||
}
|
||||
|
||||
public function testPersistSessionWithHook() {
|
||||
$that = $this;
|
||||
|
||||
$provider = new CookieSessionProvider( array(
|
||||
'priority' => 1,
|
||||
'sessionName' => 'MySessionName',
|
||||
'callUserSetCookiesHook' => true,
|
||||
'cookieOptions' => array( 'prefix' => 'x' ),
|
||||
) );
|
||||
$provider->setLogger( new \Psr\Log\NullLogger() );
|
||||
$provider->setConfig( $this->getConfig() );
|
||||
$provider->setManager( SessionManager::singleton() );
|
||||
|
||||
$sessionId = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa';
|
||||
$store = new \HashBagOStuff();
|
||||
$user = User::newFromName( 'UTSysop' );
|
||||
$anon = new User;
|
||||
|
||||
$backend = new SessionBackend(
|
||||
new SessionId( $sessionId ),
|
||||
new SessionInfo( SessionInfo::MIN_PRIORITY, array(
|
||||
'provider' => $provider,
|
||||
'id' => $sessionId,
|
||||
'persisted' => true,
|
||||
'idIsSafe' => true,
|
||||
) ),
|
||||
$store,
|
||||
new \Psr\Log\NullLogger(),
|
||||
10
|
||||
);
|
||||
\TestingAccessWrapper::newFromObject( $backend )->usePhpSessionHandling = false;
|
||||
|
||||
// Anonymous user
|
||||
$mock = $this->getMock( 'stdClass', array( 'onUserSetCookies' ) );
|
||||
$mock->expects( $this->never() )->method( 'onUserSetCookies' );
|
||||
$this->mergeMwGlobalArrayValue( 'wgHooks', array( 'UserSetCookies' => array( $mock ) ) );
|
||||
$backend->setUser( $anon );
|
||||
$backend->setRememberUser( true );
|
||||
$backend->setForceHTTPS( false );
|
||||
$request = new \FauxRequest();
|
||||
$provider->persistSession( $backend, $request );
|
||||
$this->assertSame( $sessionId, $request->response()->getCookie( 'MySessionName' ) );
|
||||
$this->assertSame( '', $request->response()->getCookie( 'xUserID' ) );
|
||||
$this->assertSame( null, $request->response()->getCookie( 'xUserName' ) );
|
||||
$this->assertSame( '', $request->response()->getCookie( 'xToken' ) );
|
||||
$this->assertSame( null, $request->response()->getCookie( 'forceHTTPS' ) );
|
||||
$this->assertSame( array(), $backend->getData() );
|
||||
|
||||
$provider->persistSession( $backend, $this->getSentRequest() );
|
||||
|
||||
// Logged-in user, no remember
|
||||
$mock = $this->getMock( __CLASS__, array( 'onUserSetCookies' ) );
|
||||
$mock->expects( $this->once() )->method( 'onUserSetCookies' )
|
||||
->will( $this->returnCallback( function ( $u, &$sessionData, &$cookies ) use ( $that, $user ) {
|
||||
$that->assertSame( $user, $u );
|
||||
$that->assertEquals( array(
|
||||
'wsUserID' => $user->getId(),
|
||||
'wsUserName' => $user->getName(),
|
||||
'wsToken' => $user->getToken(),
|
||||
), $sessionData );
|
||||
$that->assertEquals( array(
|
||||
'UserID' => $user->getId(),
|
||||
'UserName' => $user->getName(),
|
||||
'Token' => false,
|
||||
), $cookies );
|
||||
|
||||
$sessionData['foo'] = 'foo!';
|
||||
$cookies['bar'] = 'bar!';
|
||||
return true;
|
||||
} ) );
|
||||
$this->mergeMwGlobalArrayValue( 'wgHooks', array( 'UserSetCookies' => array( $mock ) ) );
|
||||
$backend->setUser( $user );
|
||||
$backend->setRememberUser( false );
|
||||
$backend->setForceHTTPS( false );
|
||||
$backend->setLoggedOutTimestamp( $loggedOut = time() );
|
||||
$request = new \FauxRequest();
|
||||
$provider->persistSession( $backend, $request );
|
||||
$this->assertSame( $sessionId, $request->response()->getCookie( 'MySessionName' ) );
|
||||
$this->assertSame( (string)$user->getId(), $request->response()->getCookie( 'xUserID' ) );
|
||||
$this->assertSame( $user->getName(), $request->response()->getCookie( 'xUserName' ) );
|
||||
$this->assertSame( '', $request->response()->getCookie( 'xToken' ) );
|
||||
$this->assertSame( null, $request->response()->getCookie( 'forceHTTPS' ) );
|
||||
$this->assertSame( 'bar!', $request->response()->getCookie( 'xbar' ) );
|
||||
$this->assertSame( (string)$loggedOut, $request->response()->getCookie( 'xLoggedOut' ) );
|
||||
$this->assertEquals( array(
|
||||
'wsUserID' => $user->getId(),
|
||||
'wsUserName' => $user->getName(),
|
||||
'wsToken' => $user->getToken(),
|
||||
'foo' => 'foo!',
|
||||
), $backend->getData() );
|
||||
|
||||
$provider->persistSession( $backend, $this->getSentRequest() );
|
||||
|
||||
// Logged-in user, remember
|
||||
$mock = $this->getMock( __CLASS__, array( 'onUserSetCookies' ) );
|
||||
$mock->expects( $this->once() )->method( 'onUserSetCookies' )
|
||||
->will( $this->returnCallback( function ( $u, &$sessionData, &$cookies ) use ( $that, $user ) {
|
||||
$that->assertSame( $user, $u );
|
||||
$that->assertEquals( array(
|
||||
'wsUserID' => $user->getId(),
|
||||
'wsUserName' => $user->getName(),
|
||||
'wsToken' => $user->getToken(),
|
||||
), $sessionData );
|
||||
$that->assertEquals( array(
|
||||
'UserID' => $user->getId(),
|
||||
'UserName' => $user->getName(),
|
||||
'Token' => $user->getToken(),
|
||||
), $cookies );
|
||||
|
||||
$sessionData['foo'] = 'foo 2!';
|
||||
$cookies['bar'] = 'bar 2!';
|
||||
return true;
|
||||
} ) );
|
||||
$this->mergeMwGlobalArrayValue( 'wgHooks', array( 'UserSetCookies' => array( $mock ) ) );
|
||||
$backend->setUser( $user );
|
||||
$backend->setRememberUser( true );
|
||||
$backend->setForceHTTPS( true );
|
||||
$backend->setLoggedOutTimestamp( 0 );
|
||||
$request = new \FauxRequest();
|
||||
$provider->persistSession( $backend, $request );
|
||||
$this->assertSame( $sessionId, $request->response()->getCookie( 'MySessionName' ) );
|
||||
$this->assertSame( (string)$user->getId(), $request->response()->getCookie( 'xUserID' ) );
|
||||
$this->assertSame( $user->getName(), $request->response()->getCookie( 'xUserName' ) );
|
||||
$this->assertSame( $user->getToken(), $request->response()->getCookie( 'xToken' ) );
|
||||
$this->assertSame( 'true', $request->response()->getCookie( 'forceHTTPS' ) );
|
||||
$this->assertSame( 'bar 2!', $request->response()->getCookie( 'xbar' ) );
|
||||
$this->assertSame( null, $request->response()->getCookie( 'xLoggedOut' ) );
|
||||
$this->assertEquals( array(
|
||||
'wsUserID' => $user->getId(),
|
||||
'wsUserName' => $user->getName(),
|
||||
'wsToken' => $user->getToken(),
|
||||
'foo' => 'foo 2!',
|
||||
), $backend->getData() );
|
||||
|
||||
$provider->persistSession( $backend, $this->getSentRequest() );
|
||||
}
|
||||
|
||||
public function testUnpersistSession() {
|
||||
$provider = new CookieSessionProvider( array(
|
||||
'priority' => 1,
|
||||
'sessionName' => 'MySessionName',
|
||||
'cookieOptions' => array( 'prefix' => 'x' ),
|
||||
) );
|
||||
$provider->setLogger( new \Psr\Log\NullLogger() );
|
||||
$provider->setConfig( $this->getConfig() );
|
||||
$provider->setManager( SessionManager::singleton() );
|
||||
|
||||
$request = new \FauxRequest();
|
||||
$provider->unpersistSession( $request );
|
||||
$this->assertSame( '', $request->response()->getCookie( 'MySessionName' ) );
|
||||
$this->assertSame( '', $request->response()->getCookie( 'xUserID' ) );
|
||||
$this->assertSame( null, $request->response()->getCookie( 'xUserName' ) );
|
||||
$this->assertSame( '', $request->response()->getCookie( 'xToken' ) );
|
||||
$this->assertSame( '', $request->response()->getCookie( 'forceHTTPS' ) );
|
||||
|
||||
$provider->unpersistSession( $this->getSentRequest() );
|
||||
}
|
||||
|
||||
public function testSetLoggedOutCookie() {
|
||||
$provider = \TestingAccessWrapper::newFromObject( new CookieSessionProvider( array(
|
||||
'priority' => 1,
|
||||
'sessionName' => 'MySessionName',
|
||||
'cookieOptions' => array( 'prefix' => 'x' ),
|
||||
) ) );
|
||||
$provider->setLogger( new \Psr\Log\NullLogger() );
|
||||
$provider->setConfig( $this->getConfig() );
|
||||
$provider->setManager( SessionManager::singleton() );
|
||||
|
||||
$t1 = time();
|
||||
$t2 = time() - 86400 * 2;
|
||||
|
||||
// Set it
|
||||
$request = new \FauxRequest();
|
||||
$provider->setLoggedOutCookie( $t1, $request );
|
||||
$this->assertSame( (string)$t1, $request->response()->getCookie( 'xLoggedOut' ) );
|
||||
|
||||
// Too old
|
||||
$request = new \FauxRequest();
|
||||
$provider->setLoggedOutCookie( $t2, $request );
|
||||
$this->assertSame( null, $request->response()->getCookie( 'xLoggedOut' ) );
|
||||
|
||||
// Don't reset if it's already set
|
||||
$request = new \FauxRequest();
|
||||
$request->setCookies( array(
|
||||
'xLoggedOut' => $t1,
|
||||
), '' );
|
||||
$provider->setLoggedOutCookie( $t1, $request );
|
||||
$this->assertSame( null, $request->response()->getCookie( 'xLoggedOut' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* To be mocked for hooks, since PHPUnit can't otherwise mock methods that
|
||||
* take references.
|
||||
*/
|
||||
public function onUserSetCookies( $user, &$sessionData, &$cookies ) {
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,301 @@
|
|||
<?php
|
||||
|
||||
namespace MediaWiki\Session;
|
||||
|
||||
use MediaWikiTestCase;
|
||||
use User;
|
||||
|
||||
/**
|
||||
* @group Session
|
||||
* @group Database
|
||||
* @covers MediaWiki\Session\ImmutableSessionProviderWithCookie
|
||||
*/
|
||||
class ImmutableSessionProviderWithCookieTest extends MediaWikiTestCase {
|
||||
|
||||
private function getProvider( $name, $prefix = null ) {
|
||||
$config = new \HashConfig();
|
||||
$config->set( 'CookiePrefix', 'wgCookiePrefix' );
|
||||
|
||||
$params = array(
|
||||
'sessionCookieName' => $name,
|
||||
'sessionCookieOptions' => array(),
|
||||
);
|
||||
if ( $prefix !== null ) {
|
||||
$params['sessionCookieOptions']['prefix'] = $prefix;
|
||||
}
|
||||
|
||||
$provider = $this->getMockBuilder( 'MediaWiki\\Session\\ImmutableSessionProviderWithCookie' )
|
||||
->setConstructorArgs( array( $params ) )
|
||||
->getMockForAbstractClass();
|
||||
$provider->setLogger( new \TestLogger() );
|
||||
$provider->setConfig( $config );
|
||||
$provider->setManager( new SessionManager() );
|
||||
|
||||
return $provider;
|
||||
}
|
||||
|
||||
public function testConstructor() {
|
||||
$provider = $this->getMockBuilder( 'MediaWiki\\Session\\ImmutableSessionProviderWithCookie' )
|
||||
->getMockForAbstractClass();
|
||||
$priv = \TestingAccessWrapper::newFromObject( $provider );
|
||||
$this->assertNull( $priv->sessionCookieName );
|
||||
$this->assertSame( array(), $priv->sessionCookieOptions );
|
||||
|
||||
$provider = $this->getMockBuilder( 'MediaWiki\\Session\\ImmutableSessionProviderWithCookie' )
|
||||
->setConstructorArgs( array( array(
|
||||
'sessionCookieName' => 'Foo',
|
||||
'sessionCookieOptions' => array( 'Bar' ),
|
||||
) ) )
|
||||
->getMockForAbstractClass();
|
||||
$priv = \TestingAccessWrapper::newFromObject( $provider );
|
||||
$this->assertSame( 'Foo', $priv->sessionCookieName );
|
||||
$this->assertSame( array( 'Bar' ), $priv->sessionCookieOptions );
|
||||
|
||||
try {
|
||||
$provider = $this->getMockBuilder( 'MediaWiki\\Session\\ImmutableSessionProviderWithCookie' )
|
||||
->setConstructorArgs( array( array(
|
||||
'sessionCookieName' => false,
|
||||
) ) )
|
||||
->getMockForAbstractClass();
|
||||
$this->fail( 'Expected exception not thrown' );
|
||||
} catch ( \InvalidArgumentException $ex ) {
|
||||
$this->assertSame(
|
||||
'sessionCookieName must be a string',
|
||||
$ex->getMessage()
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
$provider = $this->getMockBuilder( 'MediaWiki\\Session\\ImmutableSessionProviderWithCookie' )
|
||||
->setConstructorArgs( array( array(
|
||||
'sessionCookieOptions' => 'x',
|
||||
) ) )
|
||||
->getMockForAbstractClass();
|
||||
$this->fail( 'Expected exception not thrown' );
|
||||
} catch ( \InvalidArgumentException $ex ) {
|
||||
$this->assertSame(
|
||||
'sessionCookieOptions must be an array',
|
||||
$ex->getMessage()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public function testBasics() {
|
||||
$provider = $this->getProvider( null );
|
||||
$this->assertFalse( $provider->persistsSessionID() );
|
||||
$this->assertFalse( $provider->canChangeUser() );
|
||||
|
||||
$provider = $this->getProvider( 'Foo' );
|
||||
$this->assertTrue( $provider->persistsSessionID() );
|
||||
$this->assertFalse( $provider->canChangeUser() );
|
||||
|
||||
$msg = $provider->whyNoSession();
|
||||
$this->assertInstanceOf( 'Message', $msg );
|
||||
$this->assertSame( 'sessionprovider-nocookies', $msg->getKey() );
|
||||
}
|
||||
|
||||
public function testGetVaryCookies() {
|
||||
$provider = $this->getProvider( null );
|
||||
$this->assertSame( array(), $provider->getVaryCookies() );
|
||||
|
||||
$provider = $this->getProvider( 'Foo' );
|
||||
$this->assertSame( array( 'wgCookiePrefixFoo' ), $provider->getVaryCookies() );
|
||||
|
||||
$provider = $this->getProvider( 'Foo', 'Bar' );
|
||||
$this->assertSame( array( 'BarFoo' ), $provider->getVaryCookies() );
|
||||
|
||||
$provider = $this->getProvider( 'Foo', '' );
|
||||
$this->assertSame( array( 'Foo' ), $provider->getVaryCookies() );
|
||||
}
|
||||
|
||||
public function testGetSessionIdFromCookie() {
|
||||
$this->setMwGlobals( 'wgCookiePrefix', 'wgCookiePrefix' );
|
||||
$request = new \FauxRequest();
|
||||
$request->setCookies( array(
|
||||
'' => 'empty---------------------------',
|
||||
'Foo' => 'foo-----------------------------',
|
||||
'wgCookiePrefixFoo' => 'wgfoo---------------------------',
|
||||
'BarFoo' => 'foobar--------------------------',
|
||||
'bad' => 'bad',
|
||||
), '' );
|
||||
|
||||
$provider = \TestingAccessWrapper::newFromObject( $this->getProvider( null ) );
|
||||
try {
|
||||
$provider->getSessionIdFromCookie( $request );
|
||||
$this->fail( 'Expected exception not thrown' );
|
||||
} catch ( \BadMethodCallException $ex ) {
|
||||
$this->assertSame(
|
||||
'MediaWiki\\Session\\ImmutableSessionProviderWithCookie::getSessionIdFromCookie ' .
|
||||
'may not be called when $this->sessionCookieName === null',
|
||||
$ex->getMessage()
|
||||
);
|
||||
}
|
||||
|
||||
$provider = \TestingAccessWrapper::newFromObject( $this->getProvider( 'Foo' ) );
|
||||
$this->assertSame(
|
||||
'wgfoo---------------------------',
|
||||
$provider->getSessionIdFromCookie( $request )
|
||||
);
|
||||
|
||||
$provider = \TestingAccessWrapper::newFromObject( $this->getProvider( 'Foo', 'Bar' ) );
|
||||
$this->assertSame(
|
||||
'foobar--------------------------',
|
||||
$provider->getSessionIdFromCookie( $request )
|
||||
);
|
||||
|
||||
$provider = \TestingAccessWrapper::newFromObject( $this->getProvider( 'Foo', '' ) );
|
||||
$this->assertSame(
|
||||
'foo-----------------------------',
|
||||
$provider->getSessionIdFromCookie( $request )
|
||||
);
|
||||
|
||||
$provider = \TestingAccessWrapper::newFromObject( $this->getProvider( 'bad', '' ) );
|
||||
$this->assertSame( null, $provider->getSessionIdFromCookie( $request ) );
|
||||
|
||||
$provider = \TestingAccessWrapper::newFromObject( $this->getProvider( 'none', '' ) );
|
||||
$this->assertSame( null, $provider->getSessionIdFromCookie( $request ) );
|
||||
}
|
||||
|
||||
protected function getSentRequest() {
|
||||
$sentResponse = $this->getMock( 'FauxResponse', array( 'headersSent', 'setCookie', 'header' ) );
|
||||
$sentResponse->expects( $this->any() )->method( 'headersSent' )
|
||||
->will( $this->returnValue( true ) );
|
||||
$sentResponse->expects( $this->never() )->method( 'setCookie' );
|
||||
$sentResponse->expects( $this->never() )->method( 'header' );
|
||||
|
||||
$sentRequest = $this->getMock( 'FauxRequest', array( 'response' ) );
|
||||
$sentRequest->expects( $this->any() )->method( 'response' )
|
||||
->will( $this->returnValue( $sentResponse ) );
|
||||
return $sentRequest;
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider providePersistSession
|
||||
* @param bool $secure
|
||||
* @param bool $remember
|
||||
*/
|
||||
public function testPersistSession( $secure, $remember ) {
|
||||
$this->setMwGlobals( array(
|
||||
'wgCookieExpiration' => 100,
|
||||
'wgSecureLogin' => false,
|
||||
) );
|
||||
|
||||
$provider = $this->getProvider( 'session' );
|
||||
$provider->setLogger( new \Psr\Log\NullLogger() );
|
||||
$priv = \TestingAccessWrapper::newFromObject( $provider );
|
||||
$priv->sessionCookieOptions = array(
|
||||
'prefix' => 'x',
|
||||
'path' => 'CookiePath',
|
||||
'domain' => 'CookieDomain',
|
||||
'secure' => false,
|
||||
'httpOnly' => true,
|
||||
);
|
||||
|
||||
$sessionId = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa';
|
||||
$user = User::newFromName( 'UTSysop' );
|
||||
$this->assertFalse( $user->requiresHTTPS(), 'sanity check' );
|
||||
|
||||
$backend = new SessionBackend(
|
||||
new SessionId( $sessionId ),
|
||||
new SessionInfo( SessionInfo::MIN_PRIORITY, array(
|
||||
'provider' => $provider,
|
||||
'id' => $sessionId,
|
||||
'persisted' => true,
|
||||
'userInfo' => UserInfo::newFromUser( $user, true ),
|
||||
'idIsSafe' => true,
|
||||
) ),
|
||||
new \EmptyBagOStuff(),
|
||||
new \Psr\Log\NullLogger(),
|
||||
10
|
||||
);
|
||||
\TestingAccessWrapper::newFromObject( $backend )->usePhpSessionHandling = false;
|
||||
$backend->setRememberUser( $remember );
|
||||
$backend->setForceHTTPS( $secure );
|
||||
|
||||
// No cookie
|
||||
$priv->sessionCookieName = null;
|
||||
$request = new \FauxRequest();
|
||||
$provider->persistSession( $backend, $request );
|
||||
$this->assertSame( array(), $request->response()->getCookies() );
|
||||
|
||||
// Cookie
|
||||
$priv->sessionCookieName = 'session';
|
||||
$request = new \FauxRequest();
|
||||
$time = time();
|
||||
$provider->persistSession( $backend, $request );
|
||||
|
||||
$cookie = $request->response()->getCookieData( 'xsession' );
|
||||
$this->assertInternalType( 'array', $cookie );
|
||||
if ( isset( $cookie['expire'] ) && $cookie['expire'] > 0 ) {
|
||||
// Round expiry so we don't randomly fail if the seconds ticked during the test.
|
||||
$cookie['expire'] = round( $cookie['expire'] - $time, -2 );
|
||||
}
|
||||
$this->assertEquals( array(
|
||||
'value' => $sessionId,
|
||||
'expire' => null,
|
||||
'path' => 'CookiePath',
|
||||
'domain' => 'CookieDomain',
|
||||
'secure' => $secure,
|
||||
'httpOnly' => true,
|
||||
'raw' => false,
|
||||
), $cookie );
|
||||
|
||||
$cookie = $request->response()->getCookieData( 'forceHTTPS' );
|
||||
if ( $secure ) {
|
||||
$this->assertInternalType( 'array', $cookie );
|
||||
if ( isset( $cookie['expire'] ) && $cookie['expire'] > 0 ) {
|
||||
// Round expiry so we don't randomly fail if the seconds ticked during the test.
|
||||
$cookie['expire'] = round( $cookie['expire'] - $time, -2 );
|
||||
}
|
||||
$this->assertEquals( array(
|
||||
'value' => 'true',
|
||||
'expire' => $remember ? 100 : null,
|
||||
'path' => 'CookiePath',
|
||||
'domain' => 'CookieDomain',
|
||||
'secure' => false,
|
||||
'httpOnly' => true,
|
||||
'raw' => false,
|
||||
), $cookie );
|
||||
} else {
|
||||
$this->assertNull( $cookie );
|
||||
}
|
||||
|
||||
// Headers sent
|
||||
$request = $this->getSentRequest();
|
||||
$provider->persistSession( $backend, $request );
|
||||
$this->assertSame( array(), $request->response()->getCookies() );
|
||||
}
|
||||
|
||||
public static function providePersistSession() {
|
||||
return array(
|
||||
array( false, false ),
|
||||
array( false, true ),
|
||||
array( true, false ),
|
||||
array( true, true ),
|
||||
);
|
||||
}
|
||||
|
||||
public function testUnpersistSession() {
|
||||
$provider = $this->getProvider( 'session', '' );
|
||||
$provider->setLogger( new \Psr\Log\NullLogger() );
|
||||
$priv = \TestingAccessWrapper::newFromObject( $provider );
|
||||
|
||||
// No cookie
|
||||
$priv->sessionCookieName = null;
|
||||
$request = new \FauxRequest();
|
||||
$provider->unpersistSession( $request );
|
||||
$this->assertSame( null, $request->response()->getCookie( 'session', '' ) );
|
||||
|
||||
// Cookie
|
||||
$priv->sessionCookieName = 'session';
|
||||
$request = new \FauxRequest();
|
||||
$provider->unpersistSession( $request );
|
||||
$this->assertSame( '', $request->response()->getCookie( 'session', '' ) );
|
||||
|
||||
// Headers sent
|
||||
$request = $this->getSentRequest();
|
||||
$provider->unpersistSession( $request );
|
||||
$this->assertSame( null, $request->response()->getCookie( 'session', '' ) );
|
||||
}
|
||||
|
||||
}
|
||||
353
tests/phpunit/includes/session/PHPSessionHandlerTest.php
Normal file
353
tests/phpunit/includes/session/PHPSessionHandlerTest.php
Normal file
|
|
@ -0,0 +1,353 @@
|
|||
<?php
|
||||
|
||||
namespace MediaWiki\Session;
|
||||
|
||||
use Psr\Log\LogLevel;
|
||||
use MediaWikiTestCase;
|
||||
|
||||
/**
|
||||
* @group Session
|
||||
* @covers MediaWiki\Session\PHPSessionHandler
|
||||
*/
|
||||
class PHPSessionHandlerTest extends MediaWikiTestCase {
|
||||
|
||||
private function getResetter( &$rProp = null ) {
|
||||
$reset = array();
|
||||
|
||||
// Ignore "headers already sent" warnings during this test
|
||||
set_error_handler( function ( $errno, $errstr ) use ( &$warnings ) {
|
||||
if ( preg_match( '/headers already sent/', $errstr ) ) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
} );
|
||||
$reset[] = new \ScopedCallback( 'restore_error_handler' );
|
||||
|
||||
$rProp = new \ReflectionProperty( 'MediaWiki\\Session\\PHPSessionHandler', 'instance' );
|
||||
$rProp->setAccessible( true );
|
||||
if ( $rProp->getValue() ) {
|
||||
$old = \TestingAccessWrapper::newFromObject( $rProp->getValue() );
|
||||
$oldManager = $old->manager;
|
||||
$oldStore = $old->store;
|
||||
$oldLogger = $old->logger;
|
||||
$reset[] = new \ScopedCallback(
|
||||
array( 'MediaWiki\\Session\\PHPSessionHandler', 'install' ),
|
||||
array( $oldManager, $oldStore, $oldLogger )
|
||||
);
|
||||
}
|
||||
|
||||
return $reset;
|
||||
}
|
||||
|
||||
public function testEnableFlags() {
|
||||
$handler = \TestingAccessWrapper::newFromObject(
|
||||
$this->getMockBuilder( 'MediaWiki\\Session\\PHPSessionHandler' )
|
||||
->setMethods( null )
|
||||
->disableOriginalConstructor()
|
||||
->getMock()
|
||||
);
|
||||
|
||||
$rProp = new \ReflectionProperty( 'MediaWiki\\Session\\PHPSessionHandler', 'instance' );
|
||||
$rProp->setAccessible( true );
|
||||
$reset = new \ScopedCallback( array( $rProp, 'setValue' ), array( $rProp->getValue() ) );
|
||||
$rProp->setValue( $handler );
|
||||
|
||||
$handler->setEnableFlags( 'enable' );
|
||||
$this->assertTrue( $handler->enable );
|
||||
$this->assertFalse( $handler->warn );
|
||||
$this->assertTrue( PHPSessionHandler::isEnabled() );
|
||||
|
||||
$handler->setEnableFlags( 'warn' );
|
||||
$this->assertTrue( $handler->enable );
|
||||
$this->assertTrue( $handler->warn );
|
||||
$this->assertTrue( PHPSessionHandler::isEnabled() );
|
||||
|
||||
$handler->setEnableFlags( 'disable' );
|
||||
$this->assertFalse( $handler->enable );
|
||||
$this->assertFalse( PHPSessionHandler::isEnabled() );
|
||||
|
||||
$rProp->setValue( null );
|
||||
$this->assertFalse( PHPSessionHandler::isEnabled() );
|
||||
}
|
||||
|
||||
public function testInstall() {
|
||||
$reset = $this->getResetter( $rProp );
|
||||
$rProp->setValue( null );
|
||||
|
||||
session_write_close();
|
||||
ini_set( 'session.use_cookies', 1 );
|
||||
ini_set( 'session.use_trans_sid', 1 );
|
||||
|
||||
$store = new \HashBagOStuff();
|
||||
$logger = new \TestLogger();
|
||||
$manager = new SessionManager( array(
|
||||
'store' => $store,
|
||||
'logger' => $logger,
|
||||
) );
|
||||
|
||||
$this->assertFalse( PHPSessionHandler::isInstalled() );
|
||||
PHPSessionHandler::install( $manager );
|
||||
$this->assertTrue( PHPSessionHandler::isInstalled() );
|
||||
|
||||
$this->assertFalse( wfIniGetBool( 'session.use_cookies' ) );
|
||||
$this->assertFalse( wfIniGetBool( 'session.use_trans_sid' ) );
|
||||
|
||||
$this->assertNotNull( $rProp->getValue() );
|
||||
$priv = \TestingAccessWrapper::newFromObject( $rProp->getValue() );
|
||||
$this->assertSame( $manager, $priv->manager );
|
||||
$this->assertSame( $store, $priv->store );
|
||||
$this->assertSame( $logger, $priv->logger );
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider provideHandlers
|
||||
* @param string $handler php serialize_handler to use
|
||||
*/
|
||||
public function testSessionHandling( $handler ) {
|
||||
$this->hideDeprecated( '$_SESSION' );
|
||||
$reset[] = $this->getResetter( $rProp );
|
||||
|
||||
$this->setMwGlobals( array(
|
||||
'wgSessionProviders' => array( array( 'class' => 'DummySessionProvider' ) ),
|
||||
'wgObjectCacheSessionExpiry' => 2,
|
||||
) );
|
||||
|
||||
$store = new \HashBagOStuff();
|
||||
$logger = new \TestLogger( true, function ( $m ) {
|
||||
return preg_match( '/^SessionBackend a{32} /', $m ) ? null : $m;
|
||||
} );
|
||||
$manager = new SessionManager( array(
|
||||
'store' => $store,
|
||||
'logger' => $logger,
|
||||
) );
|
||||
PHPSessionHandler::install( $manager );
|
||||
$wrap = \TestingAccessWrapper::newFromObject( $rProp->getValue() );
|
||||
$reset[] = new \ScopedCallback(
|
||||
array( $wrap, 'setEnableFlags' ),
|
||||
array( $wrap->enable ? $wrap->warn ? 'warn' : 'enable' : 'disable' )
|
||||
);
|
||||
$wrap->setEnableFlags( 'warn' );
|
||||
|
||||
\MediaWiki\suppressWarnings();
|
||||
ini_set( 'session.serialize_handler', $handler );
|
||||
\MediaWiki\restoreWarnings();
|
||||
if ( ini_get( 'session.serialize_handler' ) !== $handler ) {
|
||||
$this->markTestSkipped( "Cannot set session.serialize_handler to \"$handler\"" );
|
||||
}
|
||||
|
||||
// Session IDs for testing
|
||||
$sessionA = str_repeat( 'a', 32 );
|
||||
$sessionB = str_repeat( 'b', 32 );
|
||||
$sessionC = str_repeat( 'c', 32 );
|
||||
|
||||
// Set up garbage data in the session
|
||||
$_SESSION['AuthenticationSessionTest'] = 'bogus';
|
||||
|
||||
session_id( $sessionA );
|
||||
session_start();
|
||||
$this->assertSame( array(), $_SESSION );
|
||||
$this->assertSame( $sessionA, session_id() );
|
||||
|
||||
// Set some data in the session so we can see if it works.
|
||||
$rand = mt_rand();
|
||||
$_SESSION['AuthenticationSessionTest'] = $rand;
|
||||
$expect = array( 'AuthenticationSessionTest' => $rand );
|
||||
session_write_close();
|
||||
$this->assertSame( array(
|
||||
array( LogLevel::WARNING, 'Something wrote to $_SESSION!' ),
|
||||
), $logger->getBuffer() );
|
||||
|
||||
// Screw up $_SESSION so we can tell the difference between "this
|
||||
// worked" and "this did nothing"
|
||||
$_SESSION['AuthenticationSessionTest'] = 'bogus';
|
||||
|
||||
// Re-open the session and see that data was actually reloaded
|
||||
session_start();
|
||||
$this->assertSame( $expect, $_SESSION );
|
||||
|
||||
// Make sure session_reset() works too.
|
||||
if ( function_exists( 'session_reset' ) ) {
|
||||
$_SESSION['AuthenticationSessionTest'] = 'bogus';
|
||||
session_reset();
|
||||
$this->assertSame( $expect, $_SESSION );
|
||||
}
|
||||
|
||||
// Test expiry
|
||||
session_write_close();
|
||||
ini_set( 'session.gc_divisor', 1 );
|
||||
ini_set( 'session.gc_probability', 1 );
|
||||
sleep( 3 );
|
||||
session_start();
|
||||
$this->assertSame( array(), $_SESSION );
|
||||
|
||||
// Re-fill the session, then test that session_destroy() works.
|
||||
$_SESSION['AuthenticationSessionTest'] = $rand;
|
||||
session_write_close();
|
||||
session_start();
|
||||
$this->assertSame( $expect, $_SESSION );
|
||||
session_destroy();
|
||||
session_id( $sessionA );
|
||||
session_start();
|
||||
$this->assertSame( array(), $_SESSION );
|
||||
session_write_close();
|
||||
|
||||
// Test that our session handler won't clone someone else's session
|
||||
session_id( $sessionB );
|
||||
session_start();
|
||||
$this->assertSame( $sessionB, session_id() );
|
||||
$_SESSION['id'] = 'B';
|
||||
session_write_close();
|
||||
|
||||
session_id( $sessionC );
|
||||
session_start();
|
||||
$this->assertSame( array(), $_SESSION );
|
||||
$_SESSION['id'] = 'C';
|
||||
session_write_close();
|
||||
|
||||
session_id( $sessionB );
|
||||
session_start();
|
||||
$this->assertSame( array( 'id' => 'B' ), $_SESSION );
|
||||
session_write_close();
|
||||
|
||||
session_id( $sessionC );
|
||||
session_start();
|
||||
$this->assertSame( array( 'id' => 'C' ), $_SESSION );
|
||||
session_destroy();
|
||||
|
||||
session_id( $sessionB );
|
||||
session_start();
|
||||
$this->assertSame( array( 'id' => 'B' ), $_SESSION );
|
||||
|
||||
// Test merging between Session and $_SESSION
|
||||
session_write_close();
|
||||
|
||||
$session = $manager->getEmptySession();
|
||||
$session->set( 'Unchanged', 'setup' );
|
||||
$session->set( 'Changed in $_SESSION', 'setup' );
|
||||
$session->set( 'Changed in Session', 'setup' );
|
||||
$session->set( 'Changed in both', 'setup' );
|
||||
$session->set( 'Deleted in Session', 'setup' );
|
||||
$session->set( 'Deleted in $_SESSION', 'setup' );
|
||||
$session->set( 'Deleted in both', 'setup' );
|
||||
$session->set( 'Deleted in Session, changed in $_SESSION', 'setup' );
|
||||
$session->set( 'Deleted in $_SESSION, changed in Session', 'setup' );
|
||||
$session->persist();
|
||||
$session->save();
|
||||
|
||||
session_id( $session->getId() );
|
||||
session_start();
|
||||
$session->set( 'Added in Session', 'Session' );
|
||||
$session->set( 'Added in both', 'Session' );
|
||||
$session->set( 'Changed in Session', 'Session' );
|
||||
$session->set( 'Changed in both', 'Session' );
|
||||
$session->set( 'Deleted in $_SESSION, changed in Session', 'Session' );
|
||||
$session->remove( 'Deleted in Session' );
|
||||
$session->remove( 'Deleted in both' );
|
||||
$session->remove( 'Deleted in Session, changed in $_SESSION' );
|
||||
$session->save();
|
||||
$_SESSION['Added in $_SESSION'] = '$_SESSION';
|
||||
$_SESSION['Added in both'] = '$_SESSION';
|
||||
$_SESSION['Changed in $_SESSION'] = '$_SESSION';
|
||||
$_SESSION['Changed in both'] = '$_SESSION';
|
||||
$_SESSION['Deleted in Session, changed in $_SESSION'] = '$_SESSION';
|
||||
unset( $_SESSION['Deleted in $_SESSION'] );
|
||||
unset( $_SESSION['Deleted in both'] );
|
||||
unset( $_SESSION['Deleted in $_SESSION, changed in Session'] );
|
||||
session_write_close();
|
||||
|
||||
$this->assertEquals( array(
|
||||
'Added in Session' => 'Session',
|
||||
'Added in $_SESSION' => '$_SESSION',
|
||||
'Added in both' => 'Session',
|
||||
'Unchanged' => 'setup',
|
||||
'Changed in Session' => 'Session',
|
||||
'Changed in $_SESSION' => '$_SESSION',
|
||||
'Changed in both' => 'Session',
|
||||
'Deleted in Session, changed in $_SESSION' => '$_SESSION',
|
||||
'Deleted in $_SESSION, changed in Session' => 'Session',
|
||||
), iterator_to_array( $session ) );
|
||||
|
||||
$session->clear();
|
||||
$session->set( 42, 'forty-two' );
|
||||
$session->set( 'forty-two', 42 );
|
||||
$session->set( 'wrong', 43 );
|
||||
$session->persist();
|
||||
$session->save();
|
||||
|
||||
session_start();
|
||||
$this->assertArrayHasKey( 'forty-two', $_SESSION );
|
||||
$this->assertSame( 42, $_SESSION['forty-two'] );
|
||||
$this->assertArrayHasKey( 'wrong', $_SESSION );
|
||||
unset( $_SESSION['wrong'] );
|
||||
session_write_close();
|
||||
|
||||
$this->assertEquals( array(
|
||||
42 => 'forty-two',
|
||||
'forty-two' => 42,
|
||||
), iterator_to_array( $session ) );
|
||||
}
|
||||
|
||||
public static function provideHandlers() {
|
||||
return array(
|
||||
array( 'php' ),
|
||||
array( 'php_binary' ),
|
||||
array( 'php_serialize' ),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider provideDisabled
|
||||
* @expectedException BadMethodCallException
|
||||
* @expectedExceptionMessage Attempt to use PHP session management
|
||||
*/
|
||||
public function testDisabled( $method, $args ) {
|
||||
$rProp = new \ReflectionProperty( 'MediaWiki\\Session\\PHPSessionHandler', 'instance' );
|
||||
$rProp->setAccessible( true );
|
||||
$handler = $this->getMockBuilder( 'MediaWiki\\Session\\PHPSessionHandler' )
|
||||
->setMethods( null )
|
||||
->disableOriginalConstructor()
|
||||
->getMock();
|
||||
\TestingAccessWrapper::newFromObject( $handler )->setEnableFlags( 'disable' );
|
||||
$oldValue = $rProp->getValue();
|
||||
$rProp->setValue( $handler );
|
||||
$reset = new \ScopedCallback( array( $rProp, 'setValue' ), array( $oldValue ) );
|
||||
|
||||
call_user_func_array( array( $handler, $method ), $args );
|
||||
}
|
||||
|
||||
public static function provideDisabled() {
|
||||
return array(
|
||||
array( 'open', array( '', '' ) ),
|
||||
array( 'read', array( '' ) ),
|
||||
array( 'write', array( '', '' ) ),
|
||||
array( 'destroy', array( '' ) ),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider provideWrongInstance
|
||||
* @expectedException UnexpectedValueException
|
||||
* @expectedExceptionMessageRegExp /: Wrong instance called!$/
|
||||
*/
|
||||
public function testWrongInstance( $method, $args ) {
|
||||
$handler = $this->getMockBuilder( 'MediaWiki\\Session\\PHPSessionHandler' )
|
||||
->setMethods( null )
|
||||
->disableOriginalConstructor()
|
||||
->getMock();
|
||||
\TestingAccessWrapper::newFromObject( $handler )->setEnableFlags( 'enable' );
|
||||
|
||||
call_user_func_array( array( $handler, $method ), $args );
|
||||
}
|
||||
|
||||
public static function provideWrongInstance() {
|
||||
return array(
|
||||
array( 'open', array( '', '' ) ),
|
||||
array( 'close', array() ),
|
||||
array( 'read', array( '' ) ),
|
||||
array( 'write', array( '', '' ) ),
|
||||
array( 'destroy', array( '' ) ),
|
||||
array( 'gc', array( 0 ) ),
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
746
tests/phpunit/includes/session/SessionBackendTest.php
Normal file
746
tests/phpunit/includes/session/SessionBackendTest.php
Normal file
|
|
@ -0,0 +1,746 @@
|
|||
<?php
|
||||
|
||||
namespace MediaWiki\Session;
|
||||
|
||||
use MediaWikiTestCase;
|
||||
use User;
|
||||
|
||||
/**
|
||||
* @group Session
|
||||
* @group Database
|
||||
* @covers MediaWiki\Session\SessionBackend
|
||||
*/
|
||||
class SessionBackendTest extends MediaWikiTestCase {
|
||||
const SESSIONID = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa';
|
||||
|
||||
protected $manager;
|
||||
protected $config;
|
||||
protected $provider;
|
||||
protected $store;
|
||||
|
||||
protected $onSessionMetadataCalled = false;
|
||||
|
||||
/**
|
||||
* Returns a non-persistent backend that thinks it has at least one session active
|
||||
* @param User|null $user
|
||||
*/
|
||||
protected function getBackend( User $user = null ) {
|
||||
if ( !$this->config ) {
|
||||
$this->config = new \HashConfig();
|
||||
$this->manager = null;
|
||||
}
|
||||
if ( !$this->store ) {
|
||||
$this->store = new TestBagOStuff();
|
||||
$this->manager = null;
|
||||
}
|
||||
|
||||
$logger = new \Psr\Log\NullLogger();
|
||||
if ( !$this->manager ) {
|
||||
$this->manager = new SessionManager( array(
|
||||
'store' => $this->store,
|
||||
'logger' => $logger,
|
||||
'config' => $this->config,
|
||||
) );
|
||||
}
|
||||
|
||||
if ( !$this->provider ) {
|
||||
$this->provider = new \DummySessionProvider();
|
||||
}
|
||||
$this->provider->setLogger( $logger );
|
||||
$this->provider->setConfig( $this->config );
|
||||
$this->provider->setManager( $this->manager );
|
||||
|
||||
$info = new SessionInfo( SessionInfo::MIN_PRIORITY, array(
|
||||
'provider' => $this->provider,
|
||||
'id' => self::SESSIONID,
|
||||
'persisted' => true,
|
||||
'userInfo' => UserInfo::newFromUser( $user ?: new User, true ),
|
||||
'idIsSafe' => true,
|
||||
) );
|
||||
$id = new SessionId( $info->getId() );
|
||||
|
||||
$backend = new SessionBackend( $id, $info, $this->store, $logger, 10 );
|
||||
$priv = \TestingAccessWrapper::newFromObject( $backend );
|
||||
$priv->persist = false;
|
||||
$priv->requests = array( 100 => new \FauxRequest() );
|
||||
$priv->usePhpSessionHandling = false;
|
||||
|
||||
$manager = \TestingAccessWrapper::newFromObject( $this->manager );
|
||||
$manager->allSessionBackends = array( $backend->getId() => $backend );
|
||||
$manager->allSessionIds = array( $backend->getId() => $id );
|
||||
$manager->sessionProviders = array( (string)$this->provider => $this->provider );
|
||||
|
||||
return $backend;
|
||||
}
|
||||
|
||||
public function testConstructor() {
|
||||
// Set variables
|
||||
$this->getBackend();
|
||||
|
||||
$info = new SessionInfo( SessionInfo::MIN_PRIORITY, array(
|
||||
'provider' => $this->provider,
|
||||
'id' => self::SESSIONID,
|
||||
'persisted' => true,
|
||||
'userInfo' => UserInfo::newFromName( 'UTSysop', false ),
|
||||
'idIsSafe' => true,
|
||||
) );
|
||||
$id = new SessionId( $info->getId() );
|
||||
$logger = new \Psr\Log\NullLogger();
|
||||
try {
|
||||
new SessionBackend( $id, $info, $this->store, $logger, 10 );
|
||||
$this->fail( 'Expected exception not thrown' );
|
||||
} catch ( \InvalidArgumentException $ex ) {
|
||||
$this->assertSame(
|
||||
"Refusing to create session for unverified user {$info->getUserInfo()}",
|
||||
$ex->getMessage()
|
||||
);
|
||||
}
|
||||
|
||||
$info = new SessionInfo( SessionInfo::MIN_PRIORITY, array(
|
||||
'id' => self::SESSIONID,
|
||||
'userInfo' => UserInfo::newFromName( 'UTSysop', true ),
|
||||
'idIsSafe' => true,
|
||||
) );
|
||||
$id = new SessionId( $info->getId() );
|
||||
try {
|
||||
new SessionBackend( $id, $info, $this->store, $logger, 10 );
|
||||
$this->fail( 'Expected exception not thrown' );
|
||||
} catch ( \InvalidArgumentException $ex ) {
|
||||
$this->assertSame( 'Cannot create session without a provider', $ex->getMessage() );
|
||||
}
|
||||
|
||||
$info = new SessionInfo( SessionInfo::MIN_PRIORITY, array(
|
||||
'provider' => $this->provider,
|
||||
'id' => self::SESSIONID,
|
||||
'persisted' => true,
|
||||
'userInfo' => UserInfo::newFromName( 'UTSysop', true ),
|
||||
'idIsSafe' => true,
|
||||
) );
|
||||
$id = new SessionId( '!' . $info->getId() );
|
||||
try {
|
||||
new SessionBackend( $id, $info, $this->store, $logger, 10 );
|
||||
$this->fail( 'Expected exception not thrown' );
|
||||
} catch ( \InvalidArgumentException $ex ) {
|
||||
$this->assertSame(
|
||||
'SessionId and SessionInfo don\'t match',
|
||||
$ex->getMessage()
|
||||
);
|
||||
}
|
||||
|
||||
$info = new SessionInfo( SessionInfo::MIN_PRIORITY, array(
|
||||
'provider' => $this->provider,
|
||||
'id' => self::SESSIONID,
|
||||
'persisted' => true,
|
||||
'userInfo' => UserInfo::newFromName( 'UTSysop', true ),
|
||||
'idIsSafe' => true,
|
||||
) );
|
||||
$id = new SessionId( $info->getId() );
|
||||
$backend = new SessionBackend( $id, $info, $this->store, $logger, 10 );
|
||||
$this->assertSame( self::SESSIONID, $backend->getId() );
|
||||
$this->assertSame( $id, $backend->getSessionId() );
|
||||
$this->assertSame( $this->provider, $backend->getProvider() );
|
||||
$this->assertInstanceOf( 'User', $backend->getUser() );
|
||||
$this->assertSame( 'UTSysop', $backend->getUser()->getName() );
|
||||
$this->assertSame( $info->wasPersisted(), $backend->isPersistent() );
|
||||
$this->assertSame( $info->wasRemembered(), $backend->shouldRememberUser() );
|
||||
$this->assertSame( $info->forceHTTPS(), $backend->shouldForceHTTPS() );
|
||||
|
||||
$expire = time() + 100;
|
||||
$this->store->setSessionMeta( self::SESSIONID, array( 'expires' => $expire ), 2 );
|
||||
|
||||
$info = new SessionInfo( SessionInfo::MIN_PRIORITY, array(
|
||||
'provider' => $this->provider,
|
||||
'id' => self::SESSIONID,
|
||||
'persisted' => true,
|
||||
'forceHTTPS' => true,
|
||||
'metadata' => array( 'foo' ),
|
||||
'idIsSafe' => true,
|
||||
) );
|
||||
$id = new SessionId( $info->getId() );
|
||||
$backend = new SessionBackend( $id, $info, $this->store, $logger, 10 );
|
||||
$this->assertSame( self::SESSIONID, $backend->getId() );
|
||||
$this->assertSame( $id, $backend->getSessionId() );
|
||||
$this->assertSame( $this->provider, $backend->getProvider() );
|
||||
$this->assertInstanceOf( 'User', $backend->getUser() );
|
||||
$this->assertTrue( $backend->getUser()->isAnon() );
|
||||
$this->assertSame( $info->wasPersisted(), $backend->isPersistent() );
|
||||
$this->assertSame( $info->wasRemembered(), $backend->shouldRememberUser() );
|
||||
$this->assertSame( $info->forceHTTPS(), $backend->shouldForceHTTPS() );
|
||||
$this->assertSame( $expire, \TestingAccessWrapper::newFromObject( $backend )->expires );
|
||||
$this->assertSame( array( 'foo' ), $backend->getProviderMetadata() );
|
||||
}
|
||||
|
||||
public function testSessionStuff() {
|
||||
$backend = $this->getBackend();
|
||||
$priv = \TestingAccessWrapper::newFromObject( $backend );
|
||||
$priv->requests = array(); // Remove dummy session
|
||||
|
||||
$manager = \TestingAccessWrapper::newFromObject( $this->manager );
|
||||
|
||||
$request1 = new \FauxRequest();
|
||||
$session1 = $backend->getSession( $request1 );
|
||||
$request2 = new \FauxRequest();
|
||||
$session2 = $backend->getSession( $request2 );
|
||||
|
||||
$this->assertInstanceOf( 'MediaWiki\\Session\\Session', $session1 );
|
||||
$this->assertInstanceOf( 'MediaWiki\\Session\\Session', $session2 );
|
||||
$this->assertSame( 2, count( $priv->requests ) );
|
||||
|
||||
$index = \TestingAccessWrapper::newFromObject( $session1 )->index;
|
||||
|
||||
$this->assertSame( $request1, $backend->getRequest( $index ) );
|
||||
$this->assertSame( null, $backend->suggestLoginUsername( $index ) );
|
||||
$request1->setCookie( 'UserName', 'Example' );
|
||||
$this->assertSame( 'Example', $backend->suggestLoginUsername( $index ) );
|
||||
|
||||
$session1 = null;
|
||||
$this->assertSame( 1, count( $priv->requests ) );
|
||||
$this->assertArrayHasKey( $backend->getId(), $manager->allSessionBackends );
|
||||
$this->assertSame( $backend, $manager->allSessionBackends[$backend->getId()] );
|
||||
try {
|
||||
$backend->getRequest( $index );
|
||||
$this->fail( 'Expected exception not thrown' );
|
||||
} catch ( \InvalidArgumentException $ex ) {
|
||||
$this->assertSame( 'Invalid session index', $ex->getMessage() );
|
||||
}
|
||||
try {
|
||||
$backend->suggestLoginUsername( $index );
|
||||
$this->fail( 'Expected exception not thrown' );
|
||||
} catch ( \InvalidArgumentException $ex ) {
|
||||
$this->assertSame( 'Invalid session index', $ex->getMessage() );
|
||||
}
|
||||
|
||||
$session2 = null;
|
||||
$this->assertSame( 0, count( $priv->requests ) );
|
||||
$this->assertArrayNotHasKey( $backend->getId(), $manager->allSessionBackends );
|
||||
$this->assertArrayHasKey( $backend->getId(), $manager->allSessionIds );
|
||||
}
|
||||
|
||||
public function testResetId() {
|
||||
$id = session_id();
|
||||
|
||||
$builder = $this->getMockBuilder( 'DummySessionProvider' )
|
||||
->setMethods( array( 'persistsSessionId', 'sessionIdWasReset' ) );
|
||||
|
||||
$this->provider = $builder->getMock();
|
||||
$this->provider->expects( $this->any() )->method( 'persistsSessionId' )
|
||||
->will( $this->returnValue( false ) );
|
||||
$this->provider->expects( $this->never() )->method( 'sessionIdWasReset' );
|
||||
$backend = $this->getBackend( User::newFromName( 'UTSysop' ) );
|
||||
$manager = \TestingAccessWrapper::newFromObject( $this->manager );
|
||||
$sessionId = $backend->getSessionId();
|
||||
$backend->resetId();
|
||||
$this->assertSame( self::SESSIONID, $backend->getId() );
|
||||
$this->assertSame( $backend->getId(), $sessionId->getId() );
|
||||
$this->assertSame( $id, session_id() );
|
||||
$this->assertSame( $backend, $manager->allSessionBackends[self::SESSIONID] );
|
||||
|
||||
$this->provider = $builder->getMock();
|
||||
$this->provider->expects( $this->any() )->method( 'persistsSessionId' )
|
||||
->will( $this->returnValue( true ) );
|
||||
$backend = $this->getBackend();
|
||||
$this->provider->expects( $this->once() )->method( 'sessionIdWasReset' )
|
||||
->with( $this->identicalTo( $backend ), $this->identicalTo( self::SESSIONID ) );
|
||||
$manager = \TestingAccessWrapper::newFromObject( $this->manager );
|
||||
$sessionId = $backend->getSessionId();
|
||||
$backend->resetId();
|
||||
$this->assertNotEquals( self::SESSIONID, $backend->getId() );
|
||||
$this->assertSame( $backend->getId(), $sessionId->getId() );
|
||||
$this->assertInternalType( 'array', $this->store->getSession( $backend->getId() ) );
|
||||
$this->assertFalse( $this->store->getSession( self::SESSIONID ) );
|
||||
$this->assertSame( $id, session_id() );
|
||||
$this->assertArrayNotHasKey( self::SESSIONID, $manager->allSessionBackends );
|
||||
$this->assertArrayHasKey( $backend->getId(), $manager->allSessionBackends );
|
||||
$this->assertSame( $backend, $manager->allSessionBackends[$backend->getId()] );
|
||||
}
|
||||
|
||||
public function testPersist() {
|
||||
$this->provider = $this->getMock( 'DummySessionProvider', array( 'persistSession' ) );
|
||||
$this->provider->expects( $this->once() )->method( 'persistSession' );
|
||||
$backend = $this->getBackend();
|
||||
$this->assertFalse( $backend->isPersistent(), 'sanity check' );
|
||||
$backend->save(); // This one shouldn't call $provider->persistSession()
|
||||
|
||||
$backend->persist();
|
||||
$this->assertTrue( $backend->isPersistent(), 'sanity check' );
|
||||
|
||||
$this->provider = null;
|
||||
$backend = $this->getBackend();
|
||||
$wrap = \TestingAccessWrapper::newFromObject( $backend );
|
||||
$wrap->persist = true;
|
||||
$wrap->expires = 0;
|
||||
$backend->persist();
|
||||
$this->assertNotEquals( 0, $wrap->expires );
|
||||
}
|
||||
|
||||
public function testRememberUser() {
|
||||
$backend = $this->getBackend();
|
||||
|
||||
$remembered = $backend->shouldRememberUser();
|
||||
$backend->setRememberUser( !$remembered );
|
||||
$this->assertNotEquals( $remembered, $backend->shouldRememberUser() );
|
||||
$backend->setRememberUser( $remembered );
|
||||
$this->assertEquals( $remembered, $backend->shouldRememberUser() );
|
||||
}
|
||||
|
||||
public function testForceHTTPS() {
|
||||
$backend = $this->getBackend();
|
||||
|
||||
$force = $backend->shouldForceHTTPS();
|
||||
$backend->setForceHTTPS( !$force );
|
||||
$this->assertNotEquals( $force, $backend->shouldForceHTTPS() );
|
||||
$backend->setForceHTTPS( $force );
|
||||
$this->assertEquals( $force, $backend->shouldForceHTTPS() );
|
||||
}
|
||||
|
||||
public function testLoggedOutTimestamp() {
|
||||
$backend = $this->getBackend();
|
||||
|
||||
$backend->setLoggedOutTimestamp( 42 );
|
||||
$this->assertSame( 42, $backend->getLoggedOutTimestamp() );
|
||||
$backend->setLoggedOutTimestamp( '123' );
|
||||
$this->assertSame( 123, $backend->getLoggedOutTimestamp() );
|
||||
}
|
||||
|
||||
public function testSetUser() {
|
||||
$user = User::newFromName( 'UTSysop' );
|
||||
|
||||
$this->provider = $this->getMock( 'DummySessionProvider', array( 'canChangeUser' ) );
|
||||
$this->provider->expects( $this->any() )->method( 'canChangeUser' )
|
||||
->will( $this->returnValue( false ) );
|
||||
$backend = $this->getBackend();
|
||||
$this->assertFalse( $backend->canSetUser() );
|
||||
try {
|
||||
$backend->setUser( $user );
|
||||
$this->fail( 'Expected exception not thrown' );
|
||||
} catch ( \BadMethodCallException $ex ) {
|
||||
$this->assertSame(
|
||||
'Cannot set user on this session; check $session->canSetUser() first',
|
||||
$ex->getMessage()
|
||||
);
|
||||
}
|
||||
$this->assertNotSame( $user, $backend->getUser() );
|
||||
|
||||
$this->provider = null;
|
||||
$backend = $this->getBackend();
|
||||
$this->assertTrue( $backend->canSetUser() );
|
||||
$this->assertNotSame( $user, $backend->getUser(), 'sanity check' );
|
||||
$backend->setUser( $user );
|
||||
$this->assertSame( $user, $backend->getUser() );
|
||||
}
|
||||
|
||||
public function testDirty() {
|
||||
$backend = $this->getBackend();
|
||||
$priv = \TestingAccessWrapper::newFromObject( $backend );
|
||||
$priv->dataDirty = false;
|
||||
$backend->dirty();
|
||||
$this->assertTrue( $priv->dataDirty );
|
||||
}
|
||||
|
||||
public function testGetData() {
|
||||
$backend = $this->getBackend();
|
||||
$data = $backend->getData();
|
||||
$this->assertSame( array(), $data );
|
||||
$this->assertTrue( \TestingAccessWrapper::newFromObject( $backend )->dataDirty );
|
||||
$data['???'] = '!!!';
|
||||
$this->assertSame( array( '???' => '!!!' ), $data );
|
||||
|
||||
$testData = array( 'foo' => 'foo!', 'bar', array( 'baz', null ) );
|
||||
$this->store->setSessionData( self::SESSIONID, $testData );
|
||||
$backend = $this->getBackend();
|
||||
$this->assertSame( $testData, $backend->getData() );
|
||||
$this->assertFalse( \TestingAccessWrapper::newFromObject( $backend )->dataDirty );
|
||||
}
|
||||
|
||||
public function testAddData() {
|
||||
$backend = $this->getBackend();
|
||||
$priv = \TestingAccessWrapper::newFromObject( $backend );
|
||||
|
||||
$priv->data = array( 'foo' => 1 );
|
||||
$priv->dataDirty = false;
|
||||
$backend->addData( array( 'foo' => 1 ) );
|
||||
$this->assertSame( array( 'foo' => 1 ), $priv->data );
|
||||
$this->assertFalse( $priv->dataDirty );
|
||||
|
||||
$priv->data = array( 'foo' => 1 );
|
||||
$priv->dataDirty = false;
|
||||
$backend->addData( array( 'foo' => '1' ) );
|
||||
$this->assertSame( array( 'foo' => '1' ), $priv->data );
|
||||
$this->assertTrue( $priv->dataDirty );
|
||||
|
||||
$priv->data = array( 'foo' => 1 );
|
||||
$priv->dataDirty = false;
|
||||
$backend->addData( array( 'bar' => 2 ) );
|
||||
$this->assertSame( array( 'foo' => 1, 'bar' => 2 ), $priv->data );
|
||||
$this->assertTrue( $priv->dataDirty );
|
||||
}
|
||||
|
||||
public function testDelaySave() {
|
||||
$this->mergeMwGlobalArrayValue( 'wgHooks', array( 'SessionMetadata' => array( $this ) ) );
|
||||
$backend = $this->getBackend();
|
||||
$priv = \TestingAccessWrapper::newFromObject( $backend );
|
||||
$priv->persist = true;
|
||||
|
||||
// Saves happen normally when no delay is in effect
|
||||
$this->onSessionMetadataCalled = false;
|
||||
$priv->metaDirty = true;
|
||||
$backend->save();
|
||||
$this->assertTrue( $this->onSessionMetadataCalled, 'sanity check' );
|
||||
|
||||
$this->onSessionMetadataCalled = false;
|
||||
$priv->metaDirty = true;
|
||||
$priv->autosave();
|
||||
$this->assertTrue( $this->onSessionMetadataCalled, 'sanity check' );
|
||||
|
||||
$delay = $backend->delaySave();
|
||||
|
||||
// Autosave doesn't happen when no delay is in effect
|
||||
$this->onSessionMetadataCalled = false;
|
||||
$priv->metaDirty = true;
|
||||
$priv->autosave();
|
||||
$this->assertFalse( $this->onSessionMetadataCalled );
|
||||
|
||||
// Save still does happen when no delay is in effect
|
||||
$priv->save();
|
||||
$this->assertTrue( $this->onSessionMetadataCalled );
|
||||
|
||||
// Save happens when delay is consumed
|
||||
$this->onSessionMetadataCalled = false;
|
||||
$priv->metaDirty = true;
|
||||
\ScopedCallback::consume( $delay );
|
||||
$this->assertTrue( $this->onSessionMetadataCalled );
|
||||
|
||||
// Test multiple delays
|
||||
$delay1 = $backend->delaySave();
|
||||
$delay2 = $backend->delaySave();
|
||||
$delay3 = $backend->delaySave();
|
||||
$this->onSessionMetadataCalled = false;
|
||||
$priv->metaDirty = true;
|
||||
$priv->autosave();
|
||||
$this->assertFalse( $this->onSessionMetadataCalled );
|
||||
\ScopedCallback::consume( $delay3 );
|
||||
$this->assertFalse( $this->onSessionMetadataCalled );
|
||||
\ScopedCallback::consume( $delay1 );
|
||||
$this->assertFalse( $this->onSessionMetadataCalled );
|
||||
\ScopedCallback::consume( $delay2 );
|
||||
$this->assertTrue( $this->onSessionMetadataCalled );
|
||||
}
|
||||
|
||||
public function testSave() {
|
||||
$user = User::newFromName( 'UTSysop' );
|
||||
$this->store = new TestBagOStuff();
|
||||
$testData = array( 'foo' => 'foo!', 'bar', array( 'baz', null ) );
|
||||
|
||||
$neverHook = $this->getMock( __CLASS__, array( 'onSessionMetadata' ) );
|
||||
$neverHook->expects( $this->never() )->method( 'onSessionMetadata' );
|
||||
|
||||
$neverProvider = $this->getMock( 'DummySessionProvider', array( 'persistSession' ) );
|
||||
$neverProvider->expects( $this->never() )->method( 'persistSession' );
|
||||
|
||||
// Not persistent or dirty
|
||||
$this->provider = $neverProvider;
|
||||
$this->mergeMwGlobalArrayValue( 'wgHooks', array( 'SessionMetadata' => array( $neverHook ) ) );
|
||||
$this->store->setSessionData( self::SESSIONID, $testData );
|
||||
$backend = $this->getBackend( $user );
|
||||
$this->store->deleteSession( self::SESSIONID );
|
||||
$this->assertFalse( $backend->isPersistent(), 'sanity check' );
|
||||
\TestingAccessWrapper::newFromObject( $backend )->metaDirty = false;
|
||||
\TestingAccessWrapper::newFromObject( $backend )->dataDirty = false;
|
||||
$backend->save();
|
||||
$this->assertFalse( $this->store->getSession( self::SESSIONID ), 'making sure it didn\'t save' );
|
||||
|
||||
// Not persistent, but dirty
|
||||
$this->provider = $neverProvider;
|
||||
$this->onSessionMetadataCalled = false;
|
||||
$this->mergeMwGlobalArrayValue( 'wgHooks', array( 'SessionMetadata' => array( $this ) ) );
|
||||
$this->store->setSessionData( self::SESSIONID, $testData );
|
||||
$backend = $this->getBackend( $user );
|
||||
$this->store->deleteSession( self::SESSIONID );
|
||||
$this->assertFalse( $backend->isPersistent(), 'sanity check' );
|
||||
\TestingAccessWrapper::newFromObject( $backend )->metaDirty = false;
|
||||
\TestingAccessWrapper::newFromObject( $backend )->dataDirty = true;
|
||||
$backend->save();
|
||||
$this->assertTrue( $this->onSessionMetadataCalled );
|
||||
$blob = $this->store->getSession( self::SESSIONID );
|
||||
$this->assertInternalType( 'array', $blob );
|
||||
$this->assertArrayHasKey( 'metadata', $blob );
|
||||
$metadata = $blob['metadata'];
|
||||
$this->assertInternalType( 'array', $metadata );
|
||||
$this->assertArrayHasKey( '???', $metadata );
|
||||
$this->assertSame( '!!!', $metadata['???'] );
|
||||
|
||||
// Persistent, not dirty
|
||||
$this->provider = $neverProvider;
|
||||
$this->mergeMwGlobalArrayValue( 'wgHooks', array( 'SessionMetadata' => array( $neverHook ) ) );
|
||||
$this->store->setSessionData( self::SESSIONID, $testData );
|
||||
$backend = $this->getBackend( $user );
|
||||
$this->store->deleteSession( self::SESSIONID );
|
||||
\TestingAccessWrapper::newFromObject( $backend )->persist = true;
|
||||
$this->assertTrue( $backend->isPersistent(), 'sanity check' );
|
||||
\TestingAccessWrapper::newFromObject( $backend )->metaDirty = false;
|
||||
\TestingAccessWrapper::newFromObject( $backend )->dataDirty = false;
|
||||
$backend->save();
|
||||
$this->assertFalse( $this->store->getSession( self::SESSIONID ), 'making sure it didn\'t save' );
|
||||
|
||||
$this->provider = $this->getMock( 'DummySessionProvider', array( 'persistSession' ) );
|
||||
$this->provider->expects( $this->atLeastOnce() )->method( 'persistSession' );
|
||||
$this->mergeMwGlobalArrayValue( 'wgHooks', array( 'SessionMetadata' => array( $neverHook ) ) );
|
||||
$this->store->setSessionData( self::SESSIONID, $testData );
|
||||
$backend = $this->getBackend( $user );
|
||||
$this->store->deleteSession( self::SESSIONID );
|
||||
\TestingAccessWrapper::newFromObject( $backend )->persist = true;
|
||||
\TestingAccessWrapper::newFromObject( $backend )->forcePersist = true;
|
||||
$this->assertTrue( $backend->isPersistent(), 'sanity check' );
|
||||
\TestingAccessWrapper::newFromObject( $backend )->metaDirty = false;
|
||||
\TestingAccessWrapper::newFromObject( $backend )->dataDirty = false;
|
||||
$backend->save();
|
||||
$this->assertFalse( $this->store->getSession( self::SESSIONID ), 'making sure it didn\'t save' );
|
||||
|
||||
// Persistent and dirty
|
||||
$this->provider = $neverProvider;
|
||||
$this->onSessionMetadataCalled = false;
|
||||
$this->mergeMwGlobalArrayValue( 'wgHooks', array( 'SessionMetadata' => array( $this ) ) );
|
||||
$this->store->setSessionData( self::SESSIONID, $testData );
|
||||
$backend = $this->getBackend( $user );
|
||||
$this->store->deleteSession( self::SESSIONID );
|
||||
\TestingAccessWrapper::newFromObject( $backend )->persist = true;
|
||||
$this->assertTrue( $backend->isPersistent(), 'sanity check' );
|
||||
\TestingAccessWrapper::newFromObject( $backend )->metaDirty = false;
|
||||
\TestingAccessWrapper::newFromObject( $backend )->dataDirty = true;
|
||||
$backend->save();
|
||||
$this->assertTrue( $this->onSessionMetadataCalled );
|
||||
$blob = $this->store->getSession( self::SESSIONID );
|
||||
$this->assertInternalType( 'array', $blob );
|
||||
$this->assertArrayHasKey( 'metadata', $blob );
|
||||
$metadata = $blob['metadata'];
|
||||
$this->assertInternalType( 'array', $metadata );
|
||||
$this->assertArrayHasKey( '???', $metadata );
|
||||
$this->assertSame( '!!!', $metadata['???'] );
|
||||
|
||||
$this->provider = $this->getMock( 'DummySessionProvider', array( 'persistSession' ) );
|
||||
$this->provider->expects( $this->atLeastOnce() )->method( 'persistSession' );
|
||||
$this->onSessionMetadataCalled = false;
|
||||
$this->mergeMwGlobalArrayValue( 'wgHooks', array( 'SessionMetadata' => array( $this ) ) );
|
||||
$this->store->setSessionData( self::SESSIONID, $testData );
|
||||
$backend = $this->getBackend( $user );
|
||||
$this->store->deleteSession( self::SESSIONID );
|
||||
\TestingAccessWrapper::newFromObject( $backend )->persist = true;
|
||||
\TestingAccessWrapper::newFromObject( $backend )->forcePersist = true;
|
||||
$this->assertTrue( $backend->isPersistent(), 'sanity check' );
|
||||
\TestingAccessWrapper::newFromObject( $backend )->metaDirty = false;
|
||||
\TestingAccessWrapper::newFromObject( $backend )->dataDirty = true;
|
||||
$backend->save();
|
||||
$this->assertTrue( $this->onSessionMetadataCalled );
|
||||
$blob = $this->store->getSession( self::SESSIONID );
|
||||
$this->assertInternalType( 'array', $blob );
|
||||
$this->assertArrayHasKey( 'metadata', $blob );
|
||||
$metadata = $blob['metadata'];
|
||||
$this->assertInternalType( 'array', $metadata );
|
||||
$this->assertArrayHasKey( '???', $metadata );
|
||||
$this->assertSame( '!!!', $metadata['???'] );
|
||||
|
||||
$this->provider = $this->getMock( 'DummySessionProvider', array( 'persistSession' ) );
|
||||
$this->provider->expects( $this->atLeastOnce() )->method( 'persistSession' );
|
||||
$this->onSessionMetadataCalled = false;
|
||||
$this->mergeMwGlobalArrayValue( 'wgHooks', array( 'SessionMetadata' => array( $this ) ) );
|
||||
$this->store->setSessionData( self::SESSIONID, $testData );
|
||||
$backend = $this->getBackend( $user );
|
||||
$this->store->deleteSession( self::SESSIONID );
|
||||
\TestingAccessWrapper::newFromObject( $backend )->persist = true;
|
||||
$this->assertTrue( $backend->isPersistent(), 'sanity check' );
|
||||
\TestingAccessWrapper::newFromObject( $backend )->metaDirty = true;
|
||||
\TestingAccessWrapper::newFromObject( $backend )->dataDirty = false;
|
||||
$backend->save();
|
||||
$this->assertTrue( $this->onSessionMetadataCalled );
|
||||
$blob = $this->store->getSession( self::SESSIONID );
|
||||
$this->assertInternalType( 'array', $blob );
|
||||
$this->assertArrayHasKey( 'metadata', $blob );
|
||||
$metadata = $blob['metadata'];
|
||||
$this->assertInternalType( 'array', $metadata );
|
||||
$this->assertArrayHasKey( '???', $metadata );
|
||||
$this->assertSame( '!!!', $metadata['???'] );
|
||||
|
||||
// Not marked dirty, but dirty data
|
||||
$this->provider = $neverProvider;
|
||||
$this->onSessionMetadataCalled = false;
|
||||
$this->mergeMwGlobalArrayValue( 'wgHooks', array( 'SessionMetadata' => array( $this ) ) );
|
||||
$this->store->setSessionData( self::SESSIONID, $testData );
|
||||
$backend = $this->getBackend( $user );
|
||||
$this->store->deleteSession( self::SESSIONID );
|
||||
\TestingAccessWrapper::newFromObject( $backend )->persist = true;
|
||||
$this->assertTrue( $backend->isPersistent(), 'sanity check' );
|
||||
\TestingAccessWrapper::newFromObject( $backend )->metaDirty = false;
|
||||
\TestingAccessWrapper::newFromObject( $backend )->dataDirty = false;
|
||||
\TestingAccessWrapper::newFromObject( $backend )->dataHash = 'Doesn\'t match';
|
||||
$backend->save();
|
||||
$this->assertTrue( $this->onSessionMetadataCalled );
|
||||
$blob = $this->store->getSession( self::SESSIONID );
|
||||
$this->assertInternalType( 'array', $blob );
|
||||
$this->assertArrayHasKey( 'metadata', $blob );
|
||||
$metadata = $blob['metadata'];
|
||||
$this->assertInternalType( 'array', $metadata );
|
||||
$this->assertArrayHasKey( '???', $metadata );
|
||||
$this->assertSame( '!!!', $metadata['???'] );
|
||||
|
||||
// Bad hook
|
||||
$this->provider = null;
|
||||
$mockHook = $this->getMock( __CLASS__, array( 'onSessionMetadata' ) );
|
||||
$mockHook->expects( $this->any() )->method( 'onSessionMetadata' )
|
||||
->will( $this->returnCallback(
|
||||
function ( SessionBackend $backend, array &$metadata, array $requests ) {
|
||||
$metadata['userId']++;
|
||||
}
|
||||
) );
|
||||
$this->mergeMwGlobalArrayValue( 'wgHooks', array( 'SessionMetadata' => array( $mockHook ) ) );
|
||||
$this->store->setSessionData( self::SESSIONID, $testData );
|
||||
$backend = $this->getBackend( $user );
|
||||
$backend->dirty();
|
||||
try {
|
||||
$backend->save();
|
||||
$this->fail( 'Expected exception not thrown' );
|
||||
} catch ( \UnexpectedValueException $ex ) {
|
||||
$this->assertSame(
|
||||
'SessionMetadata hook changed metadata key "userId"',
|
||||
$ex->getMessage()
|
||||
);
|
||||
}
|
||||
|
||||
// SessionManager::preventSessionsForUser
|
||||
\TestingAccessWrapper::newFromObject( $this->manager )->preventUsers = array(
|
||||
$user->getName() => true,
|
||||
);
|
||||
$this->provider = $neverProvider;
|
||||
$this->mergeMwGlobalArrayValue( 'wgHooks', array( 'SessionMetadata' => array( $neverHook ) ) );
|
||||
$this->store->setSessionData( self::SESSIONID, $testData );
|
||||
$backend = $this->getBackend( $user );
|
||||
$this->store->deleteSession( self::SESSIONID );
|
||||
\TestingAccessWrapper::newFromObject( $backend )->persist = true;
|
||||
$this->assertTrue( $backend->isPersistent(), 'sanity check' );
|
||||
\TestingAccessWrapper::newFromObject( $backend )->metaDirty = true;
|
||||
\TestingAccessWrapper::newFromObject( $backend )->dataDirty = true;
|
||||
$backend->save();
|
||||
$this->assertFalse( $this->store->getSession( self::SESSIONID ), 'making sure it didn\'t save' );
|
||||
}
|
||||
|
||||
public function testRenew() {
|
||||
$user = User::newFromName( 'UTSysop' );
|
||||
$this->store = new TestBagOStuff();
|
||||
$testData = array( 'foo' => 'foo!', 'bar', array( 'baz', null ) );
|
||||
|
||||
// Not persistent
|
||||
$this->provider = $this->getMock( 'DummySessionProvider', array( 'persistSession' ) );
|
||||
$this->provider->expects( $this->never() )->method( 'persistSession' );
|
||||
$this->onSessionMetadataCalled = false;
|
||||
$this->mergeMwGlobalArrayValue( 'wgHooks', array( 'SessionMetadata' => array( $this ) ) );
|
||||
$this->store->setSessionData( self::SESSIONID, $testData );
|
||||
$backend = $this->getBackend( $user );
|
||||
$this->store->deleteSession( self::SESSIONID );
|
||||
$wrap = \TestingAccessWrapper::newFromObject( $backend );
|
||||
$this->assertFalse( $backend->isPersistent(), 'sanity check' );
|
||||
$wrap->metaDirty = false;
|
||||
$wrap->dataDirty = false;
|
||||
$wrap->forcePersist = false;
|
||||
$wrap->expires = 0;
|
||||
$backend->renew();
|
||||
$this->assertTrue( $this->onSessionMetadataCalled );
|
||||
$blob = $this->store->getSession( self::SESSIONID );
|
||||
$this->assertInternalType( 'array', $blob );
|
||||
$this->assertArrayHasKey( 'metadata', $blob );
|
||||
$metadata = $blob['metadata'];
|
||||
$this->assertInternalType( 'array', $metadata );
|
||||
$this->assertArrayHasKey( '???', $metadata );
|
||||
$this->assertSame( '!!!', $metadata['???'] );
|
||||
$this->assertNotEquals( 0, $wrap->expires );
|
||||
|
||||
// Persistent
|
||||
$this->provider = $this->getMock( 'DummySessionProvider', array( 'persistSession' ) );
|
||||
$this->provider->expects( $this->atLeastOnce() )->method( 'persistSession' );
|
||||
$this->onSessionMetadataCalled = false;
|
||||
$this->mergeMwGlobalArrayValue( 'wgHooks', array( 'SessionMetadata' => array( $this ) ) );
|
||||
$this->store->setSessionData( self::SESSIONID, $testData );
|
||||
$backend = $this->getBackend( $user );
|
||||
$this->store->deleteSession( self::SESSIONID );
|
||||
$wrap = \TestingAccessWrapper::newFromObject( $backend );
|
||||
$wrap->persist = true;
|
||||
$this->assertTrue( $backend->isPersistent(), 'sanity check' );
|
||||
$wrap->metaDirty = false;
|
||||
$wrap->dataDirty = false;
|
||||
$wrap->forcePersist = false;
|
||||
$wrap->expires = 0;
|
||||
$backend->renew();
|
||||
$this->assertTrue( $this->onSessionMetadataCalled );
|
||||
$blob = $this->store->getSession( self::SESSIONID );
|
||||
$this->assertInternalType( 'array', $blob );
|
||||
$this->assertArrayHasKey( 'metadata', $blob );
|
||||
$metadata = $blob['metadata'];
|
||||
$this->assertInternalType( 'array', $metadata );
|
||||
$this->assertArrayHasKey( '???', $metadata );
|
||||
$this->assertSame( '!!!', $metadata['???'] );
|
||||
$this->assertNotEquals( 0, $wrap->expires );
|
||||
|
||||
// Not persistent, not expiring
|
||||
$this->provider = $this->getMock( 'DummySessionProvider', array( 'persistSession' ) );
|
||||
$this->provider->expects( $this->never() )->method( 'persistSession' );
|
||||
$this->onSessionMetadataCalled = false;
|
||||
$this->mergeMwGlobalArrayValue( 'wgHooks', array( 'SessionMetadata' => array( $this ) ) );
|
||||
$this->store->setSessionData( self::SESSIONID, $testData );
|
||||
$backend = $this->getBackend( $user );
|
||||
$this->store->deleteSession( self::SESSIONID );
|
||||
$wrap = \TestingAccessWrapper::newFromObject( $backend );
|
||||
$this->assertFalse( $backend->isPersistent(), 'sanity check' );
|
||||
$wrap->metaDirty = false;
|
||||
$wrap->dataDirty = false;
|
||||
$wrap->forcePersist = false;
|
||||
$expires = time() + $wrap->lifetime + 100;
|
||||
$wrap->expires = $expires;
|
||||
$backend->renew();
|
||||
$this->assertFalse( $this->onSessionMetadataCalled );
|
||||
$this->assertFalse( $this->store->getSession( self::SESSIONID ), 'making sure it didn\'t save' );
|
||||
$this->assertEquals( $expires, $wrap->expires );
|
||||
}
|
||||
|
||||
public function onSessionMetadata( SessionBackend $backend, array &$metadata, array $requests ) {
|
||||
$this->onSessionMetadataCalled = true;
|
||||
$metadata['???'] = '!!!';
|
||||
}
|
||||
|
||||
public function testResetIdOfGlobalSession() {
|
||||
if ( !PHPSessionHandler::isInstalled() ) {
|
||||
PHPSessionHandler::install( SessionManager::singleton() );
|
||||
}
|
||||
if ( !PHPSessionHandler::isEnabled() ) {
|
||||
$rProp = new \ReflectionProperty( 'MediaWiki\\Session\\PHPSessionHandler', 'instance' );
|
||||
$rProp->setAccessible( true );
|
||||
$handler = \TestingAccessWrapper::newFromObject( $rProp->getValue() );
|
||||
$resetHandler = new \ScopedCallback( function () use ( $handler ) {
|
||||
session_write_close();
|
||||
$handler->enable = false;
|
||||
} );
|
||||
$handler->enable = true;
|
||||
}
|
||||
|
||||
$backend = $this->getBackend( User::newFromName( 'UTSysop' ) );
|
||||
\TestingAccessWrapper::newFromObject( $backend )->usePhpSessionHandling = true;
|
||||
|
||||
TestUtils::setSessionManagerSingleton( $this->manager );
|
||||
|
||||
$manager = \TestingAccessWrapper::newFromObject( $this->manager );
|
||||
$request = \RequestContext::getMain()->getRequest();
|
||||
$manager->globalSession = $backend->getSession( $request );
|
||||
$manager->globalSessionRequest = $request;
|
||||
|
||||
session_id( self::SESSIONID );
|
||||
\MediaWiki\quietCall( 'session_start' );
|
||||
$backend->resetId();
|
||||
$this->assertNotEquals( self::SESSIONID, $backend->getId() );
|
||||
$this->assertSame( $backend->getId(), session_id() );
|
||||
session_write_close();
|
||||
|
||||
session_id( '' );
|
||||
$this->assertNotSame( $backend->getId(), session_id(), 'sanity check' );
|
||||
$backend->persist();
|
||||
$this->assertSame( $backend->getId(), session_id() );
|
||||
session_write_close();
|
||||
}
|
||||
|
||||
}
|
||||
22
tests/phpunit/includes/session/SessionIdTest.php
Normal file
22
tests/phpunit/includes/session/SessionIdTest.php
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
<?php
|
||||
|
||||
namespace MediaWiki\Session;
|
||||
|
||||
use MediaWikiTestCase;
|
||||
|
||||
/**
|
||||
* @group Session
|
||||
* @covers MediaWiki\Session\SessionId
|
||||
*/
|
||||
class SessionIdTest extends MediaWikiTestCase {
|
||||
|
||||
public function testEverything() {
|
||||
$id = new SessionId( 'foo' );
|
||||
$this->assertSame( 'foo', $id->getId() );
|
||||
$this->assertSame( 'foo', (string)$id );
|
||||
$id->setId( 'bar' );
|
||||
$this->assertSame( 'bar', $id->getId() );
|
||||
$this->assertSame( 'bar', (string)$id );
|
||||
}
|
||||
|
||||
}
|
||||
328
tests/phpunit/includes/session/SessionInfoTest.php
Normal file
328
tests/phpunit/includes/session/SessionInfoTest.php
Normal file
|
|
@ -0,0 +1,328 @@
|
|||
<?php
|
||||
|
||||
namespace MediaWiki\Session;
|
||||
|
||||
use Psr\Log\LogLevel;
|
||||
use MediaWikiTestCase;
|
||||
|
||||
/**
|
||||
* @group Session
|
||||
* @group Database
|
||||
* @covers MediaWiki\Session\SessionInfo
|
||||
*/
|
||||
class SessionInfoTest extends MediaWikiTestCase {
|
||||
|
||||
public function testBasics() {
|
||||
$anonInfo = UserInfo::newAnonymous();
|
||||
$userInfo = UserInfo::newFromName( 'UTSysop', true );
|
||||
$unverifiedUserInfo = UserInfo::newFromName( 'UTSysop', false );
|
||||
|
||||
try {
|
||||
new SessionInfo( SessionInfo::MIN_PRIORITY - 1, array() );
|
||||
$this->fail( 'Expected exception not thrown', 'priority < min' );
|
||||
} catch ( \InvalidArgumentException $ex ) {
|
||||
$this->assertSame( 'Invalid priority', $ex->getMessage(), 'priority < min' );
|
||||
}
|
||||
|
||||
try {
|
||||
new SessionInfo( SessionInfo::MAX_PRIORITY + 1, array() );
|
||||
$this->fail( 'Expected exception not thrown', 'priority > max' );
|
||||
} catch ( \InvalidArgumentException $ex ) {
|
||||
$this->assertSame( 'Invalid priority', $ex->getMessage(), 'priority > max' );
|
||||
}
|
||||
|
||||
try {
|
||||
new SessionInfo( SessionInfo::MIN_PRIORITY, array( 'id' => 'ABC?' ) );
|
||||
$this->fail( 'Expected exception not thrown', 'bad session ID' );
|
||||
} catch ( \InvalidArgumentException $ex ) {
|
||||
$this->assertSame( 'Invalid session ID', $ex->getMessage(), 'bad session ID' );
|
||||
}
|
||||
|
||||
try {
|
||||
new SessionInfo( SessionInfo::MIN_PRIORITY, array( 'userInfo' => new \stdClass ) );
|
||||
$this->fail( 'Expected exception not thrown', 'bad userInfo' );
|
||||
} catch ( \InvalidArgumentException $ex ) {
|
||||
$this->assertSame( 'Invalid userInfo', $ex->getMessage(), 'bad userInfo' );
|
||||
}
|
||||
|
||||
try {
|
||||
new SessionInfo( SessionInfo::MIN_PRIORITY, array() );
|
||||
$this->fail( 'Expected exception not thrown', 'no provider, no id' );
|
||||
} catch ( \InvalidArgumentException $ex ) {
|
||||
$this->assertSame( 'Must supply an ID when no provider is given', $ex->getMessage(),
|
||||
'no provider, no id' );
|
||||
}
|
||||
|
||||
try {
|
||||
new SessionInfo( SessionInfo::MIN_PRIORITY, array( 'copyFrom' => new \stdClass ) );
|
||||
$this->fail( 'Expected exception not thrown', 'bad copyFrom' );
|
||||
} catch ( \InvalidArgumentException $ex ) {
|
||||
$this->assertSame( 'Invalid copyFrom', $ex->getMessage(),
|
||||
'bad copyFrom' );
|
||||
}
|
||||
|
||||
$manager = new SessionManager();
|
||||
$provider = $this->getMockBuilder( 'MediaWiki\\Session\\SessionProvider' )
|
||||
->setMethods( array( 'persistsSessionId', 'canChangeUser', '__toString' ) )
|
||||
->getMockForAbstractClass();
|
||||
$provider->setManager( $manager );
|
||||
$provider->expects( $this->any() )->method( 'persistsSessionId' )
|
||||
->will( $this->returnValue( true ) );
|
||||
$provider->expects( $this->any() )->method( 'canChangeUser' )
|
||||
->will( $this->returnValue( true ) );
|
||||
$provider->expects( $this->any() )->method( '__toString' )
|
||||
->will( $this->returnValue( 'Mock' ) );
|
||||
|
||||
$provider2 = $this->getMockBuilder( 'MediaWiki\\Session\\SessionProvider' )
|
||||
->setMethods( array( 'persistsSessionId', 'canChangeUser', '__toString' ) )
|
||||
->getMockForAbstractClass();
|
||||
$provider2->setManager( $manager );
|
||||
$provider2->expects( $this->any() )->method( 'persistsSessionId' )
|
||||
->will( $this->returnValue( true ) );
|
||||
$provider2->expects( $this->any() )->method( 'canChangeUser' )
|
||||
->will( $this->returnValue( true ) );
|
||||
$provider2->expects( $this->any() )->method( '__toString' )
|
||||
->will( $this->returnValue( 'Mock2' ) );
|
||||
|
||||
try {
|
||||
new SessionInfo( SessionInfo::MIN_PRIORITY, array(
|
||||
'provider' => $provider,
|
||||
'userInfo' => $anonInfo,
|
||||
'metadata' => 'foo',
|
||||
) );
|
||||
$this->fail( 'Expected exception not thrown', 'bad metadata' );
|
||||
} catch ( \InvalidArgumentException $ex ) {
|
||||
$this->assertSame( 'Invalid metadata', $ex->getMessage(), 'bad metadata' );
|
||||
}
|
||||
|
||||
$info = new SessionInfo( SessionInfo::MIN_PRIORITY + 5, array(
|
||||
'provider' => $provider,
|
||||
'userInfo' => $anonInfo
|
||||
) );
|
||||
$this->assertSame( $provider, $info->getProvider() );
|
||||
$this->assertNotNull( $info->getId() );
|
||||
$this->assertSame( SessionInfo::MIN_PRIORITY + 5, $info->getPriority() );
|
||||
$this->assertSame( $anonInfo, $info->getUserInfo() );
|
||||
$this->assertTrue( $info->isIdSafe() );
|
||||
$this->assertFalse( $info->wasPersisted() );
|
||||
$this->assertFalse( $info->wasRemembered() );
|
||||
$this->assertFalse( $info->forceHTTPS() );
|
||||
$this->assertNull( $info->getProviderMetadata() );
|
||||
|
||||
$info = new SessionInfo( SessionInfo::MIN_PRIORITY + 5, array(
|
||||
'provider' => $provider,
|
||||
'userInfo' => $unverifiedUserInfo,
|
||||
'metadata' => array( 'Foo' ),
|
||||
) );
|
||||
$this->assertSame( $provider, $info->getProvider() );
|
||||
$this->assertNotNull( $info->getId() );
|
||||
$this->assertSame( SessionInfo::MIN_PRIORITY + 5, $info->getPriority() );
|
||||
$this->assertSame( $unverifiedUserInfo, $info->getUserInfo() );
|
||||
$this->assertTrue( $info->isIdSafe() );
|
||||
$this->assertFalse( $info->wasPersisted() );
|
||||
$this->assertFalse( $info->wasRemembered() );
|
||||
$this->assertFalse( $info->forceHTTPS() );
|
||||
$this->assertSame( array( 'Foo' ), $info->getProviderMetadata() );
|
||||
|
||||
$info = new SessionInfo( SessionInfo::MIN_PRIORITY + 5, array(
|
||||
'provider' => $provider,
|
||||
'userInfo' => $userInfo
|
||||
) );
|
||||
$this->assertSame( $provider, $info->getProvider() );
|
||||
$this->assertNotNull( $info->getId() );
|
||||
$this->assertSame( SessionInfo::MIN_PRIORITY + 5, $info->getPriority() );
|
||||
$this->assertSame( $userInfo, $info->getUserInfo() );
|
||||
$this->assertTrue( $info->isIdSafe() );
|
||||
$this->assertFalse( $info->wasPersisted() );
|
||||
$this->assertTrue( $info->wasRemembered() );
|
||||
$this->assertFalse( $info->forceHTTPS() );
|
||||
$this->assertNull( $info->getProviderMetadata() );
|
||||
|
||||
$id = $manager->generateSessionId();
|
||||
|
||||
$info = new SessionInfo( SessionInfo::MIN_PRIORITY + 5, array(
|
||||
'provider' => $provider,
|
||||
'id' => $id,
|
||||
'persisted' => true,
|
||||
'userInfo' => $anonInfo
|
||||
) );
|
||||
$this->assertSame( $provider, $info->getProvider() );
|
||||
$this->assertSame( $id, $info->getId() );
|
||||
$this->assertSame( SessionInfo::MIN_PRIORITY + 5, $info->getPriority() );
|
||||
$this->assertSame( $anonInfo, $info->getUserInfo() );
|
||||
$this->assertFalse( $info->isIdSafe() );
|
||||
$this->assertTrue( $info->wasPersisted() );
|
||||
$this->assertFalse( $info->wasRemembered() );
|
||||
$this->assertFalse( $info->forceHTTPS() );
|
||||
$this->assertNull( $info->getProviderMetadata() );
|
||||
|
||||
$info = new SessionInfo( SessionInfo::MIN_PRIORITY + 5, array(
|
||||
'provider' => $provider,
|
||||
'id' => $id,
|
||||
'userInfo' => $userInfo
|
||||
) );
|
||||
$this->assertSame( $provider, $info->getProvider() );
|
||||
$this->assertSame( $id, $info->getId() );
|
||||
$this->assertSame( SessionInfo::MIN_PRIORITY + 5, $info->getPriority() );
|
||||
$this->assertSame( $userInfo, $info->getUserInfo() );
|
||||
$this->assertFalse( $info->isIdSafe() );
|
||||
$this->assertFalse( $info->wasPersisted() );
|
||||
$this->assertTrue( $info->wasRemembered() );
|
||||
$this->assertFalse( $info->forceHTTPS() );
|
||||
$this->assertNull( $info->getProviderMetadata() );
|
||||
|
||||
$info = new SessionInfo( SessionInfo::MIN_PRIORITY + 5, array(
|
||||
'id' => $id,
|
||||
'persisted' => true,
|
||||
'userInfo' => $userInfo,
|
||||
'metadata' => array( 'Foo' ),
|
||||
) );
|
||||
$this->assertSame( $id, $info->getId() );
|
||||
$this->assertSame( SessionInfo::MIN_PRIORITY + 5, $info->getPriority() );
|
||||
$this->assertSame( $userInfo, $info->getUserInfo() );
|
||||
$this->assertFalse( $info->isIdSafe() );
|
||||
$this->assertTrue( $info->wasPersisted() );
|
||||
$this->assertFalse( $info->wasRemembered() );
|
||||
$this->assertFalse( $info->forceHTTPS() );
|
||||
$this->assertNull( $info->getProviderMetadata() );
|
||||
|
||||
$info = new SessionInfo( SessionInfo::MIN_PRIORITY + 5, array(
|
||||
'id' => $id,
|
||||
'remembered' => true,
|
||||
'userInfo' => $userInfo,
|
||||
) );
|
||||
$this->assertFalse( $info->wasRemembered(), 'no provider' );
|
||||
|
||||
$info = new SessionInfo( SessionInfo::MIN_PRIORITY + 5, array(
|
||||
'provider' => $provider,
|
||||
'id' => $id,
|
||||
'remembered' => true,
|
||||
) );
|
||||
$this->assertFalse( $info->wasRemembered(), 'no user' );
|
||||
|
||||
$info = new SessionInfo( SessionInfo::MIN_PRIORITY + 5, array(
|
||||
'provider' => $provider,
|
||||
'id' => $id,
|
||||
'remembered' => true,
|
||||
'userInfo' => $anonInfo,
|
||||
) );
|
||||
$this->assertFalse( $info->wasRemembered(), 'anonymous user' );
|
||||
|
||||
$info = new SessionInfo( SessionInfo::MIN_PRIORITY + 5, array(
|
||||
'provider' => $provider,
|
||||
'id' => $id,
|
||||
'remembered' => true,
|
||||
'userInfo' => $unverifiedUserInfo,
|
||||
) );
|
||||
$this->assertFalse( $info->wasRemembered(), 'unverified user' );
|
||||
|
||||
$info = new SessionInfo( SessionInfo::MIN_PRIORITY + 5, array(
|
||||
'provider' => $provider,
|
||||
'id' => $id,
|
||||
'remembered' => false,
|
||||
'userInfo' => $userInfo,
|
||||
) );
|
||||
$this->assertFalse( $info->wasRemembered(), 'specific override' );
|
||||
|
||||
$info = new SessionInfo( SessionInfo::MIN_PRIORITY + 5, array(
|
||||
'id' => $id,
|
||||
'idIsSafe' => true,
|
||||
) );
|
||||
$this->assertSame( $id, $info->getId() );
|
||||
$this->assertSame( SessionInfo::MIN_PRIORITY + 5, $info->getPriority() );
|
||||
$this->assertTrue( $info->isIdSafe() );
|
||||
|
||||
$info = new SessionInfo( SessionInfo::MIN_PRIORITY, array(
|
||||
'id' => $id,
|
||||
'forceHTTPS' => 1,
|
||||
) );
|
||||
$this->assertTrue( $info->forceHTTPS() );
|
||||
|
||||
$fromInfo = new SessionInfo( SessionInfo::MIN_PRIORITY, array(
|
||||
'id' => $id . 'A',
|
||||
'provider' => $provider,
|
||||
'userInfo' => $userInfo,
|
||||
'idIsSafe' => true,
|
||||
'persisted' => true,
|
||||
'remembered' => true,
|
||||
'forceHTTPS' => true,
|
||||
'metadata' => array( 'foo!' ),
|
||||
) );
|
||||
$info = new SessionInfo( SessionInfo::MIN_PRIORITY + 4, array(
|
||||
'copyFrom' => $fromInfo,
|
||||
) );
|
||||
$this->assertSame( $id . 'A', $info->getId() );
|
||||
$this->assertSame( SessionInfo::MIN_PRIORITY + 4, $info->getPriority() );
|
||||
$this->assertSame( $provider, $info->getProvider() );
|
||||
$this->assertSame( $userInfo, $info->getUserInfo() );
|
||||
$this->assertTrue( $info->isIdSafe() );
|
||||
$this->assertTrue( $info->wasPersisted() );
|
||||
$this->assertTrue( $info->wasRemembered() );
|
||||
$this->assertTrue( $info->forceHTTPS() );
|
||||
$this->assertSame( array( 'foo!' ), $info->getProviderMetadata() );
|
||||
|
||||
$info = new SessionInfo( SessionInfo::MIN_PRIORITY + 4, array(
|
||||
'id' => $id . 'X',
|
||||
'provider' => $provider2,
|
||||
'userInfo' => $unverifiedUserInfo,
|
||||
'idIsSafe' => false,
|
||||
'persisted' => false,
|
||||
'remembered' => false,
|
||||
'forceHTTPS' => false,
|
||||
'metadata' => null,
|
||||
'copyFrom' => $fromInfo,
|
||||
) );
|
||||
$this->assertSame( $id . 'X', $info->getId() );
|
||||
$this->assertSame( SessionInfo::MIN_PRIORITY + 4, $info->getPriority() );
|
||||
$this->assertSame( $provider2, $info->getProvider() );
|
||||
$this->assertSame( $unverifiedUserInfo, $info->getUserInfo() );
|
||||
$this->assertFalse( $info->isIdSafe() );
|
||||
$this->assertFalse( $info->wasPersisted() );
|
||||
$this->assertFalse( $info->wasRemembered() );
|
||||
$this->assertFalse( $info->forceHTTPS() );
|
||||
$this->assertNull( $info->getProviderMetadata() );
|
||||
|
||||
$info = new SessionInfo( SessionInfo::MIN_PRIORITY, array(
|
||||
'id' => $id,
|
||||
) );
|
||||
$this->assertSame(
|
||||
'[' . SessionInfo::MIN_PRIORITY . "]null<null>$id",
|
||||
(string)$info,
|
||||
'toString'
|
||||
);
|
||||
|
||||
$info = new SessionInfo( SessionInfo::MIN_PRIORITY, array(
|
||||
'provider' => $provider,
|
||||
'id' => $id,
|
||||
'persisted' => true,
|
||||
'userInfo' => $userInfo
|
||||
) );
|
||||
$this->assertSame(
|
||||
'[' . SessionInfo::MIN_PRIORITY . "]Mock<+:{$userInfo->getId()}:UTSysop>$id",
|
||||
(string)$info,
|
||||
'toString'
|
||||
);
|
||||
|
||||
$info = new SessionInfo( SessionInfo::MIN_PRIORITY, array(
|
||||
'provider' => $provider,
|
||||
'id' => $id,
|
||||
'persisted' => true,
|
||||
'userInfo' => $unverifiedUserInfo
|
||||
) );
|
||||
$this->assertSame(
|
||||
'[' . SessionInfo::MIN_PRIORITY . "]Mock<-:{$userInfo->getId()}:UTSysop>$id",
|
||||
(string)$info,
|
||||
'toString'
|
||||
);
|
||||
}
|
||||
|
||||
public function testCompare() {
|
||||
$id = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa';
|
||||
$info1 = new SessionInfo( SessionInfo::MIN_PRIORITY + 1, array( 'id' => $id ) );
|
||||
$info2 = new SessionInfo( SessionInfo::MIN_PRIORITY + 2, array( 'id' => $id ) );
|
||||
|
||||
$this->assertTrue( SessionInfo::compare( $info1, $info2 ) < 0, '<' );
|
||||
$this->assertTrue( SessionInfo::compare( $info2, $info1 ) > 0, '>' );
|
||||
$this->assertTrue( SessionInfo::compare( $info1, $info1 ) === 0, '==' );
|
||||
}
|
||||
}
|
||||
1683
tests/phpunit/includes/session/SessionManagerTest.php
Normal file
1683
tests/phpunit/includes/session/SessionManagerTest.php
Normal file
File diff suppressed because it is too large
Load diff
177
tests/phpunit/includes/session/SessionProviderTest.php
Normal file
177
tests/phpunit/includes/session/SessionProviderTest.php
Normal file
|
|
@ -0,0 +1,177 @@
|
|||
<?php
|
||||
|
||||
namespace MediaWiki\Session;
|
||||
|
||||
use MediaWikiTestCase;
|
||||
|
||||
/**
|
||||
* @group Session
|
||||
* @group Database
|
||||
* @covers MediaWiki\Session\SessionProvider
|
||||
*/
|
||||
class SessionProviderTest extends MediaWikiTestCase {
|
||||
|
||||
public function testBasics() {
|
||||
$manager = new SessionManager();
|
||||
$logger = new \TestLogger();
|
||||
$config = new \HashConfig();
|
||||
|
||||
$provider = $this->getMockForAbstractClass( 'MediaWiki\\Session\\SessionProvider' );
|
||||
$priv = \TestingAccessWrapper::newFromObject( $provider );
|
||||
|
||||
$provider->setConfig( $config );
|
||||
$this->assertSame( $config, $priv->config );
|
||||
$provider->setLogger( $logger );
|
||||
$this->assertSame( $logger, $priv->logger );
|
||||
$provider->setManager( $manager );
|
||||
$this->assertSame( $manager, $priv->manager );
|
||||
$this->assertSame( $manager, $provider->getManager() );
|
||||
|
||||
$this->assertSame( array(), $provider->getVaryHeaders() );
|
||||
$this->assertSame( array(), $provider->getVaryCookies() );
|
||||
$this->assertSame( null, $provider->suggestLoginUsername( new \FauxRequest ) );
|
||||
|
||||
$this->assertSame( get_class( $provider ), (string)$provider );
|
||||
|
||||
$this->assertNull( $provider->whyNoSession() );
|
||||
|
||||
$info = new SessionInfo( SessionInfo::MIN_PRIORITY, array(
|
||||
'id' => 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',
|
||||
'provider' => $provider,
|
||||
) );
|
||||
$metadata = array( 'foo' );
|
||||
$this->assertTrue( $provider->refreshSessionInfo( $info, new \FauxRequest, $metadata ) );
|
||||
$this->assertSame( array( 'foo' ), $metadata );
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider provideNewSessionInfo
|
||||
* @param bool $persistId Return value for ->persistsSessionId()
|
||||
* @param bool $persistUser Return value for ->persistsSessionUser()
|
||||
* @param bool $ok Whether a SessionInfo is provided
|
||||
*/
|
||||
public function testNewSessionInfo( $persistId, $persistUser, $ok ) {
|
||||
$manager = new SessionManager();
|
||||
|
||||
$provider = $this->getMockBuilder( 'MediaWiki\\Session\\SessionProvider' )
|
||||
->setMethods( array( 'canChangeUser', 'persistsSessionId' ) )
|
||||
->getMockForAbstractClass();
|
||||
$provider->expects( $this->any() )->method( 'persistsSessionId' )
|
||||
->will( $this->returnValue( $persistId ) );
|
||||
$provider->expects( $this->any() )->method( 'canChangeUser' )
|
||||
->will( $this->returnValue( $persistUser ) );
|
||||
$provider->setManager( $manager );
|
||||
|
||||
if ( $ok ) {
|
||||
$info = $provider->newSessionInfo();
|
||||
$this->assertNotNull( $info );
|
||||
$this->assertFalse( $info->wasPersisted() );
|
||||
$this->assertTrue( $info->isIdSafe() );
|
||||
|
||||
$id = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa';
|
||||
$info = $provider->newSessionInfo( $id );
|
||||
$this->assertNotNull( $info );
|
||||
$this->assertSame( $id, $info->getId() );
|
||||
$this->assertFalse( $info->wasPersisted() );
|
||||
$this->assertTrue( $info->isIdSafe() );
|
||||
} else {
|
||||
$this->assertNull( $provider->newSessionInfo() );
|
||||
}
|
||||
}
|
||||
|
||||
public function testMergeMetadata() {
|
||||
$provider = $this->getMockBuilder( 'MediaWiki\\Session\\SessionProvider' )
|
||||
->getMockForAbstractClass();
|
||||
|
||||
try {
|
||||
$provider->mergeMetadata(
|
||||
array( 'foo' => 1, 'baz' => 3 ),
|
||||
array( 'bar' => 2, 'baz' => '3' )
|
||||
);
|
||||
$this->fail( 'Expected exception not thrown' );
|
||||
} catch ( \UnexpectedValueException $ex ) {
|
||||
$this->assertSame( 'Key "baz" changed', $ex->getMessage() );
|
||||
}
|
||||
|
||||
$res = $provider->mergeMetadata(
|
||||
array( 'foo' => 1, 'baz' => 3 ),
|
||||
array( 'bar' => 2, 'baz' => 3 )
|
||||
);
|
||||
$this->assertSame( array( 'bar' => 2, 'baz' => 3 ), $res );
|
||||
}
|
||||
|
||||
public static function provideNewSessionInfo() {
|
||||
return array(
|
||||
array( false, false, false ),
|
||||
array( true, false, false ),
|
||||
array( false, true, false ),
|
||||
array( true, true, true ),
|
||||
);
|
||||
}
|
||||
|
||||
public function testImmutableSessions() {
|
||||
$provider = $this->getMockBuilder( 'MediaWiki\\Session\\SessionProvider' )
|
||||
->setMethods( array( 'canChangeUser', 'persistsSessionId' ) )
|
||||
->getMockForAbstractClass();
|
||||
$provider->expects( $this->any() )->method( 'canChangeUser' )
|
||||
->will( $this->returnValue( true ) );
|
||||
$provider->preventSessionsForUser( 'Foo' );
|
||||
|
||||
$provider = $this->getMockBuilder( 'MediaWiki\\Session\\SessionProvider' )
|
||||
->setMethods( array( 'canChangeUser', 'persistsSessionId' ) )
|
||||
->getMockForAbstractClass();
|
||||
$provider->expects( $this->any() )->method( 'canChangeUser' )
|
||||
->will( $this->returnValue( false ) );
|
||||
try {
|
||||
$provider->preventSessionsForUser( 'Foo' );
|
||||
$this->fail( 'Expected exception not thrown' );
|
||||
} catch ( \BadMethodCallException $ex ) {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public function testHashToSessionId() {
|
||||
$config = new \HashConfig( array(
|
||||
'SecretKey' => 'Shhh!',
|
||||
) );
|
||||
|
||||
$provider = $this->getMockForAbstractClass( 'MediaWiki\\Session\\SessionProvider',
|
||||
array(), 'MockSessionProvider' );
|
||||
$provider->setConfig( $config );
|
||||
$priv = \TestingAccessWrapper::newFromObject( $provider );
|
||||
|
||||
$this->assertSame( 'eoq8cb1mg7j30ui5qolafps4hg29k5bb', $priv->hashToSessionId( 'foobar' ) );
|
||||
$this->assertSame( '4do8j7tfld1g8tte9jqp3csfgmulaun9',
|
||||
$priv->hashToSessionId( 'foobar', 'secret' ) );
|
||||
|
||||
try {
|
||||
$priv->hashToSessionId( array() );
|
||||
$this->fail( 'Expected exception not thrown' );
|
||||
} catch ( \InvalidArgumentException $ex ) {
|
||||
$this->assertSame(
|
||||
'$data must be a string, array was passed',
|
||||
$ex->getMessage()
|
||||
);
|
||||
}
|
||||
try {
|
||||
$priv->hashToSessionId( '', false );
|
||||
$this->fail( 'Expected exception not thrown' );
|
||||
} catch ( \InvalidArgumentException $ex ) {
|
||||
$this->assertSame(
|
||||
'$key must be a string or null, boolean was passed',
|
||||
$ex->getMessage()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public function testDescribe() {
|
||||
$provider = $this->getMockForAbstractClass( 'MediaWiki\\Session\\SessionProvider',
|
||||
array(), 'MockSessionProvider' );
|
||||
|
||||
$this->assertSame(
|
||||
'MockSessionProvider sessions',
|
||||
$provider->describe( \Language::factory( 'en' ) )
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
201
tests/phpunit/includes/session/SessionTest.php
Normal file
201
tests/phpunit/includes/session/SessionTest.php
Normal file
|
|
@ -0,0 +1,201 @@
|
|||
<?php
|
||||
|
||||
namespace MediaWiki\Session;
|
||||
|
||||
use MediaWikiTestCase;
|
||||
use User;
|
||||
|
||||
/**
|
||||
* @group Session
|
||||
* @covers MediaWiki\Session\Session
|
||||
*/
|
||||
class SessionTest extends MediaWikiTestCase {
|
||||
|
||||
public function testConstructor() {
|
||||
$backend = TestUtils::getDummySessionBackend();
|
||||
\TestingAccessWrapper::newFromObject( $backend )->requests = array( -1 => 'dummy' );
|
||||
\TestingAccessWrapper::newFromObject( $backend )->id = new SessionId( 'abc' );
|
||||
|
||||
$session = new Session( $backend, 42 );
|
||||
$priv = \TestingAccessWrapper::newFromObject( $session );
|
||||
$this->assertSame( $backend, $priv->backend );
|
||||
$this->assertSame( 42, $priv->index );
|
||||
|
||||
$request = new \FauxRequest();
|
||||
$priv2 = \TestingAccessWrapper::newFromObject( $session->sessionWithRequest( $request ) );
|
||||
$this->assertSame( $backend, $priv2->backend );
|
||||
$this->assertNotSame( $priv->index, $priv2->index );
|
||||
$this->assertSame( $request, $priv2->getRequest() );
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider provideMethods
|
||||
* @param string $m Method to test
|
||||
* @param array $args Arguments to pass to the method
|
||||
* @param bool $index Whether the backend method gets passed the index
|
||||
* @param bool $ret Whether the method returns a value
|
||||
*/
|
||||
public function testMethods( $m, $args, $index, $ret ) {
|
||||
$mock = $this->getMock( 'MediaWiki\\Session\\DummySessionBackend',
|
||||
array( $m, 'deregisterSession' ) );
|
||||
$mock->expects( $this->once() )->method( 'deregisterSession' )
|
||||
->with( $this->identicalTo( 42 ) );
|
||||
|
||||
$tmp = $mock->expects( $this->once() )->method( $m );
|
||||
$expectArgs = array();
|
||||
if ( $index ) {
|
||||
$expectArgs[] = $this->identicalTo( 42 );
|
||||
}
|
||||
foreach ( $args as $arg ) {
|
||||
$expectArgs[] = $this->identicalTo( $arg );
|
||||
}
|
||||
$tmp = call_user_func_array( array( $tmp, 'with' ), $expectArgs );
|
||||
|
||||
$retval = new \stdClass;
|
||||
$tmp->will( $this->returnValue( $retval ) );
|
||||
|
||||
$session = TestUtils::getDummySession( $mock, 42 );
|
||||
|
||||
if ( $ret ) {
|
||||
$this->assertSame( $retval, call_user_func_array( array( $session, $m ), $args ) );
|
||||
} else {
|
||||
$this->assertNull( call_user_func_array( array( $session, $m ), $args ) );
|
||||
}
|
||||
|
||||
// Trigger Session destructor
|
||||
$session = null;
|
||||
}
|
||||
|
||||
public static function provideMethods() {
|
||||
return array(
|
||||
array( 'getId', array(), false, true ),
|
||||
array( 'getSessionId', array(), false, true ),
|
||||
array( 'resetId', array(), false, true ),
|
||||
array( 'getProvider', array(), false, true ),
|
||||
array( 'isPersistent', array(), false, true ),
|
||||
array( 'persist', array(), false, false ),
|
||||
array( 'shouldRememberUser', array(), false, true ),
|
||||
array( 'setRememberUser', array( true ), false, false ),
|
||||
array( 'getRequest', array(), true, true ),
|
||||
array( 'getUser', array(), false, true ),
|
||||
array( 'canSetUser', array(), false, true ),
|
||||
array( 'setUser', array( new \stdClass ), false, false ),
|
||||
array( 'suggestLoginUsername', array(), true, true ),
|
||||
array( 'shouldForceHTTPS', array(), false, true ),
|
||||
array( 'setForceHTTPS', array( true ), false, false ),
|
||||
array( 'getLoggedOutTimestamp', array(), false, true ),
|
||||
array( 'setLoggedOutTimestamp', array( 123 ), false, false ),
|
||||
array( 'getProviderMetadata', array(), false, true ),
|
||||
array( 'save', array(), false, false ),
|
||||
array( 'delaySave', array(), false, true ),
|
||||
array( 'renew', array(), false, false ),
|
||||
);
|
||||
}
|
||||
|
||||
public function testDataAccess() {
|
||||
$session = TestUtils::getDummySession();
|
||||
$backend = \TestingAccessWrapper::newFromObject( $session )->backend;
|
||||
|
||||
$this->assertEquals( 1, $session->get( 'foo' ) );
|
||||
$this->assertEquals( 'zero', $session->get( 0 ) );
|
||||
$this->assertFalse( $backend->dirty );
|
||||
|
||||
$this->assertEquals( null, $session->get( 'null' ) );
|
||||
$this->assertEquals( 'default', $session->get( 'null', 'default' ) );
|
||||
$this->assertFalse( $backend->dirty );
|
||||
|
||||
$session->set( 'foo', 55 );
|
||||
$this->assertEquals( 55, $backend->data['foo'] );
|
||||
$this->assertTrue( $backend->dirty );
|
||||
$backend->dirty = false;
|
||||
|
||||
$session->set( 1, 'one' );
|
||||
$this->assertEquals( 'one', $backend->data[1] );
|
||||
$this->assertTrue( $backend->dirty );
|
||||
$backend->dirty = false;
|
||||
|
||||
$session->set( 1, 'one' );
|
||||
$this->assertFalse( $backend->dirty );
|
||||
|
||||
$this->assertTrue( $session->exists( 'foo' ) );
|
||||
$this->assertTrue( $session->exists( 1 ) );
|
||||
$this->assertFalse( $session->exists( 'null' ) );
|
||||
$this->assertFalse( $session->exists( 100 ) );
|
||||
$this->assertFalse( $backend->dirty );
|
||||
|
||||
$session->remove( 'foo' );
|
||||
$this->assertArrayNotHasKey( 'foo', $backend->data );
|
||||
$this->assertTrue( $backend->dirty );
|
||||
$backend->dirty = false;
|
||||
$session->remove( 1 );
|
||||
$this->assertArrayNotHasKey( 1, $backend->data );
|
||||
$this->assertTrue( $backend->dirty );
|
||||
$backend->dirty = false;
|
||||
|
||||
$session->remove( 101 );
|
||||
$this->assertFalse( $backend->dirty );
|
||||
|
||||
$backend->data = array( 'a', 'b', '?' => 'c' );
|
||||
$this->assertSame( 3, $session->count() );
|
||||
$this->assertSame( 3, count( $session ) );
|
||||
$this->assertFalse( $backend->dirty );
|
||||
|
||||
$data = array();
|
||||
foreach ( $session as $key => $value ) {
|
||||
$data[$key] = $value;
|
||||
}
|
||||
$this->assertEquals( $backend->data, $data );
|
||||
$this->assertFalse( $backend->dirty );
|
||||
|
||||
$this->assertEquals( $backend->data, iterator_to_array( $session ) );
|
||||
$this->assertFalse( $backend->dirty );
|
||||
}
|
||||
|
||||
public function testClear() {
|
||||
$session = TestUtils::getDummySession();
|
||||
$priv = \TestingAccessWrapper::newFromObject( $session );
|
||||
|
||||
$backend = $this->getMock(
|
||||
'MediaWiki\\Session\\DummySessionBackend', array( 'canSetUser', 'setUser', 'save' )
|
||||
);
|
||||
$backend->expects( $this->once() )->method( 'canSetUser' )
|
||||
->will( $this->returnValue( true ) );
|
||||
$backend->expects( $this->once() )->method( 'setUser' )
|
||||
->with( $this->callback( function ( $user ) {
|
||||
return $user instanceof User && $user->isAnon();
|
||||
} ) );
|
||||
$backend->expects( $this->once() )->method( 'save' );
|
||||
$priv->backend = $backend;
|
||||
$session->clear();
|
||||
$this->assertSame( array(), $backend->data );
|
||||
$this->assertTrue( $backend->dirty );
|
||||
|
||||
$backend = $this->getMock(
|
||||
'MediaWiki\\Session\\DummySessionBackend', array( 'canSetUser', 'setUser', 'save' )
|
||||
);
|
||||
$backend->data = array();
|
||||
$backend->expects( $this->once() )->method( 'canSetUser' )
|
||||
->will( $this->returnValue( true ) );
|
||||
$backend->expects( $this->once() )->method( 'setUser' )
|
||||
->with( $this->callback( function ( $user ) {
|
||||
return $user instanceof User && $user->isAnon();
|
||||
} ) );
|
||||
$backend->expects( $this->once() )->method( 'save' );
|
||||
$priv->backend = $backend;
|
||||
$session->clear();
|
||||
$this->assertFalse( $backend->dirty );
|
||||
|
||||
$backend = $this->getMock(
|
||||
'MediaWiki\\Session\\DummySessionBackend', array( 'canSetUser', 'setUser', 'save' )
|
||||
);
|
||||
$backend->expects( $this->once() )->method( 'canSetUser' )
|
||||
->will( $this->returnValue( false ) );
|
||||
$backend->expects( $this->never() )->method( 'setUser' );
|
||||
$backend->expects( $this->once() )->method( 'save' );
|
||||
$priv->backend = $backend;
|
||||
$session->clear();
|
||||
$this->assertSame( array(), $backend->data );
|
||||
$this->assertTrue( $backend->dirty );
|
||||
}
|
||||
|
||||
}
|
||||
78
tests/phpunit/includes/session/TestBagOStuff.php
Normal file
78
tests/phpunit/includes/session/TestBagOStuff.php
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
<?php
|
||||
|
||||
namespace MediaWiki\Session;
|
||||
|
||||
/**
|
||||
* BagOStuff with utility functions for MediaWiki\\Session\\* testing
|
||||
*/
|
||||
class TestBagOStuff extends \HashBagOStuff {
|
||||
|
||||
/**
|
||||
* @param string $id Session ID
|
||||
* @param array $data Session data
|
||||
* @param int $expiry Expiry
|
||||
* @param User $user User for metadata
|
||||
*/
|
||||
public function setSessionData( $id, array $data, $expiry = 0, User $user = null ) {
|
||||
$this->setSession( $id, array( 'data' => $data ), $expiry, $user );
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $id Session ID
|
||||
* @param array $metadata Session metadata
|
||||
* @param int $expiry Expiry
|
||||
*/
|
||||
public function setSessionMeta( $id, array $metadata, $expiry = 0 ) {
|
||||
$this->setSession( $id, array( 'metadata' => $metadata ), $expiry );
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $id Session ID
|
||||
* @param array $blob Session metadata and data
|
||||
* @param int $expiry Expiry
|
||||
* @param User $user User for metadata
|
||||
*/
|
||||
public function setSession( $id, array $blob, $expiry = 0, User $user = null ) {
|
||||
$blob += array(
|
||||
'data' => array(),
|
||||
'metadata' => array(),
|
||||
);
|
||||
$blob['metadata'] += array(
|
||||
'userId' => $user ? $user->getId() : 0,
|
||||
'userName' => $user ? $user->getName() : null,
|
||||
'userToken' => $user ? $user->getToken( true ) : null,
|
||||
'provider' => 'DummySessionProvider',
|
||||
);
|
||||
|
||||
$this->setRawSession( $id, $blob, $expiry, $user );
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $id Session ID
|
||||
* @param array|mixed $blob Session metadata and data
|
||||
* @param int $expiry Expiry
|
||||
*/
|
||||
public function setRawSession( $id, $blob, $expiry = 0 ) {
|
||||
if ( $expiry <= 0 ) {
|
||||
$expiry = \RequestContext::getMain()->getConfig()->get( 'ObjectCacheSessionExpiry' );
|
||||
}
|
||||
|
||||
$this->set( wfMemcKey( 'MWSession', $id ), $blob, $expiry );
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $id Session ID
|
||||
* @return mixed
|
||||
*/
|
||||
public function getSession( $id ) {
|
||||
return $this->get( wfMemcKey( 'MWSession', $id ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $id Session ID
|
||||
*/
|
||||
public function deleteSession( $id ) {
|
||||
$this->delete( wfMemcKey( 'MWSession', $id ) );
|
||||
}
|
||||
|
||||
}
|
||||
99
tests/phpunit/includes/session/TestUtils.php
Normal file
99
tests/phpunit/includes/session/TestUtils.php
Normal file
|
|
@ -0,0 +1,99 @@
|
|||
<?php
|
||||
|
||||
namespace MediaWiki\Session;
|
||||
|
||||
/**
|
||||
* Utility functions for Session unit tests
|
||||
*/
|
||||
class TestUtils {
|
||||
|
||||
/**
|
||||
* Override the singleton for unit testing
|
||||
* @param SessionManager|null $manager
|
||||
* @return \\ScopedCallback|null
|
||||
*/
|
||||
public static function setSessionManagerSingleton( SessionManager $manager = null ) {
|
||||
session_write_close();
|
||||
|
||||
$rInstance = new \ReflectionProperty(
|
||||
'MediaWiki\\Session\\SessionManager', 'instance'
|
||||
);
|
||||
$rInstance->setAccessible( true );
|
||||
$rGlobalSession = new \ReflectionProperty(
|
||||
'MediaWiki\\Session\\SessionManager', 'globalSession'
|
||||
);
|
||||
$rGlobalSession->setAccessible( true );
|
||||
$rGlobalSessionRequest = new \ReflectionProperty(
|
||||
'MediaWiki\\Session\\SessionManager', 'globalSessionRequest'
|
||||
);
|
||||
$rGlobalSessionRequest->setAccessible( true );
|
||||
|
||||
$oldInstance = $rInstance->getValue();
|
||||
|
||||
$reset = array(
|
||||
array( $rInstance, $oldInstance ),
|
||||
array( $rGlobalSession, $rGlobalSession->getValue() ),
|
||||
array( $rGlobalSessionRequest, $rGlobalSessionRequest->getValue() ),
|
||||
);
|
||||
|
||||
$rInstance->setValue( $manager );
|
||||
$rGlobalSession->setValue( null );
|
||||
$rGlobalSessionRequest->setValue( null );
|
||||
if ( $manager && PHPSessionHandler::isInstalled() ) {
|
||||
PHPSessionHandler::install( $manager );
|
||||
}
|
||||
|
||||
return new \ScopedCallback( function () use ( &$reset, $oldInstance ) {
|
||||
foreach ( $reset as &$arr ) {
|
||||
$arr[0]->setValue( $arr[1] );
|
||||
}
|
||||
if ( $oldInstance && PHPSessionHandler::isInstalled() ) {
|
||||
PHPSessionHandler::install( $oldInstance );
|
||||
}
|
||||
} );
|
||||
}
|
||||
|
||||
/**
|
||||
* If you need a SessionBackend for testing but don't want to create a real
|
||||
* one, use this.
|
||||
* @return SessionBackend Unconfigured! Use reflection to set any private
|
||||
* fields necessary.
|
||||
*/
|
||||
public static function getDummySessionBackend() {
|
||||
$rc = new \ReflectionClass( 'MediaWiki\\Session\\SessionBackend' );
|
||||
if ( !method_exists( $rc, 'newInstanceWithoutConstructor' ) ) {
|
||||
\PHPUnit_Framework_Assert::markTestSkipped(
|
||||
'ReflectionClass::newInstanceWithoutConstructor isn\'t available'
|
||||
);
|
||||
}
|
||||
|
||||
return $rc->newInstanceWithoutConstructor();
|
||||
}
|
||||
|
||||
/**
|
||||
* If you need a Session for testing but don't want to create a backend to
|
||||
* construct one, use this.
|
||||
* @param object $backend Object to serve as the SessionBackend
|
||||
* @param int $index Index
|
||||
* @return Session
|
||||
*/
|
||||
public static function getDummySession( $backend = null, $index = -1 ) {
|
||||
$rc = new \ReflectionClass( 'MediaWiki\\Session\\Session' );
|
||||
if ( !method_exists( $rc, 'newInstanceWithoutConstructor' ) ) {
|
||||
\PHPUnit_Framework_Assert::markTestSkipped(
|
||||
'ReflectionClass::newInstanceWithoutConstructor isn\'t available'
|
||||
);
|
||||
}
|
||||
|
||||
if ( $backend === null ) {
|
||||
$backend = new DummySessionBackend;
|
||||
}
|
||||
|
||||
$session = $rc->newInstanceWithoutConstructor();
|
||||
$priv = \TestingAccessWrapper::newFromObject( $session );
|
||||
$priv->backend = $backend;
|
||||
$priv->index = $index;
|
||||
return $session;
|
||||
}
|
||||
|
||||
}
|
||||
186
tests/phpunit/includes/session/UserInfoTest.php
Normal file
186
tests/phpunit/includes/session/UserInfoTest.php
Normal file
|
|
@ -0,0 +1,186 @@
|
|||
<?php
|
||||
|
||||
namespace MediaWiki\Session;
|
||||
|
||||
use MediaWikiTestCase;
|
||||
use User;
|
||||
|
||||
/**
|
||||
* @group Session
|
||||
* @group Database
|
||||
* @covers MediaWiki\Session\UserInfo
|
||||
*/
|
||||
class UserInfoTest extends MediaWikiTestCase {
|
||||
|
||||
public function testNewAnonymous() {
|
||||
$userinfo = UserInfo::newAnonymous();
|
||||
|
||||
$this->assertTrue( $userinfo->isAnon() );
|
||||
$this->assertTrue( $userinfo->isVerified() );
|
||||
$this->assertSame( 0, $userinfo->getId() );
|
||||
$this->assertSame( null, $userinfo->getName() );
|
||||
$this->assertSame( null, $userinfo->getToken() );
|
||||
$this->assertNotNull( $userinfo->getUser() );
|
||||
$this->assertSame( $userinfo, $userinfo->verified() );
|
||||
$this->assertSame( '<anon>', (string)$userinfo );
|
||||
}
|
||||
|
||||
public function testNewFromId() {
|
||||
$id = wfGetDB( DB_MASTER )->selectField( 'user', 'MAX(user_id)' ) + 1;
|
||||
try {
|
||||
UserInfo::newFromId( $id );
|
||||
$this->fail( 'Expected exception not thrown' );
|
||||
} catch ( \InvalidArgumentException $ex ) {
|
||||
$this->assertSame( 'Invalid ID', $ex->getMessage() );
|
||||
}
|
||||
|
||||
$user = User::newFromName( 'UTSysop' );
|
||||
$userinfo = UserInfo::newFromId( $user->getId() );
|
||||
$this->assertFalse( $userinfo->isAnon() );
|
||||
$this->assertFalse( $userinfo->isVerified() );
|
||||
$this->assertSame( $user->getId(), $userinfo->getId() );
|
||||
$this->assertSame( $user->getName(), $userinfo->getName() );
|
||||
$this->assertSame( $user->getToken( true ), $userinfo->getToken() );
|
||||
$this->assertInstanceOf( 'User', $userinfo->getUser() );
|
||||
$userinfo2 = $userinfo->verified();
|
||||
$this->assertNotSame( $userinfo2, $userinfo );
|
||||
$this->assertSame( "<-:{$user->getId()}:{$user->getName()}>", (string)$userinfo );
|
||||
|
||||
$this->assertFalse( $userinfo2->isAnon() );
|
||||
$this->assertTrue( $userinfo2->isVerified() );
|
||||
$this->assertSame( $user->getId(), $userinfo2->getId() );
|
||||
$this->assertSame( $user->getName(), $userinfo2->getName() );
|
||||
$this->assertSame( $user->getToken( true ), $userinfo2->getToken() );
|
||||
$this->assertInstanceOf( 'User', $userinfo2->getUser() );
|
||||
$this->assertSame( $userinfo2, $userinfo2->verified() );
|
||||
$this->assertSame( "<+:{$user->getId()}:{$user->getName()}>", (string)$userinfo2 );
|
||||
|
||||
$userinfo = UserInfo::newFromId( $user->getId(), true );
|
||||
$this->assertTrue( $userinfo->isVerified() );
|
||||
$this->assertSame( $userinfo, $userinfo->verified() );
|
||||
}
|
||||
|
||||
public function testNewFromName() {
|
||||
try {
|
||||
UserInfo::newFromName( '<bad name>' );
|
||||
$this->fail( 'Expected exception not thrown' );
|
||||
} catch ( \InvalidArgumentException $ex ) {
|
||||
$this->assertSame( 'Invalid user name', $ex->getMessage() );
|
||||
}
|
||||
|
||||
// User name that exists
|
||||
$user = User::newFromName( 'UTSysop' );
|
||||
$userinfo = UserInfo::newFromName( $user->getName() );
|
||||
$this->assertFalse( $userinfo->isAnon() );
|
||||
$this->assertFalse( $userinfo->isVerified() );
|
||||
$this->assertSame( $user->getId(), $userinfo->getId() );
|
||||
$this->assertSame( $user->getName(), $userinfo->getName() );
|
||||
$this->assertSame( $user->getToken( true ), $userinfo->getToken() );
|
||||
$this->assertInstanceOf( 'User', $userinfo->getUser() );
|
||||
$userinfo2 = $userinfo->verified();
|
||||
$this->assertNotSame( $userinfo2, $userinfo );
|
||||
$this->assertSame( "<-:{$user->getId()}:{$user->getName()}>", (string)$userinfo );
|
||||
|
||||
$this->assertFalse( $userinfo2->isAnon() );
|
||||
$this->assertTrue( $userinfo2->isVerified() );
|
||||
$this->assertSame( $user->getId(), $userinfo2->getId() );
|
||||
$this->assertSame( $user->getName(), $userinfo2->getName() );
|
||||
$this->assertSame( $user->getToken( true ), $userinfo2->getToken() );
|
||||
$this->assertInstanceOf( 'User', $userinfo2->getUser() );
|
||||
$this->assertSame( $userinfo2, $userinfo2->verified() );
|
||||
$this->assertSame( "<+:{$user->getId()}:{$user->getName()}>", (string)$userinfo2 );
|
||||
|
||||
$userinfo = UserInfo::newFromName( $user->getName(), true );
|
||||
$this->assertTrue( $userinfo->isVerified() );
|
||||
$this->assertSame( $userinfo, $userinfo->verified() );
|
||||
|
||||
// User name that does not exist should still be non-anon
|
||||
$user = User::newFromName( 'DoesNotExist' );
|
||||
$this->assertSame( 0, $user->getId(), 'sanity check' );
|
||||
$userinfo = UserInfo::newFromName( $user->getName() );
|
||||
$this->assertFalse( $userinfo->isAnon() );
|
||||
$this->assertFalse( $userinfo->isVerified() );
|
||||
$this->assertSame( $user->getId(), $userinfo->getId() );
|
||||
$this->assertSame( $user->getName(), $userinfo->getName() );
|
||||
$this->assertSame( null, $userinfo->getToken() );
|
||||
$this->assertInstanceOf( 'User', $userinfo->getUser() );
|
||||
$userinfo2 = $userinfo->verified();
|
||||
$this->assertNotSame( $userinfo2, $userinfo );
|
||||
$this->assertSame( "<-:{$user->getId()}:{$user->getName()}>", (string)$userinfo );
|
||||
|
||||
$this->assertFalse( $userinfo2->isAnon() );
|
||||
$this->assertTrue( $userinfo2->isVerified() );
|
||||
$this->assertSame( $user->getId(), $userinfo2->getId() );
|
||||
$this->assertSame( $user->getName(), $userinfo2->getName() );
|
||||
$this->assertSame( null, $userinfo2->getToken() );
|
||||
$this->assertInstanceOf( 'User', $userinfo2->getUser() );
|
||||
$this->assertSame( $userinfo2, $userinfo2->verified() );
|
||||
$this->assertSame( "<+:{$user->getId()}:{$user->getName()}>", (string)$userinfo2 );
|
||||
|
||||
$userinfo = UserInfo::newFromName( $user->getName(), true );
|
||||
$this->assertTrue( $userinfo->isVerified() );
|
||||
$this->assertSame( $userinfo, $userinfo->verified() );
|
||||
}
|
||||
|
||||
public function testNewFromUser() {
|
||||
// User that exists
|
||||
$user = User::newFromName( 'UTSysop' );
|
||||
$userinfo = UserInfo::newFromUser( $user );
|
||||
$this->assertFalse( $userinfo->isAnon() );
|
||||
$this->assertFalse( $userinfo->isVerified() );
|
||||
$this->assertSame( $user->getId(), $userinfo->getId() );
|
||||
$this->assertSame( $user->getName(), $userinfo->getName() );
|
||||
$this->assertSame( $user->getToken( true ), $userinfo->getToken() );
|
||||
$this->assertSame( $user, $userinfo->getUser() );
|
||||
$userinfo2 = $userinfo->verified();
|
||||
$this->assertNotSame( $userinfo2, $userinfo );
|
||||
$this->assertSame( "<-:{$user->getId()}:{$user->getName()}>", (string)$userinfo );
|
||||
|
||||
$this->assertFalse( $userinfo2->isAnon() );
|
||||
$this->assertTrue( $userinfo2->isVerified() );
|
||||
$this->assertSame( $user->getId(), $userinfo2->getId() );
|
||||
$this->assertSame( $user->getName(), $userinfo2->getName() );
|
||||
$this->assertSame( $user->getToken( true ), $userinfo2->getToken() );
|
||||
$this->assertSame( $user, $userinfo2->getUser() );
|
||||
$this->assertSame( $userinfo2, $userinfo2->verified() );
|
||||
$this->assertSame( "<+:{$user->getId()}:{$user->getName()}>", (string)$userinfo2 );
|
||||
|
||||
$userinfo = UserInfo::newFromUser( $user, true );
|
||||
$this->assertTrue( $userinfo->isVerified() );
|
||||
$this->assertSame( $userinfo, $userinfo->verified() );
|
||||
|
||||
// User name that does not exist should still be non-anon
|
||||
$user = User::newFromName( 'DoesNotExist' );
|
||||
$this->assertSame( 0, $user->getId(), 'sanity check' );
|
||||
$userinfo = UserInfo::newFromUser( $user );
|
||||
$this->assertFalse( $userinfo->isAnon() );
|
||||
$this->assertFalse( $userinfo->isVerified() );
|
||||
$this->assertSame( $user->getId(), $userinfo->getId() );
|
||||
$this->assertSame( $user->getName(), $userinfo->getName() );
|
||||
$this->assertSame( null, $userinfo->getToken() );
|
||||
$this->assertSame( $user, $userinfo->getUser() );
|
||||
$userinfo2 = $userinfo->verified();
|
||||
$this->assertNotSame( $userinfo2, $userinfo );
|
||||
$this->assertSame( "<-:{$user->getId()}:{$user->getName()}>", (string)$userinfo );
|
||||
|
||||
$this->assertFalse( $userinfo2->isAnon() );
|
||||
$this->assertTrue( $userinfo2->isVerified() );
|
||||
$this->assertSame( $user->getId(), $userinfo2->getId() );
|
||||
$this->assertSame( $user->getName(), $userinfo2->getName() );
|
||||
$this->assertSame( null, $userinfo2->getToken() );
|
||||
$this->assertSame( $user, $userinfo2->getUser() );
|
||||
$this->assertSame( $userinfo2, $userinfo2->verified() );
|
||||
$this->assertSame( "<+:{$user->getId()}:{$user->getName()}>", (string)$userinfo2 );
|
||||
|
||||
$userinfo = UserInfo::newFromUser( $user, true );
|
||||
$this->assertTrue( $userinfo->isVerified() );
|
||||
$this->assertSame( $userinfo, $userinfo->verified() );
|
||||
|
||||
// Anonymous user gives anon
|
||||
$userinfo = UserInfo::newFromUser( new User, false );
|
||||
$this->assertTrue( $userinfo->isVerified() );
|
||||
$this->assertSame( 0, $userinfo->getId() );
|
||||
$this->assertSame( null, $userinfo->getName() );
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -16,7 +16,6 @@ class UploadFromUrlTest extends ApiTestCase {
|
|||
'wgAllowCopyUploads' => true,
|
||||
'wgAllowAsyncCopyUploads' => true,
|
||||
) );
|
||||
wfSetupSession();
|
||||
|
||||
if ( wfLocalFile( 'UploadFromUrlTest.png' )->exists() ) {
|
||||
$this->deleteFile( 'UploadFromUrlTest.png' );
|
||||
|
|
@ -26,15 +25,12 @@ class UploadFromUrlTest extends ApiTestCase {
|
|||
protected function doApiRequest( array $params, array $unused = null,
|
||||
$appendModule = false, User $user = null
|
||||
) {
|
||||
$sessionId = session_id();
|
||||
session_write_close();
|
||||
global $wgRequest;
|
||||
|
||||
$req = new FauxRequest( $params, true, $_SESSION );
|
||||
$req = new FauxRequest( $params, true, $wgRequest->getSession() );
|
||||
$module = new ApiMain( $req, true );
|
||||
$module->execute();
|
||||
|
||||
wfSetupSession( $sessionId );
|
||||
|
||||
return array(
|
||||
$module->getResult()->getResultData( null, array( 'Strip' => 'all' ) ),
|
||||
$req
|
||||
|
|
|
|||
|
|
@ -446,89 +446,4 @@ class UserTest extends MediaWikiTestCase {
|
|||
$this->assertGreaterThan(
|
||||
$touched, $user->getDBTouched(), "user_touched increased with casOnTouched() #2" );
|
||||
}
|
||||
|
||||
public static function setExtendedLoginCookieDataProvider() {
|
||||
$data = array();
|
||||
$now = time();
|
||||
|
||||
$secondsInDay = 86400;
|
||||
|
||||
// Arbitrary durations, in units of days, to ensure it chooses the
|
||||
// right one. There is a 5-minute grace period (see testSetExtendedLoginCookie)
|
||||
// to work around slow tests, since we're not currently mocking time() for PHP.
|
||||
|
||||
$durationOne = $secondsInDay * 5;
|
||||
$durationTwo = $secondsInDay * 29;
|
||||
$durationThree = $secondsInDay * 17;
|
||||
|
||||
// If $wgExtendedLoginCookieExpiration is null, then the expiry passed to
|
||||
// set cookie is time() + $wgCookieExpiration
|
||||
$data[] = array(
|
||||
null,
|
||||
$durationOne,
|
||||
$now + $durationOne,
|
||||
);
|
||||
|
||||
// If $wgExtendedLoginCookieExpiration isn't null, then the expiry passed to
|
||||
// set cookie is $now + $wgExtendedLoginCookieExpiration
|
||||
$data[] = array(
|
||||
$durationTwo,
|
||||
$durationThree,
|
||||
$now + $durationTwo,
|
||||
);
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider setExtendedLoginCookieDataProvider
|
||||
* @covers User::getRequest
|
||||
* @covers User::setCookie
|
||||
* @backupGlobals enabled
|
||||
*/
|
||||
public function testSetExtendedLoginCookie(
|
||||
$extendedLoginCookieExpiration,
|
||||
$cookieExpiration,
|
||||
$expectedExpiry
|
||||
) {
|
||||
$this->setMwGlobals( array(
|
||||
'wgExtendedLoginCookieExpiration' => $extendedLoginCookieExpiration,
|
||||
'wgCookieExpiration' => $cookieExpiration,
|
||||
) );
|
||||
|
||||
$response = $this->getMock( 'WebResponse' );
|
||||
$setcookieSpy = $this->any();
|
||||
$response->expects( $setcookieSpy )
|
||||
->method( 'setcookie' );
|
||||
|
||||
$request = new MockWebRequest( $response );
|
||||
$user = new UserProxy( User::newFromSession( $request ) );
|
||||
$user->setExtendedLoginCookie( 'name', 'value', true );
|
||||
|
||||
$setcookieInvocations = $setcookieSpy->getInvocations();
|
||||
$setcookieInvocation = end( $setcookieInvocations );
|
||||
$actualExpiry = $setcookieInvocation->parameters[2];
|
||||
|
||||
// TODO: ± 600 seconds compensates for
|
||||
// slow-running tests. However, the dependency on the time
|
||||
// function should be removed. This requires some way
|
||||
// to mock/isolate User->setExtendedLoginCookie's call to time()
|
||||
$this->assertEquals( $expectedExpiry, $actualExpiry, '', 600 );
|
||||
}
|
||||
}
|
||||
|
||||
class UserProxy extends User {
|
||||
|
||||
/**
|
||||
* @var User
|
||||
*/
|
||||
protected $user;
|
||||
|
||||
public function __construct( User $user ) {
|
||||
$this->user = $user;
|
||||
}
|
||||
|
||||
public function setExtendedLoginCookie( $name, $value, $secure ) {
|
||||
$this->user->setExtendedLoginCookie( $name, $value, $secure );
|
||||
}
|
||||
}
|
||||
|
|
|
|||
29
tests/phpunit/mocks/session/DummySessionBackend.php
Normal file
29
tests/phpunit/mocks/session/DummySessionBackend.php
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
<?php
|
||||
|
||||
namespace MediaWiki\Session;
|
||||
|
||||
/**
|
||||
* Dummy session backend
|
||||
*
|
||||
* This isn't a real backend, but implements some methods that SessionBackend
|
||||
* does so tests can run.
|
||||
*/
|
||||
class DummySessionBackend {
|
||||
public $data = array(
|
||||
'foo' => 1,
|
||||
'bar' => 2,
|
||||
0 => 'zero',
|
||||
);
|
||||
public $dirty = false;
|
||||
|
||||
public function &getData() {
|
||||
return $this->data;
|
||||
}
|
||||
|
||||
public function dirty() {
|
||||
$this->dirty = true;
|
||||
}
|
||||
|
||||
public function deregisterSession( $index ) {
|
||||
}
|
||||
}
|
||||
60
tests/phpunit/mocks/session/DummySessionProvider.php
Normal file
60
tests/phpunit/mocks/session/DummySessionProvider.php
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
<?php
|
||||
use MediaWiki\Session\SessionProvider;
|
||||
use MediaWiki\Session\SessionInfo;
|
||||
use MediaWiki\Session\SessionBackend;
|
||||
use MediaWiki\Session\UserInfo;
|
||||
|
||||
/**
|
||||
* Dummy session provider
|
||||
*
|
||||
* An implementation of a session provider that doesn't actually do anything.
|
||||
*/
|
||||
class DummySessionProvider extends SessionProvider {
|
||||
|
||||
const ID = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa';
|
||||
|
||||
public function provideSessionInfo( WebRequest $request ) {
|
||||
return new SessionInfo( SessionInfo::MIN_PRIORITY, array(
|
||||
'provider' => $this,
|
||||
'id' => self::ID,
|
||||
'persisted' => true,
|
||||
'userInfo' => UserInfo::newAnonymous(),
|
||||
) );
|
||||
}
|
||||
|
||||
public function newSessionInfo( $id = null ) {
|
||||
return new SessionInfo( SessionInfo::MIN_PRIORITY, array(
|
||||
'id' => $id,
|
||||
'idIsSafe' => true,
|
||||
'provider' => $this,
|
||||
'persisted' => false,
|
||||
'userInfo' => UserInfo::newAnonymous(),
|
||||
) );
|
||||
}
|
||||
|
||||
public function persistsSessionId() {
|
||||
return true;
|
||||
}
|
||||
|
||||
public function canChangeUser() {
|
||||
return $this->persistsSessionId();
|
||||
}
|
||||
|
||||
public function persistSession( SessionBackend $session, WebRequest $request ) {
|
||||
}
|
||||
|
||||
public function unpersistSession( WebRequest $request ) {
|
||||
}
|
||||
|
||||
public function immutableSessionCouldExistForUser( $user ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
public function preventImmutableSessionsForUser( $user ) {
|
||||
}
|
||||
|
||||
public function suggestLoginUsername( WebRequest $request ) {
|
||||
return $request->getCookie( 'UserName' );
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -73,6 +73,7 @@ class PHPUnitMaintClass extends Maintenance {
|
|||
global $wgLanguageConverterCacheType, $wgUseDatabaseMessages;
|
||||
global $wgLocaltimezone, $wgLocalisationCacheConf;
|
||||
global $wgDevelopmentWarnings;
|
||||
global $wgSessionProviders;
|
||||
|
||||
// Inject test autoloader
|
||||
require_once __DIR__ . '/../TestsAutoLoader.php';
|
||||
|
|
@ -103,6 +104,19 @@ class PHPUnitMaintClass extends Maintenance {
|
|||
|
||||
$wgLocalisationCacheConf['storeClass'] = 'LCStoreNull';
|
||||
|
||||
// Generic MediaWiki\Session\SessionManager configuration for tests
|
||||
// We use CookieSessionProvider because things might be expecting
|
||||
// cookies to show up in a FauxRequest somewhere.
|
||||
$wgSessionProviders = array(
|
||||
array(
|
||||
'class' => 'MediaWiki\\Session\\CookieSessionProvider',
|
||||
'args' => array( array(
|
||||
'priority' => 30,
|
||||
'callUserSetCookiesHook' => true,
|
||||
) ),
|
||||
),
|
||||
);
|
||||
|
||||
// Bug 44192 Do not attempt to send a real e-mail
|
||||
Hooks::clear( 'AlternateUserMailer' );
|
||||
Hooks::register(
|
||||
|
|
|
|||
Loading…
Reference in a new issue