diff --git a/core/lib/Drupal/Core/Test/FunctionalTestSetupTrait.php b/core/lib/Drupal/Core/Test/FunctionalTestSetupTrait.php index 57e68e2ad3..524d430a44 100644 --- a/core/lib/Drupal/Core/Test/FunctionalTestSetupTrait.php +++ b/core/lib/Drupal/Core/Test/FunctionalTestSetupTrait.php @@ -17,6 +17,7 @@ use Drupal\Core\Session\UserSession; use Drupal\Core\Site\Settings; use Drupal\Core\StreamWrapper\StreamWrapperInterface; +use Drupal\Tests\RequestSessionTestTrait; use Drupal\Tests\SessionTestTrait; use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\HttpFoundation\Request; @@ -31,6 +32,7 @@ trait FunctionalTestSetupTrait { use SessionTestTrait; use RefreshVariablesTrait; + use RequestSessionTestTrait; /** * The "#1" admin user. @@ -271,6 +273,7 @@ protected function prepareRequestForGenerator($clean_urls = TRUE, $override_serv $server = array_merge($server, $override_server_vars); $request = Request::create($request_path, 'GET', [], [], [], $server); + $request->setSession($this->getSessionFromRequest()); // Ensure the request time is REQUEST_TIME to ensure that API calls // in the test use the right timestamp. $request->server->set('REQUEST_TIME', REQUEST_TIME); @@ -692,6 +695,25 @@ protected function prepareEnvironment() { $callbacks = []; } + /** + * Prepare the session and add it to the request. + */ + protected function prepareSession(ContainerInterface $container) { + // Disable session writing. + + /** @var \Drupal\Core\Session\WriteSafeSessionHandlerInterface $writeSafeHandler */ + $writeSafeHandler = $container->get('session_handler.write_safe'); + $writeSafeHandler->setSessionWritable(FALSE); + + /** @var \Symfony\Component\HttpFoundation\Session\SessionInterface $session */ + $session = $container->get('session'); + $session->start(); + + /** @var \Symfony\Component\HttpFoundation\Request $request */ + $request = $container->get('request_stack')->getCurrentRequest(); + $request->setSession($session); + } + /** * Returns all supported database driver installer objects. * diff --git a/core/modules/jsonapi/src/Controller/FileUpload.php b/core/modules/jsonapi/src/Controller/FileUpload.php index c11244a799..fa7d423a4d 100644 --- a/core/modules/jsonapi/src/Controller/FileUpload.php +++ b/core/modules/jsonapi/src/Controller/FileUpload.php @@ -141,7 +141,9 @@ public function handleFileUploadForExistingResource(Request $request, ResourceTy $route_parameters = ['entity' => $entity->uuid()]; $route_name = sprintf('jsonapi.%s.%s.related', $resource_type->getTypeName(), $resource_type->getPublicName($file_field_name)); $related_url = Url::fromRoute($route_name, $route_parameters)->toString(TRUE); + $session = $request->getSession(); $request = Request::create($related_url->getGeneratedUrl(), 'GET', [], $request->cookies->all(), [], $request->server->all()); + $request->setSession($session); return $this->httpKernel->handle($request, HttpKernelInterface::SUB_REQUEST); } diff --git a/core/modules/language/src/Plugin/LanguageNegotiation/LanguageNegotiationSession.php b/core/modules/language/src/Plugin/LanguageNegotiation/LanguageNegotiationSession.php index f36d43e946..746515d181 100644 --- a/core/modules/language/src/Plugin/LanguageNegotiation/LanguageNegotiationSession.php +++ b/core/modules/language/src/Plugin/LanguageNegotiation/LanguageNegotiationSession.php @@ -4,11 +4,14 @@ use Drupal\Core\Language\LanguageInterface; use Drupal\Core\PathProcessor\OutboundPathProcessorInterface; +use Drupal\Core\Plugin\ContainerFactoryPluginInterface; use Drupal\Core\Render\BubbleableMetadata; use Drupal\Core\Url; use Drupal\language\LanguageNegotiationMethodBase; use Drupal\language\LanguageSwitcherInterface; +use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\RequestStack; /** * Identify language from a request/session parameter. @@ -21,7 +24,7 @@ * config_route_name = "language.negotiation_session" * ) */ -class LanguageNegotiationSession extends LanguageNegotiationMethodBase implements OutboundPathProcessorInterface, LanguageSwitcherInterface { +class LanguageNegotiationSession extends LanguageNegotiationMethodBase implements OutboundPathProcessorInterface, LanguageSwitcherInterface, ContainerFactoryPluginInterface { /** * Flag used to determine whether query rewriting is active. @@ -44,11 +47,37 @@ class LanguageNegotiationSession extends LanguageNegotiationMethodBase implement */ protected $queryValue; + /** + * The request stack. + * + * @var \Symfony\Component\HttpFoundation\RequestStack + */ + protected $requestStack; + /** * The language negotiation method id. */ const METHOD_ID = 'language-session'; + /** + * Constructs a LanguageNegotiationSession object. + * + * @param \Symfony\Component\HttpFoundation\RequestStack $request_stack + * The request stack. + */ + public function __construct(RequestStack $request_stack) { + $this->requestStack = $request_stack; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { + return new static( + $container->get('request_stack') + ); + } + /** * {@inheritdoc} */ @@ -56,8 +85,8 @@ public function getLangcode(Request $request = NULL) { $config = $this->config->get('language.negotiation')->get('session'); $param = $config['parameter']; $langcode = $request && $request->query->get($param) ? $request->query->get($param) : NULL; - if (!$langcode && isset($_SESSION[$param])) { - $langcode = $_SESSION[$param]; + if (!$langcode && $request && $request->getSession()->has($param)) { + $langcode = $request->getSession()->get($param); } return $langcode; } @@ -73,7 +102,7 @@ public function persist(LanguageInterface $language) { $languages = $this->languageManager->getLanguages(); if ($this->currentUser->isAuthenticated() && isset($languages[$langcode])) { $config = $this->config->get('language.negotiation')->get('session'); - $_SESSION[$config['parameter']] = $langcode; + $this->requestStack->getCurrentRequest()->getSession()->set($config['parameter'], $langcode); } } } @@ -127,7 +156,7 @@ public function getLanguageSwitchLinks(Request $request, $type, Url $url) { $links = []; $config = $this->config->get('language.negotiation')->get('session'); $param = $config['parameter']; - $language_query = $_SESSION[$param] ?? $this->languageManager->getCurrentLanguage($type)->getId(); + $language_query = $request->getSession()->has($param) ? $request->getSession()->get($param) : $this->languageManager->getCurrentLanguage($type)->getId(); $query = $request->query->all(); foreach ($this->languageManager->getNativeLanguages() as $language) { diff --git a/core/modules/language/tests/src/Functional/LanguageUILanguageNegotiationTest.php b/core/modules/language/tests/src/Functional/LanguageUILanguageNegotiationTest.php index 900b0c9dc4..45ca2a4c62 100644 --- a/core/modules/language/tests/src/Functional/LanguageUILanguageNegotiationTest.php +++ b/core/modules/language/tests/src/Functional/LanguageUILanguageNegotiationTest.php @@ -555,6 +555,7 @@ public function testLanguageDomain() { // Test HTTPS via current URL scheme. $request = Request::create('', 'GET', [], [], [], ['HTTPS' => 'on']); + $request->setSession($this->getSessionFromRequest()); $this->container->get('request_stack')->push($request); $italian_url = Url::fromRoute('system.admin', [], ['language' => $languages['it']])->toString(); $correct_link = 'https://' . $link; diff --git a/core/modules/views_ui/src/ViewUI.php b/core/modules/views_ui/src/ViewUI.php index 531c27a7e7..7da59b9a7a 100644 --- a/core/modules/views_ui/src/ViewUI.php +++ b/core/modules/views_ui/src/ViewUI.php @@ -584,6 +584,7 @@ public function renderPreview($display_id, $args = []) { $raw_parameters->set('view', $this->id()); $raw_parameters->set('display_id', $display_id); $request->attributes->set('_raw_variables', $raw_parameters); + $request->setSession($current_request->getSession()); foreach ($args as $key => $arg) { $request->attributes->set('arg_' . $key, $arg); diff --git a/core/tests/Drupal/FunctionalTests/BrowserTestBaseTest.php b/core/tests/Drupal/FunctionalTests/BrowserTestBaseTest.php index 2b9656e70c..09431b7424 100644 --- a/core/tests/Drupal/FunctionalTests/BrowserTestBaseTest.php +++ b/core/tests/Drupal/FunctionalTests/BrowserTestBaseTest.php @@ -673,6 +673,25 @@ public function testEscapingAssertions() { $assert->assertNoEscaped(""); } + /** + * Tests that a usable session is on the request in test-runner. + */ + public function testSessionOnRequest() { + /** @var \Symfony\Component\HttpFoundation\Request $request */ + $request = $this->container->get('request_stack')->getCurrentRequest(); + + // Verify that a usable session is available from the request. + /** @var \Symfony\Component\HttpFoundation\Session\SessionInterface $session */ + $session = $request->getSession(); + $this->assertNotNull($session); + + $session->set('some-val', 'do-not-cleanup'); + $this->assertEquals('do-not-cleanup', $session->get('some-val')); + + $session->set('some-other-val', 'do-cleanup'); + $this->assertEquals('do-cleanup', $session->remove('some-other-val')); + } + /** * Tests that deprecation headers do not get duplicated. * diff --git a/core/tests/Drupal/KernelTests/Core/Session/MockSessionManager.php b/core/tests/Drupal/KernelTests/Core/Session/MockSessionManager.php new file mode 100644 index 0000000000..62f9731a1c --- /dev/null +++ b/core/tests/Drupal/KernelTests/Core/Session/MockSessionManager.php @@ -0,0 +1,139 @@ +storage = new MockArraySessionStorage('MOCK_SESSION_ID', $metadata_bag); + } + + /** + * {@inheritdoc} + */ + public function start(): bool { + return $this->storage->start(); + } + + /** + * {@inheritdoc} + */ + public function isStarted(): bool { + return $this->storage->isStarted(); + } + + /** + * {@inheritdoc} + */ + public function getId(): string { + return $this->storage->getId(); + } + + /** + * {@inheritdoc} + */ + public function setId(string $id) { + return $this->storage->setId($id); + } + + /** + * {@inheritdoc} + */ + public function getName(): string { + return $this->storage->getName(); + } + + /** + * {@inheritdoc} + */ + public function setName(string $name) { + $this->storage->setName($name); + } + + /** + * {@inheritdoc} + */ + public function regenerate(bool $destroy = FALSE, int $lifetime = NULL): bool { + return $this->storage->regenerate($destroy, $lifetime); + } + + /** + * {@inheritdoc} + */ + public function save() { + $this->storage->save(); + } + + /** + * {@inheritdoc} + */ + public function clear() { + $this->storage->clear(); + } + + /** + * {@inheritdoc} + */ + public function getBag(string $name): SessionBagInterface { + return $this->storage->getBag($name); + } + + /** + * {@inheritdoc} + */ + public function registerBag(SessionBagInterface $bag) { + $this->storage->registerBag($bag); + } + + /** + * {@inheritdoc} + */ + public function getMetadataBag(): MetadataBag { + return $this->storage->getMetadataBag(); + } + + /** + * {@inheritdoc} + */ + public function delete($uid) { + // Ignored. + } + + /** + * {@inheritdoc} + */ + public function destroy() { + @trigger_error(__METHOD__ . ' not implemented in MockSessionManager', E_USER_ERROR); + } + + /** + * {@inheritdoc} + */ + public function setWriteSafeHandler(WriteSafeSessionHandlerInterface $handler) { + @trigger_error(__METHOD__ . ' not implemented in MockSessionManager', E_USER_ERROR); + } + +} diff --git a/core/tests/Drupal/KernelTests/Core/Session/SessionManagerDestroyNoCliCheckTest.php b/core/tests/Drupal/KernelTests/Core/Session/SessionManagerDestroyNoCliCheckTest.php index 01fdd84ca9..fd4f203b5a 100644 --- a/core/tests/Drupal/KernelTests/Core/Session/SessionManagerDestroyNoCliCheckTest.php +++ b/core/tests/Drupal/KernelTests/Core/Session/SessionManagerDestroyNoCliCheckTest.php @@ -2,6 +2,7 @@ namespace Drupal\KernelTests\Core\Session; +use Drupal\Core\DependencyInjection\ContainerBuilder; use Drupal\KernelTests\KernelTestBase; /** @@ -11,12 +12,24 @@ */ class SessionManagerDestroyNoCliCheckTest extends KernelTestBase { + /** + * {@inheritdoc} + */ + public function register(ContainerBuilder $container) { + // This test needs to operate on the production session_manager. Thus move + // it to another id before parent::register() installs the mock session + // manager. + $session_manager_prod = $container->getDefinition('session_manager'); + $container->setDefinition('session_manager.production', $session_manager_prod); + parent::register($container); + } + /** * Tests starting and destroying a session from the CLI. */ public function testCallSessionManagerStartAndDestroy() { - $this->assertFalse(\Drupal::service('session_manager')->start()); - $this->assertNull(\Drupal::service('session_manager')->destroy()); + $this->assertFalse(\Drupal::service('session_manager.production')->start()); + $this->assertNull(\Drupal::service('session_manager.production')->destroy()); } } diff --git a/core/tests/Drupal/KernelTests/KernelTestBase.php b/core/tests/Drupal/KernelTests/KernelTestBase.php index 47fd9b5862..6bace3ba4c 100644 --- a/core/tests/Drupal/KernelTests/KernelTestBase.php +++ b/core/tests/Drupal/KernelTests/KernelTestBase.php @@ -18,6 +18,7 @@ use Drupal\Tests\ConfigTestTrait; use Drupal\Tests\ExtensionListTestTrait; use Drupal\Tests\RandomGeneratorTrait; +use Drupal\Tests\RequestSessionTestTrait; use Drupal\Tests\PhpUnitCompatibilityTrait; use Drupal\Tests\TestRequirementsTrait; use Drupal\Tests\Traits\PhpUnitWarnings; @@ -92,6 +93,7 @@ abstract class KernelTestBase extends TestCase implements ServiceProviderInterfa use PhpUnitCompatibilityTrait; use ProphecyTrait; use ExpectDeprecationTrait; + use RequestSessionTestTrait; /** * {@inheritdoc} @@ -378,6 +380,12 @@ private function bootKernel() { $this->container = $kernel->getContainer(); + // Prepare session and add it to the request. + /** @var \Symfony\Component\HttpFoundation\Session\SessionInterface $session */ + $session = $this->container->get('session'); + $session->start(); + $request->setSession($session); + // Run database tasks and check for errors. $installer_class = $namespace . "\\Install\\Tasks"; $errors = (new $installer_class())->runTasks(); @@ -550,6 +558,11 @@ public function register(ContainerBuilder $container) { ->addTag('persist'); $container ->setAlias('keyvalue', 'keyvalue.memory'); + $container + ->register('session_manager.memory', 'Drupal\KernelTests\Core\Session\MockSessionManager') + ->addArgument(new Reference('session_manager.metadata_bag')); + $container + ->setAlias('session_manager', 'session_manager.memory'); // Set the default language on the minimal container. $container->setParameter('language.default_values', Language::$defaultValues); diff --git a/core/tests/Drupal/KernelTests/KernelTestBaseTest.php b/core/tests/Drupal/KernelTests/KernelTestBaseTest.php index fefc271f91..5f23a08df8 100644 --- a/core/tests/Drupal/KernelTests/KernelTestBaseTest.php +++ b/core/tests/Drupal/KernelTests/KernelTestBaseTest.php @@ -138,6 +138,19 @@ public function testRegister() { // Ensure getting the router.route_provider does not trigger a deprecation // message that errors. $this->container->get('router.route_provider'); + + // Verify that a usable session is available from the request. + /** @var \Symfony\Component\HttpFoundation\Session\SessionInterface $session */ + $session = $request->getSession(); + + $this->assertNotNull($session); + $this->assertTrue($session->isStarted()); + + $session->set('some-val', 'do-not-cleanup'); + $this->assertEquals('do-not-cleanup', $session->get('some-val')); + + $session->set('some-other-val', 'do-cleanup'); + $this->assertEquals('do-cleanup', $session->remove('some-other-val')); } /** diff --git a/core/tests/Drupal/Tests/BrowserTestBase.php b/core/tests/Drupal/Tests/BrowserTestBase.php index cd75578c4e..d6961d5eec 100644 --- a/core/tests/Drupal/Tests/BrowserTestBase.php +++ b/core/tests/Drupal/Tests/BrowserTestBase.php @@ -424,6 +424,15 @@ protected function cleanupEnvironment() { \Drupal::service('file_system')->deleteRecursive($this->siteDirectory, [$this, 'filePreDeleteCallback']); } + /** + * Clear and stop a session triggered during test run. + */ + protected function cleanupSession() { + $session = $this->getSessionFromRequest(); + $session->clear(); + $session->save(); + } + /** * {@inheritdoc} */ @@ -432,6 +441,7 @@ protected function tearDown(): void { // Destroy the testing kernel. if (isset($this->kernel)) { + $this->cleanupSession(); $this->cleanupEnvironment(); $this->kernel->shutdown(); } @@ -538,6 +548,7 @@ public function installDrupal() { $this->doInstall(); $this->initSettings(); $this->container = $container = $this->initKernel(\Drupal::request()); + $this->prepareSession($container); $this->initConfig($container); $this->installDefaultThemeFromClassProperty($container); $this->installModulesFromClassProperty($container); diff --git a/core/tests/Drupal/Tests/RequestSessionTestTrait.php b/core/tests/Drupal/Tests/RequestSessionTestTrait.php new file mode 100644 index 0000000000..c26851d7f9 --- /dev/null +++ b/core/tests/Drupal/Tests/RequestSessionTestTrait.php @@ -0,0 +1,33 @@ +container->get('request_stack'); + $request = $request_stack->getCurrentRequest(); + + if ($request && $request->hasSession()) { + $session = $request->getSession(); + } + else { + /** @var \Symfony\Component\HttpFoundation\Session\Session $session */ + $session = $this->container->get('session'); + @trigger_error('Failed to retrieve session from current request, request stack was modified during test.', E_USER_WARNING); + } + + return $session; + } + +}