diff --git a/core/core.services.yml b/core/core.services.yml index 9c3f606..d0d73c6 100644 --- a/core/core.services.yml +++ b/core/core.services.yml @@ -162,7 +162,7 @@ services: arguments: ['@typed_data_manager'] cron: class: Drupal\Core\Cron - arguments: ['@module_handler', '@lock', '@queue', '@state', '@current_user', '@session_manager', '@logger.channel.cron', '@plugin.manager.queue_worker'] + arguments: ['@module_handler', '@lock', '@queue', '@state', '@account_switcher', '@logger.channel.cron', '@plugin.manager.queue_worker'] diff.formatter: class: Drupal\Core\Diff\DiffFormatter arguments: ['@config.factory'] @@ -996,6 +996,9 @@ services: tags: - { name: event_subscriber } arguments: ['@authentication'] + account_switcher: + class: Drupal\Core\Session\AccountSwitcher + arguments: ['@current_user', '@session_manager'] current_user: class: Drupal\Core\Session\AccountProxy arguments: ['@authentication', '@request_stack'] diff --git a/core/lib/Drupal/Core/Cron.php b/core/lib/Drupal/Core/Cron.php index bc256f8..1c17c94 100644 --- a/core/lib/Drupal/Core/Cron.php +++ b/core/lib/Drupal/Core/Cron.php @@ -12,9 +12,8 @@ use Drupal\Core\State\StateInterface; use Drupal\Core\Lock\LockBackendInterface; use Drupal\Core\Queue\QueueFactory; -use Drupal\Core\Session\AccountProxyInterface; use Drupal\Core\Session\AnonymousUserSession; -use Drupal\Core\Session\SessionManagerInterface; +use Drupal\Core\Session\AccountSwitcherInterface; use Drupal\Core\Queue\SuspendQueueException; use Psr\Log\LoggerInterface; @@ -52,18 +51,11 @@ class Cron implements CronInterface { protected $state; /** - * The current user. + * The account switcher service. * - * @var \Drupal\Core\Session\AccountProxyInterface + * @var \Drupal\Core\Session\AccountSwitcherInterface */ - protected $currentUser; - - /** - * The session manager. - * - * @var \Drupal\Core\Session\SessionManagerInterface - */ - protected $sessionManager; + protected $accountSwitcher; /** * A logger instance. @@ -90,22 +82,19 @@ class Cron implements CronInterface { * The queue service. * @param \Drupal\Core\State\StateInterface $state * The state service. - * @param \Drupal\Core\Session\AccountProxyInterface $current_user - * The current user. - * @param \Drupal\Core\Session\SessionManagerInterface $session_manager - * The session manager. + * @param \Drupal\Core\Session\AccountSwitcherInterface $account_switcher + * The account switching service. * @param \Psr\Log\LoggerInterface $logger * A logger instance. * @param \Drupal\Core\Queue\QueueWorkerManagerInterface * The queue plugin manager. */ - public function __construct(ModuleHandlerInterface $module_handler, LockBackendInterface $lock, QueueFactory $queue_factory, StateInterface $state, AccountProxyInterface $current_user, SessionManagerInterface $session_manager, LoggerInterface $logger, QueueWorkerManagerInterface $queue_manager) { + public function __construct(ModuleHandlerInterface $module_handler, LockBackendInterface $lock, QueueFactory $queue_factory, StateInterface $state, AccountSwitcherInterface $account_switcher, LoggerInterface $logger, QueueWorkerManagerInterface $queue_manager) { $this->moduleHandler = $module_handler; $this->lock = $lock; $this->queueFactory = $queue_factory; $this->state = $state; - $this->currentUser = $current_user; - $this->sessionManager = $session_manager; + $this->accountSwitcher = $account_switcher; $this->logger = $logger; $this->queueManager = $queue_manager; } @@ -117,14 +106,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->accountSwitcher->switchTo(new AnonymousUserSession()); // Try to allocate enough time to run all the hook_cron implementations. drupal_set_time_limit(240); @@ -151,10 +135,7 @@ public function run() { $this->processQueues(); // Restore the user. - $this->currentUser->setAccount($original_user); - if ($original_session_saving) { - $this->sessionManager->enable(); - } + $this->accountSwitcher->switchBack(); return $return; } diff --git a/core/lib/Drupal/Core/Session/AccountSwitcher.php b/core/lib/Drupal/Core/Session/AccountSwitcher.php new file mode 100644 index 0000000..0737258 --- /dev/null +++ b/core/lib/Drupal/Core/Session/AccountSwitcher.php @@ -0,0 +1,96 @@ +currentUser = $current_user; + $this->sessionManager = $session_manager; + } + + /** + * {@inheritdoc} + */ + public function switchTo(AccountInterface $account) { + // Prevent session information from being saved and push previous account. + if (!isset($this->originalSessionSaving)) { + // Ensure that only the first session saving status is saved. + $this->originalSessionSaving = $this->sessionManager->isEnabled(); + } + $this->sessionManager->disable(); + array_push($this->accountStack, $this->currentUser->getAccount()); + $this->currentUser->setAccount($account); + return $this; + } + + /** + * {@inheritdoc} + */ + public function switchBack() { + // Restore the previous account from the stack. + if (!empty($this->accountStack)) { + $this->currentUser->setAccount(array_pop($this->accountStack)); + } + else { + throw new \RuntimeException('No more accounts to revert to.'); + } + // Restore original session saving status if all account switches are + // reverted. + if (empty($this->accountStack)) { + if ($this->originalSessionSaving) { + $this->sessionManager->enable(); + } + } + return $this; + } + +} diff --git a/core/lib/Drupal/Core/Session/AccountSwitcherInterface.php b/core/lib/Drupal/Core/Session/AccountSwitcherInterface.php new file mode 100644 index 0000000..15ee497 --- /dev/null +++ b/core/lib/Drupal/Core/Session/AccountSwitcherInterface.php @@ -0,0 +1,43 @@ +container->get('session_manager'); + $user = $this->container->get('current_user'); + $switcher = $this->container->get('account_switcher'); + $original_user = $user->getAccount(); + $original_session_saving = $session_manager->isEnabled(); + + // Switch to user with uid 2. + $switcher->switchTo(new UserSession(array('uid' => 2))); + + // Verify that the active user has changed, and that session saving is + // disabled. + $this->assertEqual($user->id(), 2, 'Switched to user 2.'); + $this->assertFalse($session_manager->isEnabled(), 'Session saving is disabled.'); + + // Perform a second (nested) user account switch. + $switcher->switchTo(new UserSession(array('uid' => 3))); + $this->assertEqual($user->id(), 3, 'Switched to user 3.'); + + // Revert to the user session that was active between the first and second + // switch. + $switcher->switchBack(); + + // Since we are still in the account from the first switch, session handling + // still needs to be disabled. + $this->assertEqual($user->id(), 2, 'Reverted back to user 2.'); + $this->assertFalse($session_manager->isEnabled(), 'Session saving still disabled.'); + + // Revert to the original account which was active before the first switch. + $switcher->switchBack(); + + // Assert that the original account is active again, and that session saving + // has been re-enabled. + $this->assertEqual($user->id(), $original_user->id(), 'Original user correctly restored.'); + $this->assertEqual($session_manager->isEnabled(), $original_session_saving, 'Original session saving correctly restored.'); + + // Verify that AccountSwitcherInterface::switchBack() will throw + // an exception if there are no accounts left in the stack. + try { + $switcher->switchBack(); + $this->fail('::switchBack() throws exception if called without previous switch.'); + } + catch (\RuntimeException $e) { + if ($e->getMessage() == 'No more accounts to revert to.') { + $this->pass('::switchBack() throws exception if called without previous switch.'); + } + else { + $this->fail($e->getMessage()); + } + } + } + +}