diff --git a/core/core.services.yml b/core/core.services.yml index be51f03..1996e6d 100644 --- a/core/core.services.yml +++ b/core/core.services.yml @@ -773,7 +773,7 @@ services: arguments: ['@authentication'] current_user: class: Drupal\Core\Session\AccountProxy - arguments: ['@authentication', '@request'] + arguments: ['@authentication', '@request', '@session_manager'] session_manager: class: Drupal\Core\Session\SessionManager arguments: ['@request_stack', '@database', '@session_manager.metadata_bag', '@settings'] diff --git a/core/lib/Drupal/Core/Cron.php b/core/lib/Drupal/Core/Cron.php index b639f5c..2c73810 100644 --- a/core/lib/Drupal/Core/Cron.php +++ b/core/lib/Drupal/Core/Cron.php @@ -95,14 +95,9 @@ public function run() { // Allow execution to continue even if the request gets cancelled. @ignore_user_abort(TRUE); - // Prevent session information from being saved while cron is running. - $original_session_saving = $this->sessionManager->isEnabled(); - $this->sessionManager->disable(); - // Force the current user to anonymous to ensure consistent permissions on // cron runs. - $original_user = $this->currentUser->getAccount(); - $this->currentUser->setAccount(new AnonymousUserSession()); + $this->currentUser->impersonateAccount(new AnonymousUserSession()); // Try to allocate enough time to run all the hook_cron implementations. drupal_set_time_limit(240); @@ -129,10 +124,7 @@ public function run() { $this->processQueues(); // Restore the user. - $this->currentUser->setAccount($original_user); - if ($original_session_saving) { - $this->sessionManager->enable(); - } + $this->currentUser->revertAccount(); return $return; } diff --git a/core/lib/Drupal/Core/Installer/InstallerServiceProvider.php b/core/lib/Drupal/Core/Installer/InstallerServiceProvider.php index 6c393b1..27d7afa 100644 --- a/core/lib/Drupal/Core/Installer/InstallerServiceProvider.php +++ b/core/lib/Drupal/Core/Installer/InstallerServiceProvider.php @@ -46,6 +46,8 @@ public function register(ContainerBuilder $container) { ->register('url_generator', 'Drupal\Core\Routing\NullGenerator'); $container ->register('router.dumper', 'Drupal\Core\Routing\NullMatcherDumper'); + $container + ->register('current_user', 'Drupal\Core\Session\NullAccountProxy'); // Replace the route builder with an empty implementation. // @todo Convert installer steps into routes; add an installer.routing.yml. diff --git a/core/lib/Drupal/Core/Session/AccountProxy.php b/core/lib/Drupal/Core/Session/AccountProxy.php index 9f00218..dbe165e 100644 --- a/core/lib/Drupal/Core/Session/AccountProxy.php +++ b/core/lib/Drupal/Core/Session/AccountProxy.php @@ -45,16 +45,40 @@ class AccountProxy implements AccountProxyInterface { protected $account; /** + * A stack of previous overridden accounts. + * + * @var \Drupal\Core\Session\AccountInterface[] + */ + protected $accountStack = array(); + + /** + * The session manager. + * + * @var \Drupal\Core\Session\SessionManager + */ + protected $sessionManager; + + /** + * The original state of session saving prior to user impersonations. + * + * @var bool + */ + protected $originalSessionSaving; + + /** * Constructs a new AccountProxy. * * @param \Drupal\Core\Authentication\AuthenticationManagerInterface $authentication_manager * The authentication manager. * @param \Symfony\Component\HttpFoundation\Request $request * The request object used for authenticating. + * @param \Drupal\Core\Session\SessionManager $session_manager + * The session manager. */ - public function __construct(AuthenticationManagerInterface $authentication_manager, Request $request) { + public function __construct(AuthenticationManagerInterface $authentication_manager, Request $request, SessionManager $session_manager) { $this->authenticationManager = $authentication_manager; $this->request = $request; + $this->sessionManager = $session_manager; } /** @@ -82,6 +106,57 @@ public function getAccount() { /** * {@inheritdoc} */ + public function impersonateAccount(AccountInterface $account) { + // Prevent session information from being saved and push the previous account. + array_push($this->accountStack, $this->account); + $this->originalSessionSaving = $this->sessionManager->isEnabled(); + $this->sessionManager->disable(); + $this->setAccount($account); + return $this; + } + + /** + * {@inheritdoc} + */ + public function revertAccount() { + // Restore the previous account from the stack. + if (!empty($this->accountStack)) { + $this->account = array_pop($this->accountStack); + } + else { + throw new \RuntimeException(t('No more account impersonations to revert.')); + } + // Restore original session saving status if all impersonations are reverted. + if (empty($this->accountStack)) { + if ($this->originalSessionSaving) { + $this->sessionManager->enable(); + } + } + return $this; + } + + /** + * {@inheritdoc} + */ + public function revertAll() { + // Restore the original account from the stack. + if (!empty($this->accountStack)) { + $this->account = array_shift($this->accountStack); + $this->accountStack = array(); + } + else { + throw new \RuntimeException(t('No more account impersonations to revert.')); + } + // Restore original session saving status if all impersonations are reverted. + if ($this->originalSessionSaving) { + $this->sessionManager->enable(); + } + return $this; + } + + /** + * {@inheritdoc} + */ public function id() { return $this->getAccount()->id(); } @@ -183,6 +258,4 @@ public function getTimeZone() { public function getLastAccessedTime() { return $this->getAccount()->getLastAccessedTime(); } - } - diff --git a/core/lib/Drupal/Core/Session/AccountProxyInterface.php b/core/lib/Drupal/Core/Session/AccountProxyInterface.php index 649e07e..c4cf390 100644 --- a/core/lib/Drupal/Core/Session/AccountProxyInterface.php +++ b/core/lib/Drupal/Core/Session/AccountProxyInterface.php @@ -31,5 +31,34 @@ public function setAccount(AccountInterface $account); */ public function getAccount(); + /** + * Set the current wrapped account to impersonate another account. + * + * Always remember to call AccountProxyInterface::revertAccount() after this + * call! + * + * @param \Drupal\Core\Session\AccountInterface + * $this. + */ + public function impersonateAccount(AccountInterface $account); + + /** + * Revert from impersonating another account. + * + * @return \Drupal\Core\Session\AccountInterface + * $this. + */ + public function revertAccount(); + + /** + * Reverts all account impersonations to original account. + * + * This is useful for when there is a failure or an exception and there has + * been possibility of multiple impersonations. + * + * @return \Drupal\Core\Session\AccountInterface + * $this. + */ + public function revertAll(); } diff --git a/core/lib/Drupal/Core/Session/NullAccountProxy.php b/core/lib/Drupal/Core/Session/NullAccountProxy.php new file mode 100644 index 0000000..499fecb --- /dev/null +++ b/core/lib/Drupal/Core/Session/NullAccountProxy.php @@ -0,0 +1,45 @@ +account = new AnonymousUserSession(); + } + + /** + * {@inheritdoc} + */ + public function impersonateAccount(AccountInterface $account) { + // Push the previous account. + $this->accountStack[] = $this->account; + $this->setAccount($account); + return $this->account; + } + + /** + * {@inheritdoc} + */ + public function revertAccount() { + // Restore the previous account from the stack. + if (!empty($this->accountStack)) { + $this->account = array_pop($this->accountStack); + } + return $this->account; + } +} diff --git a/core/modules/simpletest/src/TestBase.php b/core/modules/simpletest/src/TestBase.php index 023cf8e..50f28a2 100644 --- a/core/modules/simpletest/src/TestBase.php +++ b/core/modules/simpletest/src/TestBase.php @@ -15,9 +15,7 @@ use Drupal\Core\DependencyInjection\ContainerBuilder; use Drupal\Core\Database\ConnectionNotDefinedException; use Drupal\Core\Config\StorageInterface; -use Drupal\Core\DrupalKernel; use Drupal\Core\Language\Language; -use Drupal\Core\Session\AccountProxy; use Drupal\Core\Session\AnonymousUserSession; use Drupal\Core\Site\Settings; use Drupal\Core\StreamWrapper\PublicStream; @@ -994,7 +992,6 @@ protected function beforePrepareEnvironment() { * @see TestBase::beforePrepareEnvironment() */ private function prepareEnvironment() { - $user = \Drupal::currentUser(); // Allow (base) test classes to backup global state information. $this->beforePrepareEnvironment(); @@ -1027,10 +1024,6 @@ private function prepareEnvironment() { // simpletest directory if a test is executed within a test. $this->originalFileDirectory = Settings::get('file_public_path', conf_path() . '/files'); $this->originalProfile = drupal_get_profile(); - $this->originalUser = isset($user) ? clone $user : NULL; - - // Ensure that the current session is not changed by the new environment. - \Drupal::service('session_manager')->disable(); // Save and clean the shutdown callbacks array because it is static cached // and will be changed by the test run. Otherwise it will contain callbacks @@ -1089,9 +1082,19 @@ private function prepareEnvironment() { $request = Request::create('/'); $this->container->set('request', $request); - // Run all tests as a anonymous user by default, web tests will replace that + // Copy the current session and authentication managers into the new environment. + $this->container->set('session_manager', \Drupal::service('session_manager')); + $this->container->set('authentication', \Drupal::service('authentication')); + + // Create new current user service for the test environment. + $this->container->register('current_user', '\Drupal\Core\Session\AccountProxy') + ->addArgument(new Reference('authentication')) + ->addArgument(new Reference('request')) + ->addArgument(new Reference('session_manager')); + + // Run all tests as an anonymous user by default, web tests will replace that // during the test set up. - $this->container->set('current_user', new AnonymousUserSession()); + $this->container->get('current_user')->impersonateAccount(new AnonymousUserSession()); \Drupal::setContainer($this->container); @@ -1227,10 +1230,6 @@ private function restoreEnvironment() { // Restore original shutdown callbacks. $callbacks = &drupal_register_shutdown_function(); $callbacks = $this->originalShutdownCallbacks; - - // Restore original user session. - $this->container->set('current_user', $this->originalUser); - \Drupal::service('session_manager')->enable(); } /** diff --git a/core/modules/system/lib/Drupal/system/Tests/Session/UserImpersonatingUserTest.php b/core/modules/system/lib/Drupal/system/Tests/Session/UserImpersonatingUserTest.php new file mode 100644 index 0000000..2e08b23 --- /dev/null +++ b/core/modules/system/lib/Drupal/system/Tests/Session/UserImpersonatingUserTest.php @@ -0,0 +1,95 @@ + 'Impersonate users', + 'description' => 'Temporarily impersonate another user account, and then restore the original account.', + 'group' => 'Session', + ); + } + + function setUp() { + parent::setUp(); + } + + function testUserImpersonateUser() { + $session_manager = $this->container->get('session_manager'); + $user = $this->container->get('current_user'); + $original_user = clone $user; + $this->verbose('Session saving status: '. var_dump($session_manager->isEnabled())); + $this->assertFalse($session_manager->isEnabled(), 'Session saving is initially disabled.'); + + // If not currently logged in, use AccountProxy::impersonateAccount() to switch to + // user 1. If logged in, switch to the anonymous user instead. + if ($user->isAnonymous()) { + $user->impersonateAccount(new UserSession(array('uid' => 1))); + } else { + $user->impersonateAccount(new AnonymousUserSession()); + } + + // Verify that the active user has changed, and that session saving is + // disabled. + $this->assertEqual($user->id(), ($original_user->id() == 0 ? 1 : 0), 'User switched'); + $this->assertFalse($session_manager->isEnabled(), 'Session saving is disabled.'); + + // Enable session saving for the purpose of this test. + $session_manager->enable(); + + // Perform a second (nested) impersonation. + $user->impersonateAccount(new UserSession(array('uid' => 2))); + $this->assertEqual($user->id(), 2, 'User switched.'); + + // Revert to the user which was active between the first and second + // impersonation attempt. + $user->revertAccount(); + + // Since we are still impersonating the user from the first attempt, + // session handling still needs to be disabled. + $this->assertEqual($user->id(), ($original_user->id() == 0 ? 1 : 0), 'User switched.'); + $this->assertFalse($session_manager->isEnabled(), 'Session saving is disabled.'); + + // Revert to the original user which was active before the first + // impersonation attempt. + $user->revertAccount(); + + // Assert that the original user is the active user again, and that session + // saving has been re-enabled. + $this->assertEqual($user->id(), $original_user->id(), 'Original user successfully restored.'); + $this->assertTrue($session_manager->isEnabled(), 'Session saving is enabled.'); + + // Verify that AccountProxy::revertAccount and AccountProxy::revertAll() + // will throw exceptions if there is none left in the stack. + try { + $ex1 = new \RuntimeException(); + $user->revertAccount(); + } + catch (\RuntimeException $e) { + $ex1 = $e; + } + $this->assertEqual($ex1->getMessage(), 'No more account impersonations to revert.', 'Revert account throws exception if called without previous impersonation.'); + try { + $ex2 = new \RuntimeException(); + $user->revertAll(); + } + catch (\RuntimeException $e) { + $ex2 = $e; + } + $this->assertEqual($ex2->getMessage(), 'No more account impersonations to revert.', 'Revert all throws exception if called without previous impersonation.'); + } +}