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() { |
||
| 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() { |
||
| 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 = [] ) { |
||
| 176 | |||
| 177 | public function setLogger( LoggerInterface $logger ) { |
||
| 180 | |||
| 181 | public function getSessionForRequest( WebRequest $request ) { |
||
| 191 | |||
| 192 | public function getSessionById( $id, $create = false, WebRequest $request = null ) { |
||
| 233 | |||
| 234 | public function getEmptySession( WebRequest $request = null ) { |
||
| 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 ) { |
||
| 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() { |
||
| 340 | |||
| 341 | public function getVaryCookies() { |
||
| 356 | |||
| 357 | /** |
||
| 358 | * Validate a session ID |
||
| 359 | * @param string $id |
||
| 360 | * @return bool |
||
| 361 | */ |
||
| 362 | public static function validateSessionId( $id ) { |
||
| 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 ) { |
||
| 379 | global $wgAuth, $wgDisableAuthManager; |
||
| 380 | |||
| 381 | // @codeCoverageIgnoreStart |
||
| 382 | if ( !$wgDisableAuthManager ) { |
||
| 383 | wfDeprecated( __METHOD__, '1.27' ); |
||
| 384 | return \MediaWiki\Auth\AuthManager::singleton()->autoCreateUser( |
||
| 385 | $user, |
||
| 386 | \MediaWiki\Auth\AuthManager::AUTOCREATE_SOURCE_SESSION, |
||
| 387 | false |
||
| 388 | )->isGood(); |
||
| 389 | } |
||
| 390 | // @codeCoverageIgnoreEnd |
||
| 391 | |||
| 392 | $logger = self::singleton()->logger; |
||
| 393 | |||
| 394 | // Much of this code is based on that in CentralAuth |
||
| 395 | |||
| 396 | // Try the local user from the slave DB |
||
| 397 | $localId = User::idFromName( $user->getName() ); |
||
| 398 | $flags = 0; |
||
| 399 | |||
| 400 | // Fetch the user ID from the master, so that we don't try to create the user |
||
| 401 | // when they already exist, due to replication lag |
||
| 402 | // @codeCoverageIgnoreStart |
||
| 403 | View Code Duplication | if ( !$localId && wfGetLB()->getReaderIndex() != 0 ) { |
|
| 404 | $localId = User::idFromName( $user->getName(), User::READ_LATEST ); |
||
| 405 | $flags = User::READ_LATEST; |
||
| 406 | } |
||
| 407 | // @codeCoverageIgnoreEnd |
||
| 408 | |||
| 409 | if ( $localId ) { |
||
| 410 | // User exists after all. |
||
| 411 | $user->setId( $localId ); |
||
| 412 | $user->loadFromId( $flags ); |
||
| 413 | return false; |
||
| 414 | } |
||
| 415 | |||
| 416 | // Denied by AuthPlugin? But ignore AuthPlugin itself. |
||
| 417 | if ( get_class( $wgAuth ) !== 'AuthPlugin' && !$wgAuth->autoCreate() ) { |
||
| 418 | $logger->debug( __METHOD__ . ': denied by AuthPlugin' ); |
||
| 419 | $user->setId( 0 ); |
||
| 420 | $user->loadFromId(); |
||
| 421 | return false; |
||
| 422 | } |
||
| 423 | |||
| 424 | // Wiki is read-only? |
||
| 425 | View Code Duplication | if ( wfReadOnly() ) { |
|
| 426 | $logger->debug( __METHOD__ . ': denied by wfReadOnly()' ); |
||
| 427 | $user->setId( 0 ); |
||
| 428 | $user->loadFromId(); |
||
| 429 | return false; |
||
| 430 | } |
||
| 431 | |||
| 432 | $userName = $user->getName(); |
||
| 433 | |||
| 434 | // Check the session, if we tried to create this user already there's |
||
| 435 | // no point in retrying. |
||
| 436 | $session = self::getGlobalSession(); |
||
| 437 | $reason = $session->get( 'MWSession::AutoCreateBlacklist' ); |
||
| 438 | View Code Duplication | if ( $reason ) { |
|
| 439 | $logger->debug( __METHOD__ . ": blacklisted in session ($reason)" ); |
||
| 440 | $user->setId( 0 ); |
||
| 441 | $user->loadFromId(); |
||
| 442 | return false; |
||
| 443 | } |
||
| 444 | |||
| 445 | // Is the IP user able to create accounts? |
||
| 446 | $anon = new User; |
||
| 447 | if ( !$anon->isAllowedAny( 'createaccount', 'autocreateaccount' ) |
||
| 448 | || $anon->isBlockedFromCreateAccount() |
||
| 449 | ) { |
||
| 450 | // Blacklist the user to avoid repeated DB queries subsequently |
||
| 451 | $logger->debug( __METHOD__ . ': user is blocked from this wiki, blacklisting' ); |
||
| 452 | $session->set( 'MWSession::AutoCreateBlacklist', 'blocked', 600 ); |
||
| 453 | $session->persist(); |
||
| 454 | $user->setId( 0 ); |
||
| 455 | $user->loadFromId(); |
||
| 456 | return false; |
||
| 457 | } |
||
| 458 | |||
| 459 | // Check for validity of username |
||
| 460 | if ( !User::isCreatableName( $userName ) ) { |
||
| 461 | $logger->debug( __METHOD__ . ': Invalid username, blacklisting' ); |
||
| 462 | $session->set( 'MWSession::AutoCreateBlacklist', 'invalid username', 600 ); |
||
| 463 | $session->persist(); |
||
| 464 | $user->setId( 0 ); |
||
| 465 | $user->loadFromId(); |
||
| 466 | return false; |
||
| 467 | } |
||
| 468 | |||
| 469 | // Give other extensions a chance to stop auto creation. |
||
| 470 | $user->loadDefaults( $userName ); |
||
| 471 | $abortMessage = ''; |
||
| 472 | if ( !\Hooks::run( 'AbortAutoAccount', [ $user, &$abortMessage ] ) ) { |
||
| 473 | // In this case we have no way to return the message to the user, |
||
| 474 | // but we can log it. |
||
| 475 | $logger->debug( __METHOD__ . ": denied by hook: $abortMessage" ); |
||
| 476 | $session->set( 'MWSession::AutoCreateBlacklist', "hook aborted: $abortMessage", 600 ); |
||
| 477 | $session->persist(); |
||
| 478 | $user->setId( 0 ); |
||
| 479 | $user->loadFromId(); |
||
| 480 | return false; |
||
| 481 | } |
||
| 482 | |||
| 483 | // Make sure the name has not been changed |
||
| 484 | if ( $user->getName() !== $userName ) { |
||
| 485 | $user->setId( 0 ); |
||
| 486 | $user->loadFromId(); |
||
| 487 | throw new \UnexpectedValueException( |
||
| 488 | 'AbortAutoAccount hook tried to change the user name' |
||
| 489 | ); |
||
| 490 | } |
||
| 491 | |||
| 492 | // Ignore warnings about master connections/writes...hard to avoid here |
||
| 493 | \Profiler::instance()->getTransactionProfiler()->resetExpectations(); |
||
| 494 | |||
| 495 | $cache = \ObjectCache::getLocalClusterInstance(); |
||
| 496 | $backoffKey = wfMemcKey( 'MWSession', 'autocreate-failed', md5( $userName ) ); |
||
| 497 | if ( $cache->get( $backoffKey ) ) { |
||
| 498 | $logger->debug( __METHOD__ . ': denied by prior creation attempt failures' ); |
||
| 499 | $user->setId( 0 ); |
||
| 500 | $user->loadFromId(); |
||
| 501 | return false; |
||
| 502 | } |
||
| 503 | |||
| 504 | // Checks passed, create the user... |
||
| 505 | $from = isset( $_SERVER['REQUEST_URI'] ) ? $_SERVER['REQUEST_URI'] : 'CLI'; |
||
| 506 | $logger->info( __METHOD__ . ': creating new user ({username}) - from: {url}', |
||
| 507 | [ |
||
| 508 | 'username' => $userName, |
||
| 509 | 'url' => $from, |
||
| 510 | ] ); |
||
| 511 | |||
| 512 | try { |
||
| 513 | // Insert the user into the local DB master |
||
| 514 | $status = $user->addToDatabase(); |
||
| 515 | if ( !$status->isOK() ) { |
||
| 516 | // @codeCoverageIgnoreStart |
||
| 517 | // double-check for a race condition (T70012) |
||
| 518 | $id = User::idFromName( $user->getName(), User::READ_LATEST ); |
||
| 519 | if ( $id ) { |
||
| 520 | $logger->info( __METHOD__ . ': tried to autocreate existing user', |
||
| 521 | [ |
||
| 522 | 'username' => $userName, |
||
| 523 | ] ); |
||
| 524 | } else { |
||
| 525 | $logger->error( |
||
| 526 | __METHOD__ . ': failed with message ' . $status->getWikiText( false, false, 'en' ), |
||
| 527 | [ |
||
| 528 | 'username' => $userName, |
||
| 529 | ] |
||
| 530 | ); |
||
| 531 | } |
||
| 532 | $user->setId( $id ); |
||
| 533 | $user->loadFromId( User::READ_LATEST ); |
||
| 534 | return false; |
||
| 535 | // @codeCoverageIgnoreEnd |
||
| 536 | } |
||
| 537 | } catch ( \Exception $ex ) { |
||
| 538 | // @codeCoverageIgnoreStart |
||
| 539 | $logger->error( __METHOD__ . ': failed with exception {exception}', [ |
||
| 540 | 'exception' => $ex, |
||
| 541 | 'username' => $userName, |
||
| 542 | ] ); |
||
| 543 | // Do not keep throwing errors for a while |
||
| 544 | $cache->set( $backoffKey, 1, 600 ); |
||
| 545 | // Bubble up error; which should normally trigger DB rollbacks |
||
| 546 | throw $ex; |
||
| 547 | // @codeCoverageIgnoreEnd |
||
| 548 | } |
||
| 549 | |||
| 550 | # Notify AuthPlugin |
||
| 551 | // @codeCoverageIgnoreStart |
||
| 552 | $tmpUser = $user; |
||
| 553 | $wgAuth->initUser( $tmpUser, true ); |
||
| 554 | if ( $tmpUser !== $user ) { |
||
| 555 | $logger->warning( __METHOD__ . ': ' . |
||
| 556 | get_class( $wgAuth ) . '::initUser() replaced the user object' ); |
||
| 557 | } |
||
| 558 | // @codeCoverageIgnoreEnd |
||
| 559 | |||
| 560 | # Notify hooks (e.g. Newuserlog) |
||
| 561 | \Hooks::run( 'AuthPluginAutoCreate', [ $user ] ); |
||
| 562 | \Hooks::run( 'LocalUserCreated', [ $user, true ] ); |
||
| 563 | |||
| 564 | $user->saveSettings(); |
||
| 565 | |||
| 566 | # Update user count |
||
| 567 | \DeferredUpdates::addUpdate( new \SiteStatsUpdate( 0, 0, 0, 0, 1 ) ); |
||
| 568 | |||
| 569 | # Watch user's userpage and talk page |
||
| 570 | $user->addWatch( $user->getUserPage(), User::IGNORE_USER_RIGHTS ); |
||
| 571 | |||
| 572 | return true; |
||
| 573 | } |
||
| 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 ) { |
||
| 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.