Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.
Common duplication problems, and corresponding solutions are:
Complex classes like SessionManager often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.
Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.
While breaking up the class, it is a good idea to analyze how other classes use SessionManager, and based on these observations, apply Extract Interface, too.
| 1 | <?php |
||
| 41 | final class SessionManager implements SessionManagerInterface { |
||
| 42 | /** @var SessionManager|null */ |
||
| 43 | private static $instance = null; |
||
| 44 | |||
| 45 | /** @var Session|null */ |
||
| 46 | private static $globalSession = null; |
||
| 47 | |||
| 48 | /** @var WebRequest|null */ |
||
| 49 | private static $globalSessionRequest = null; |
||
| 50 | |||
| 51 | /** @var LoggerInterface */ |
||
| 52 | private $logger; |
||
| 53 | |||
| 54 | /** @var Config */ |
||
| 55 | private $config; |
||
| 56 | |||
| 57 | /** @var CachedBagOStuff|null */ |
||
| 58 | private $store; |
||
| 59 | |||
| 60 | /** @var SessionProvider[] */ |
||
| 61 | private $sessionProviders = null; |
||
| 62 | |||
| 63 | /** @var string[] */ |
||
| 64 | private $varyCookies = null; |
||
| 65 | |||
| 66 | /** @var array */ |
||
| 67 | private $varyHeaders = null; |
||
| 68 | |||
| 69 | /** @var SessionBackend[] */ |
||
| 70 | private $allSessionBackends = []; |
||
| 71 | |||
| 72 | /** @var SessionId[] */ |
||
| 73 | private $allSessionIds = []; |
||
| 74 | |||
| 75 | /** @var string[] */ |
||
| 76 | private $preventUsers = []; |
||
| 77 | |||
| 78 | /** |
||
| 79 | * Get the global SessionManager |
||
| 80 | * @return SessionManagerInterface |
||
| 81 | * (really a SessionManager, but this is to make IDEs less confused) |
||
| 82 | */ |
||
| 83 | public static function singleton() { |
||
| 84 | if ( self::$instance === null ) { |
||
| 85 | self::$instance = new self(); |
||
| 86 | } |
||
| 87 | return self::$instance; |
||
| 88 | } |
||
| 89 | |||
| 90 | /** |
||
| 91 | * Get the "global" session |
||
| 92 | * |
||
| 93 | * If PHP's session_id() has been set, returns that session. Otherwise |
||
| 94 | * returns the session for RequestContext::getMain()->getRequest(). |
||
| 95 | * |
||
| 96 | * @return Session |
||
| 97 | */ |
||
| 98 | public static function getGlobalSession() { |
||
| 99 | if ( !PHPSessionHandler::isEnabled() ) { |
||
| 100 | $id = ''; |
||
| 101 | } else { |
||
| 102 | $id = session_id(); |
||
| 103 | } |
||
| 104 | |||
| 105 | $request = \RequestContext::getMain()->getRequest(); |
||
| 106 | if ( |
||
| 107 | !self::$globalSession // No global session is set up yet |
||
| 108 | || self::$globalSessionRequest !== $request // The global WebRequest changed |
||
| 109 | || $id !== '' && self::$globalSession->getId() !== $id // Someone messed with session_id() |
||
| 110 | ) { |
||
| 111 | self::$globalSessionRequest = $request; |
||
| 112 | if ( $id === '' ) { |
||
| 113 | // session_id() wasn't used, so fetch the Session from the WebRequest. |
||
| 114 | // We use $request->getSession() instead of $singleton->getSessionForRequest() |
||
| 115 | // because doing the latter would require a public |
||
| 116 | // "$request->getSessionId()" method that would confuse end |
||
| 117 | // users by returning SessionId|null where they'd expect it to |
||
| 118 | // be short for $request->getSession()->getId(), and would |
||
| 119 | // wind up being a duplicate of the code in |
||
| 120 | // $request->getSession() anyway. |
||
| 121 | self::$globalSession = $request->getSession(); |
||
| 122 | } else { |
||
| 123 | // Someone used session_id(), so we need to follow suit. |
||
| 124 | // Note this overwrites whatever session might already be |
||
| 125 | // associated with $request with the one for $id. |
||
| 126 | self::$globalSession = self::singleton()->getSessionById( $id, true, $request ) |
||
| 127 | ?: $request->getSession(); |
||
| 128 | } |
||
| 129 | } |
||
| 130 | return self::$globalSession; |
||
| 131 | } |
||
| 132 | |||
| 133 | /** |
||
| 134 | * @param array $options |
||
| 135 | * - config: Config to fetch configuration from. Defaults to the default 'main' config. |
||
| 136 | * - logger: LoggerInterface to use for logging. Defaults to the 'session' channel. |
||
| 137 | * - store: BagOStuff to store session data in. |
||
| 138 | */ |
||
| 139 | public function __construct( $options = [] ) { |
||
| 140 | if ( isset( $options['config'] ) ) { |
||
| 141 | $this->config = $options['config']; |
||
| 142 | if ( !$this->config instanceof Config ) { |
||
| 143 | throw new \InvalidArgumentException( |
||
| 144 | '$options[\'config\'] must be an instance of Config' |
||
| 145 | ); |
||
| 146 | } |
||
| 147 | } else { |
||
| 148 | $this->config = \ConfigFactory::getDefaultInstance()->makeConfig( 'main' ); |
||
|
|
|||
| 149 | } |
||
| 150 | |||
| 151 | if ( isset( $options['logger'] ) ) { |
||
| 152 | if ( !$options['logger'] instanceof LoggerInterface ) { |
||
| 153 | throw new \InvalidArgumentException( |
||
| 154 | '$options[\'logger\'] must be an instance of LoggerInterface' |
||
| 155 | ); |
||
| 156 | } |
||
| 157 | $this->setLogger( $options['logger'] ); |
||
| 158 | } else { |
||
| 159 | $this->setLogger( \MediaWiki\Logger\LoggerFactory::getInstance( 'session' ) ); |
||
| 160 | } |
||
| 161 | |||
| 162 | if ( isset( $options['store'] ) ) { |
||
| 163 | if ( !$options['store'] instanceof BagOStuff ) { |
||
| 164 | throw new \InvalidArgumentException( |
||
| 165 | '$options[\'store\'] must be an instance of BagOStuff' |
||
| 166 | ); |
||
| 167 | } |
||
| 168 | $store = $options['store']; |
||
| 169 | } else { |
||
| 170 | $store = \ObjectCache::getInstance( $this->config->get( 'SessionCacheType' ) ); |
||
| 171 | } |
||
| 172 | $this->store = $store instanceof CachedBagOStuff ? $store : new CachedBagOStuff( $store ); |
||
| 173 | |||
| 174 | register_shutdown_function( [ $this, 'shutdown' ] ); |
||
| 175 | } |
||
| 176 | |||
| 177 | public function setLogger( LoggerInterface $logger ) { |
||
| 178 | $this->logger = $logger; |
||
| 179 | } |
||
| 180 | |||
| 181 | public function getSessionForRequest( WebRequest $request ) { |
||
| 182 | $info = $this->getSessionInfoForRequest( $request ); |
||
| 183 | |||
| 184 | if ( !$info ) { |
||
| 185 | $session = $this->getEmptySession( $request ); |
||
| 186 | } else { |
||
| 187 | $session = $this->getSessionFromInfo( $info, $request ); |
||
| 188 | } |
||
| 189 | return $session; |
||
| 190 | } |
||
| 191 | |||
| 192 | public function getSessionById( $id, $create = false, WebRequest $request = null ) { |
||
| 193 | if ( !self::validateSessionId( $id ) ) { |
||
| 194 | throw new \InvalidArgumentException( 'Invalid session ID' ); |
||
| 195 | } |
||
| 196 | if ( !$request ) { |
||
| 197 | $request = new FauxRequest; |
||
| 198 | } |
||
| 199 | |||
| 200 | $session = null; |
||
| 201 | $info = new SessionInfo( SessionInfo::MIN_PRIORITY, [ 'id' => $id, 'idIsSafe' => true ] ); |
||
| 202 | |||
| 203 | // If we already have the backend loaded, use it directly |
||
| 204 | if ( isset( $this->allSessionBackends[$id] ) ) { |
||
| 205 | return $this->getSessionFromInfo( $info, $request ); |
||
| 206 | } |
||
| 207 | |||
| 208 | // Test if the session is in storage, and if so try to load it. |
||
| 209 | $key = wfMemcKey( 'MWSession', $id ); |
||
| 210 | if ( is_array( $this->store->get( $key ) ) ) { |
||
| 211 | $create = false; // If loading fails, don't bother creating because it probably will fail too. |
||
| 212 | if ( $this->loadSessionInfoFromStore( $info, $request ) ) { |
||
| 213 | $session = $this->getSessionFromInfo( $info, $request ); |
||
| 214 | } |
||
| 215 | } |
||
| 216 | |||
| 217 | if ( $create && $session === null ) { |
||
| 218 | $ex = null; |
||
| 219 | try { |
||
| 220 | $session = $this->getEmptySessionInternal( $request, $id ); |
||
| 221 | } catch ( \Exception $ex ) { |
||
| 222 | $this->logger->error( 'Failed to create empty session: {exception}', |
||
| 223 | [ |
||
| 224 | 'method' => __METHOD__, |
||
| 225 | 'exception' => $ex, |
||
| 226 | ] ); |
||
| 227 | $session = null; |
||
| 228 | } |
||
| 229 | } |
||
| 230 | |||
| 231 | return $session; |
||
| 232 | } |
||
| 233 | |||
| 234 | public function getEmptySession( WebRequest $request = null ) { |
||
| 235 | return $this->getEmptySessionInternal( $request ); |
||
| 236 | } |
||
| 237 | |||
| 238 | /** |
||
| 239 | * @see SessionManagerInterface::getEmptySession |
||
| 240 | * @param WebRequest|null $request |
||
| 241 | * @param string|null $id ID to force on the new session |
||
| 242 | * @return Session |
||
| 243 | */ |
||
| 244 | private function getEmptySessionInternal( WebRequest $request = null, $id = null ) { |
||
| 245 | if ( $id !== null ) { |
||
| 246 | if ( !self::validateSessionId( $id ) ) { |
||
| 247 | throw new \InvalidArgumentException( 'Invalid session ID' ); |
||
| 248 | } |
||
| 249 | |||
| 250 | $key = wfMemcKey( 'MWSession', $id ); |
||
| 251 | if ( is_array( $this->store->get( $key ) ) ) { |
||
| 252 | throw new \InvalidArgumentException( 'Session ID already exists' ); |
||
| 253 | } |
||
| 254 | } |
||
| 255 | if ( !$request ) { |
||
| 256 | $request = new FauxRequest; |
||
| 257 | } |
||
| 258 | |||
| 259 | $infos = []; |
||
| 260 | foreach ( $this->getProviders() as $provider ) { |
||
| 261 | $info = $provider->newSessionInfo( $id ); |
||
| 262 | if ( !$info ) { |
||
| 263 | continue; |
||
| 264 | } |
||
| 265 | if ( $info->getProvider() !== $provider ) { |
||
| 266 | throw new \UnexpectedValueException( |
||
| 267 | "$provider returned an empty session info for a different provider: $info" |
||
| 268 | ); |
||
| 269 | } |
||
| 270 | if ( $id !== null && $info->getId() !== $id ) { |
||
| 271 | throw new \UnexpectedValueException( |
||
| 272 | "$provider returned empty session info with a wrong id: " . |
||
| 273 | $info->getId() . ' != ' . $id |
||
| 274 | ); |
||
| 275 | } |
||
| 276 | if ( !$info->isIdSafe() ) { |
||
| 277 | throw new \UnexpectedValueException( |
||
| 278 | "$provider returned empty session info with id flagged unsafe" |
||
| 279 | ); |
||
| 280 | } |
||
| 281 | $compare = $infos ? SessionInfo::compare( $infos[0], $info ) : -1; |
||
| 282 | if ( $compare > 0 ) { |
||
| 283 | continue; |
||
| 284 | } |
||
| 285 | if ( $compare === 0 ) { |
||
| 286 | $infos[] = $info; |
||
| 287 | } else { |
||
| 288 | $infos = [ $info ]; |
||
| 289 | } |
||
| 290 | } |
||
| 291 | |||
| 292 | // Make sure there's exactly one |
||
| 293 | if ( count( $infos ) > 1 ) { |
||
| 294 | throw new \UnexpectedValueException( |
||
| 295 | 'Multiple empty sessions tied for top priority: ' . implode( ', ', $infos ) |
||
| 296 | ); |
||
| 297 | } elseif ( count( $infos ) < 1 ) { |
||
| 298 | throw new \UnexpectedValueException( 'No provider could provide an empty session!' ); |
||
| 299 | } |
||
| 300 | |||
| 301 | return $this->getSessionFromInfo( $infos[0], $request ); |
||
| 302 | } |
||
| 303 | |||
| 304 | public function invalidateSessionsForUser( User $user ) { |
||
| 305 | $user->setToken(); |
||
| 306 | $user->saveSettings(); |
||
| 307 | |||
| 308 | $authUser = \MediaWiki\Auth\AuthManager::callLegacyAuthPlugin( 'getUserInstance', [ &$user ] ); |
||
| 309 | if ( $authUser ) { |
||
| 310 | $authUser->resetAuthToken(); |
||
| 311 | } |
||
| 312 | |||
| 313 | foreach ( $this->getProviders() as $provider ) { |
||
| 314 | $provider->invalidateSessionsForUser( $user ); |
||
| 315 | } |
||
| 316 | } |
||
| 317 | |||
| 318 | public function getVaryHeaders() { |
||
| 319 | // @codeCoverageIgnoreStart |
||
| 320 | if ( defined( 'MW_NO_SESSION' ) && MW_NO_SESSION !== 'warn' ) { |
||
| 321 | return []; |
||
| 322 | } |
||
| 323 | // @codeCoverageIgnoreEnd |
||
| 324 | if ( $this->varyHeaders === null ) { |
||
| 325 | $headers = []; |
||
| 326 | foreach ( $this->getProviders() as $provider ) { |
||
| 327 | foreach ( $provider->getVaryHeaders() as $header => $options ) { |
||
| 328 | if ( !isset( $headers[$header] ) ) { |
||
| 329 | $headers[$header] = []; |
||
| 330 | } |
||
| 331 | if ( is_array( $options ) ) { |
||
| 332 | $headers[$header] = array_unique( array_merge( $headers[$header], $options ) ); |
||
| 333 | } |
||
| 334 | } |
||
| 335 | } |
||
| 336 | $this->varyHeaders = $headers; |
||
| 337 | } |
||
| 338 | return $this->varyHeaders; |
||
| 339 | } |
||
| 340 | |||
| 341 | public function getVaryCookies() { |
||
| 342 | // @codeCoverageIgnoreStart |
||
| 343 | if ( defined( 'MW_NO_SESSION' ) && MW_NO_SESSION !== 'warn' ) { |
||
| 344 | return []; |
||
| 345 | } |
||
| 346 | // @codeCoverageIgnoreEnd |
||
| 347 | if ( $this->varyCookies === null ) { |
||
| 348 | $cookies = []; |
||
| 349 | foreach ( $this->getProviders() as $provider ) { |
||
| 350 | $cookies = array_merge( $cookies, $provider->getVaryCookies() ); |
||
| 351 | } |
||
| 352 | $this->varyCookies = array_values( array_unique( $cookies ) ); |
||
| 353 | } |
||
| 354 | return $this->varyCookies; |
||
| 355 | } |
||
| 356 | |||
| 357 | /** |
||
| 358 | * Validate a session ID |
||
| 359 | * @param string $id |
||
| 360 | * @return bool |
||
| 361 | */ |
||
| 362 | public static function validateSessionId( $id ) { |
||
| 363 | return is_string( $id ) && preg_match( '/^[a-zA-Z0-9_-]{32,}$/', $id ); |
||
| 364 | } |
||
| 365 | |||
| 366 | /** |
||
| 367 | * @name Internal methods |
||
| 368 | * @{ |
||
| 369 | */ |
||
| 370 | |||
| 371 | /** |
||
| 372 | * Auto-create the given user, if necessary |
||
| 373 | * @private Don't call this yourself. Let Setup.php do it for you at the right time. |
||
| 374 | * @deprecated since 1.27, use MediaWiki\Auth\AuthManager::autoCreateUser instead |
||
| 375 | * @param User $user User to auto-create |
||
| 376 | * @return bool Success |
||
| 377 | */ |
||
| 378 | public static function autoCreateUser( User $user ) { |
||
| 574 | |||
| 575 | /** |
||
| 576 | * Prevent future sessions for the user |
||
| 577 | * |
||
| 578 | * The intention is that the named account will never again be usable for |
||
| 579 | * normal login (i.e. there is no way to undo the prevention of access). |
||
| 580 | * |
||
| 581 | * @private For use from \User::newSystemUser only |
||
| 582 | * @param string $username |
||
| 583 | */ |
||
| 584 | public function preventSessionsForUser( $username ) { |
||
| 585 | $this->preventUsers[$username] = true; |
||
| 586 | |||
| 592 | |||
| 593 | /** |
||
| 594 | * Test if a user is prevented |
||
| 595 | * @private For use from SessionBackend only |
||
| 596 | * @param string $username |
||
| 597 | * @return bool |
||
| 598 | */ |
||
| 599 | public function isUserSessionPrevented( $username ) { |
||
| 602 | |||
| 603 | /** |
||
| 604 | * Get the available SessionProviders |
||
| 605 | * @return SessionProvider[] |
||
| 606 | */ |
||
| 607 | protected function getProviders() { |
||
| 623 | |||
| 624 | /** |
||
| 625 | * Get a session provider by name |
||
| 626 | * |
||
| 627 | * Generally, this will only be used by internal implementation of some |
||
| 628 | * special session-providing mechanism. General purpose code, if it needs |
||
| 629 | * to access a SessionProvider at all, will use Session::getProvider(). |
||
| 630 | * |
||
| 631 | * @param string $name |
||
| 632 | * @return SessionProvider|null |
||
| 633 | */ |
||
| 634 | public function getProvider( $name ) { |
||
| 638 | |||
| 639 | /** |
||
| 640 | * Save all active sessions on shutdown |
||
| 641 | * @private For internal use with register_shutdown_function() |
||
| 642 | */ |
||
| 643 | public function shutdown() { |
||
| 656 | |||
| 657 | /** |
||
| 658 | * Fetch the SessionInfo(s) for a request |
||
| 659 | * @param WebRequest $request |
||
| 660 | * @return SessionInfo|null |
||
| 661 | */ |
||
| 662 | private function getSessionInfoForRequest( WebRequest $request ) { |
||
| 718 | |||
| 719 | /** |
||
| 720 | * Load and verify the session info against the store |
||
| 721 | * |
||
| 722 | * @param SessionInfo &$info Will likely be replaced with an updated SessionInfo instance |
||
| 723 | * @param WebRequest $request |
||
| 724 | * @return bool Whether the session info matches the stored data (if any) |
||
| 725 | */ |
||
| 726 | private function loadSessionInfoFromStore( SessionInfo &$info, WebRequest $request ) { |
||
| 1007 | |||
| 1008 | /** |
||
| 1009 | * Create a session corresponding to the passed SessionInfo |
||
| 1010 | * @private For use by a SessionProvider that needs to specially create its |
||
| 1011 | * own session. |
||
| 1012 | * @param SessionInfo $info |
||
| 1013 | * @param WebRequest $request |
||
| 1014 | * @return Session |
||
| 1015 | */ |
||
| 1016 | public function getSessionFromInfo( SessionInfo $info, WebRequest $request ) { |
||
| 1066 | |||
| 1067 | /** |
||
| 1068 | * Deregister a SessionBackend |
||
| 1069 | * @private For use from \MediaWiki\Session\SessionBackend only |
||
| 1070 | * @param SessionBackend $backend |
||
| 1071 | */ |
||
| 1072 | public function deregisterSessionBackend( SessionBackend $backend ) { |
||
| 1084 | |||
| 1085 | /** |
||
| 1086 | * Change a SessionBackend's ID |
||
| 1087 | * @private For use from \MediaWiki\Session\SessionBackend only |
||
| 1088 | * @param SessionBackend $backend |
||
| 1089 | */ |
||
| 1090 | public function changeBackendId( SessionBackend $backend ) { |
||
| 1107 | |||
| 1108 | /** |
||
| 1109 | * Generate a new random session ID |
||
| 1110 | * @return string |
||
| 1111 | */ |
||
| 1112 | public function generateSessionId() { |
||
| 1119 | |||
| 1120 | /** |
||
| 1121 | * Call setters on a PHPSessionHandler |
||
| 1122 | * @private Use PhpSessionHandler::install() |
||
| 1123 | * @param PHPSessionHandler $handler |
||
| 1124 | */ |
||
| 1125 | public function setupPHPSessionHandler( PHPSessionHandler $handler ) { |
||
| 1128 | |||
| 1129 | /** |
||
| 1130 | * Reset the internal caching for unit testing |
||
| 1131 | */ |
||
| 1132 | public static function resetCache() { |
||
| 1142 | |||
| 1143 | /**@}*/ |
||
| 1144 | |||
| 1145 | } |
||
| 1146 |
This method has been deprecated. The supplier of the class has supplied an explanatory message.
The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.