diff --git a/core/core.services.yml b/core/core.services.yml index e15618f..6276c6c 100644 --- a/core/core.services.yml +++ b/core/core.services.yml @@ -764,7 +764,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 046216e..0b5aca4 100644 --- a/core/lib/Drupal/Core/Cron.php +++ b/core/lib/Drupal/Core/Cron.php @@ -94,14 +94,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); @@ -128,10 +123,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..4fe3813 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,54 @@ 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(); + } + // 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 +255,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 a415ea3..b507150 100644 --- a/core/lib/Drupal/Core/Session/AccountProxyInterface.php +++ b/core/lib/Drupal/Core/Session/AccountProxyInterface.php @@ -29,10 +29,39 @@ public function setAccount(AccountInterface $account); * Setting the current account is highly discouraged! Instead, make sure to * inject the desired user object into the dependent code directly * - * @param \Drupal\Core\Session\AccountInterface + * @return \Drupal\Core\Session\AccountInterface * The current 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/lib/Drupal/simpletest/TestBase.php b/core/modules/simpletest/lib/Drupal/simpletest/TestBase.php index 19dd044..e761fbb 100644 --- a/core/modules/simpletest/lib/Drupal/simpletest/TestBase.php +++ b/core/modules/simpletest/lib/Drupal/simpletest/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,10 @@ 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 + // 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->set('current_user', \Drupal::currentUser()); + $this->container->get('current_user')->impersonateAccount(new AnonymousUserSession()); \Drupal::setContainer($this->container); @@ -1229,8 +1223,7 @@ private function restoreEnvironment() { $callbacks = $this->originalShutdownCallbacks; // Restore original user session. - $this->container->set('current_user', $this->originalUser); - \Drupal::service('session_manager')->enable(); + \Drupal::currentUser()->revertAll(); } /** 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..1aa319e --- /dev/null +++ b/core/modules/system/lib/Drupal/system/Tests/Session/UserImpersonatingUserTest.php @@ -0,0 +1,75 @@ + 'Impersonate users', + 'description' => 'Temporarily impersonate another user, and then restore the original user.', + 'group' => 'User', + ); + } + + function setUp() { + parent::setUp(); + } + + function testUserImpersonateUser() { + $user = \Drupal::currentUser(); + $original_user = clone $user; + + // If not currently logged in, use user_user_impersonate_user() 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), t('User switched')); + $this->assertFalse(\Drupal::service('session_manager')->isEnabled(), t('Session saving is disabled.')); + + // Perform a second (nested) impersonation. + $user->impersonateAccount(new UserSession(array('uid' => 2))); + $this->assertEqual($user->id(), 2, t('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), t('User switched.')); + $this->assertFalse(\Drupal::service('session_manager')->isEnabled(), t('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(), t('Original user successfully restored.')); + + // Simpletest uses user_impersonate_user() too, revert the impersonation by + // Simpletest to enable session saving again. This is safe because calling + // user_revert_user() too often simply results in returning the active user. + $user->revertAccount(); + $this->assertTrue(\Drupal::service('session_manager')->isEnabled(), t('Session saving is enabled.')); + } +}