X-Git-Url: https://scripts.mit.edu/gitweb/autoinstallsdev/mediawiki.git/blobdiff_plain/19e297c21b10b1b8a3acad5e73fc71dcb35db44a..6932310fd58ebef145fa01eb76edf7150284d8ea:/includes/session/PHPSessionHandler.php diff --git a/includes/session/PHPSessionHandler.php b/includes/session/PHPSessionHandler.php new file mode 100644 index 00000000..b76f0ff6 --- /dev/null +++ b/includes/session/PHPSessionHandler.php @@ -0,0 +1,391 @@ +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; + } + + // @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(); + + // 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 it's 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 sane serialization handler + \Wikimedia\PhpSessionSerializer::setSerializeHandler(); + + // Register this as the save handler, and register an appropriate + // shutdown function. + session_set_save_handler( self::$instance, true ); + } + + /** + * Set the manager, store, and logger + * @private Use self::install(). + * @param SessionManager $manager + * @param BagOStuff $store + * @param LoggerInterface $logger + */ + 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 ); + } + } + + /** + * Workaround for PHP5 bug + * + * PHP5 has a bug in handling boolean return values for + * SessionHandlerInterface methods, it expects 0 or -1 instead of true or + * false. See . + * + * PHP7 and HHVM are not affected. + * + * @todo When we drop support for Zend PHP 5, this can be removed. + * @return bool|int + * @codeCoverageIgnore + */ + protected static function returnSuccess() { + return defined( 'HHVM_VERSION' ) || version_compare( PHP_VERSION, '7.0.0', '>=' ) ? true : 0; + } + + /** + * Workaround for PHP5 bug + * @see self::returnSuccess() + * @return bool|int + * @codeCoverageIgnore + */ + protected static function returnFailure() { + return defined( 'HHVM_VERSION' ) || version_compare( PHP_VERSION, '7.0.0', '>=' ) ? false : -1; + } + + /** + * 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|int Success (see self::returnSuccess()) + */ + 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 self::returnSuccess(); + } + + /** + * Close the session (handler) + * @private For internal use only + * @return bool|int Success (see self::returnSuccess()) + */ + public function close() { + if ( self::$instance !== $this ) { + throw new \UnexpectedValueException( __METHOD__ . ': Wrong instance called!' ); + } + $this->sessionFieldCache = []; + return self::returnSuccess(); + } + + /** + * 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, false ); + 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|int Success (see self::returnSuccess()) + */ + 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 self::returnSuccess(); + } + + // First, decode the string PHP handed us + $data = \Wikimedia\PhpSessionSerializer::decode( $dataStr ); + if ( $data === null ) { + // @codeCoverageIgnoreStart + return self::returnFailure(); + // @codeCoverageIgnoreEnd + } + + // Now merge the data into the Session object. + $changed = false; + $cache = isset( $this->sessionFieldCache[$id] ) ? $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) + \Wikimedia\PhpSessionSerializer::setLogger( new \Psr\Log\NullLogger() ); + foreach ( $cache as $key => $value ) { + if ( !array_key_exists( $key, $data ) && $session->exists( $key ) && + \Wikimedia\PhpSessionSerializer::encode( [ $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 self::returnSuccess(); + } + + /** + * Destroy a session + * @private For internal use only + * @param string $id Session id + * @return bool|int Success (see self::returnSuccess()) + */ + 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 self::returnSuccess(); + } + + /** + * Execute garbage collection. + * @private For internal use only + * @param int $maxlifetime Maximum session life time (ignored) + * @return bool|int Success (see self::returnSuccess()) + * @codeCoverageIgnore See T135576 + */ + public function gc( $maxlifetime ) { + if ( self::$instance !== $this ) { + throw new \UnexpectedValueException( __METHOD__ . ': Wrong instance called!' ); + } + $before = date( 'YmdHis', time() ); + $this->store->deleteObjectsExpiringBefore( $before ); + return self::returnSuccess(); + } +}