And deprecated aliases for the the no namespaced classes. ReplicatedBagOStuff that already is deprecated isn't moved. Bug: T353458 Change-Id: Ie01962517e5b53e59b9721e9996d4f1ea95abb51
383 lines
11 KiB
PHP
383 lines
11 KiB
PHP
<?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 MediaWiki\MainConfigNames;
|
|
use MediaWiki\MediaWikiServices;
|
|
use Psr\Log\LoggerInterface;
|
|
use Psr\Log\NullLogger;
|
|
use SessionHandlerInterface;
|
|
use Wikimedia\AtEase\AtEase;
|
|
use Wikimedia\ObjectCache\BagOStuff;
|
|
use Wikimedia\PhpSessionSerializer;
|
|
|
|
/**
|
|
* Adapter for PHP's session handling
|
|
* @ingroup Session
|
|
* @since 1.27
|
|
*/
|
|
class PHPSessionHandler implements SessionHandlerInterface {
|
|
/** @var PHPSessionHandler */
|
|
protected static $instance = null;
|
|
|
|
/** @var bool Whether PHP session handling is enabled */
|
|
protected $enable = false;
|
|
|
|
/** @var bool */
|
|
protected $warn = true;
|
|
|
|
/** @var SessionManagerInterface|null */
|
|
protected $manager;
|
|
|
|
/** @var BagOStuff|null */
|
|
protected $store;
|
|
|
|
/** @var LoggerInterface */
|
|
protected $logger;
|
|
|
|
/** @var array Track original session fields for later modification check */
|
|
protected $sessionFieldCache = [];
|
|
|
|
protected function __construct( SessionManager $manager ) {
|
|
$this->setEnableFlags(
|
|
MediaWikiServices::getInstance()->getMainConfig()->get( MainConfigNames::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;
|
|
}
|
|
|
|
// @codeCoverageIgnoreStart
|
|
if ( defined( 'MW_NO_SESSION_HANDLER' ) ) {
|
|
throw new \BadMethodCallException( 'MW_NO_SESSION_HANDLER is defined' );
|
|
}
|
|
// @codeCoverageIgnoreEnd
|
|
|
|
self::$instance = new self( $manager );
|
|
|
|
// Close any auto-started session, before we replace it
|
|
session_write_close();
|
|
|
|
try {
|
|
AtEase::suppressWarnings();
|
|
|
|
// Tell PHP not to mess with cookies itself
|
|
ini_set( 'session.use_cookies', 0 );
|
|
ini_set( 'session.use_trans_sid', 0 );
|
|
|
|
// T124510: Disable automatic PHP session related cache headers.
|
|
// MediaWiki adds its own headers and the default PHP behavior may
|
|
// set headers such as 'Pragma: no-cache' that cause problems with
|
|
// some user agents.
|
|
session_cache_limiter( '' );
|
|
|
|
// Also set a serialization handler
|
|
PhpSessionSerializer::setSerializeHandler();
|
|
|
|
// Register this as the save handler, and register an appropriate
|
|
// shutdown function.
|
|
session_set_save_handler( self::$instance, true );
|
|
} finally {
|
|
AtEase::restoreWarnings();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Set the manager, store, and logger
|
|
* @internal Use self::install().
|
|
* @param SessionManagerInterface $manager
|
|
* @param BagOStuff $store
|
|
* @param LoggerInterface $logger
|
|
*/
|
|
public function setManager(
|
|
SessionManagerInterface $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;
|
|
PhpSessionSerializer::setLogger( $this->logger );
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Initialize the session (handler)
|
|
* @internal For internal use only
|
|
* @param string $save_path Path used to store session files (ignored)
|
|
* @param string $session_name Session name (ignored)
|
|
* @return true
|
|
*/
|
|
#[\ReturnTypeWillChange]
|
|
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)
|
|
* @internal For internal use only
|
|
* @return true
|
|
*/
|
|
#[\ReturnTypeWillChange]
|
|
public function close() {
|
|
if ( self::$instance !== $this ) {
|
|
throw new \UnexpectedValueException( __METHOD__ . ': Wrong instance called!' );
|
|
}
|
|
$this->sessionFieldCache = [];
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Read session data
|
|
* @internal For internal use only
|
|
* @param string $id Session id
|
|
* @return string Session data
|
|
*/
|
|
#[\ReturnTypeWillChange]
|
|
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, false );
|
|
if ( !$session ) {
|
|
return '';
|
|
}
|
|
$session->persist();
|
|
|
|
$data = iterator_to_array( $session );
|
|
$this->sessionFieldCache[$id] = $data;
|
|
return (string)PhpSessionSerializer::encode( $data );
|
|
}
|
|
|
|
/**
|
|
* Write session data
|
|
* @internal 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
|
|
*/
|
|
#[\ReturnTypeWillChange]
|
|
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, true );
|
|
if ( !$session ) {
|
|
// This can happen under normal circumstances, if the session exists but is
|
|
// invalid. Let's emit a log warning instead of a PHP warning.
|
|
$this->logger->warning(
|
|
__METHOD__ . ': Session "{session}" cannot be loaded, skipping write.',
|
|
[
|
|
'session' => $id,
|
|
] );
|
|
return true;
|
|
}
|
|
|
|
// First, decode the string PHP handed us
|
|
$data = PhpSessionSerializer::decode( $dataStr );
|
|
if ( $data === null ) {
|
|
// @codeCoverageIgnoreStart
|
|
return false;
|
|
// @codeCoverageIgnoreEnd
|
|
}
|
|
|
|
// Now merge the data into the Session object.
|
|
$changed = false;
|
|
$cache = $this->sessionFieldCache[$id] ?? [];
|
|
foreach ( $data as $key => $value ) {
|
|
if ( !array_key_exists( $key, $cache ) ) {
|
|
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)
|
|
PhpSessionSerializer::setLogger( new NullLogger() );
|
|
foreach ( $cache as $key => $value ) {
|
|
if ( !array_key_exists( $key, $data ) && $session->exists( $key ) &&
|
|
PhpSessionSerializer::encode( [ $key => true ] )
|
|
) {
|
|
if ( $value === $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!"
|
|
);
|
|
}
|
|
}
|
|
}
|
|
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
|
|
* @internal For internal use only
|
|
* @param string $id Session id
|
|
* @return true
|
|
*/
|
|
#[\ReturnTypeWillChange]
|
|
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, false );
|
|
if ( $session ) {
|
|
$session->clear();
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Execute garbage collection.
|
|
* @internal For internal use only
|
|
* @param int $maxlifetime Maximum session life time (ignored)
|
|
* @return true
|
|
* @codeCoverageIgnore See T135576
|
|
*/
|
|
#[\ReturnTypeWillChange]
|
|
public function gc( $maxlifetime ) {
|
|
if ( self::$instance !== $this ) {
|
|
throw new \UnexpectedValueException( __METHOD__ . ': Wrong instance called!' );
|
|
}
|
|
$this->store->deleteObjectsExpiringBefore( wfTimestampNow() );
|
|
return true;
|
|
}
|
|
}
|