diff --git a/core/core.services.yml b/core/core.services.yml index 70c6441f91e0d8a7a749a6c01c716aaa698aefbc..c63bde7a4136a28b5db686eda7d15bf43b1b7aca 100644 --- a/core/core.services.yml +++ b/core/core.services.yml @@ -1126,7 +1126,7 @@ services: arguments: ['@state', '@current_user'] maintenance_mode_subscriber: class: Drupal\Core\EventSubscriber\MaintenanceModeSubscriber - arguments: ['@maintenance_mode', '@config.factory', '@string_translation', '@url_generator', '@current_user', '@bare_html_page_renderer'] + arguments: ['@maintenance_mode', '@config.factory', '@string_translation', '@url_generator', '@current_user', '@bare_html_page_renderer', '@messenger'] tags: - { name: event_subscriber } path_subscriber: @@ -1631,3 +1631,6 @@ services: arguments: ['@current_user', '@path.current', '@path.matcher', '@language_manager'] tags: - { name: event_subscriber } + messenger: + class: Drupal\Core\Messenger\SessionMessenger + arguments: ['@page_cache_kill_switch'] diff --git a/core/includes/bootstrap.inc b/core/includes/bootstrap.inc index ec1209740d766327585d744875275d1e6e7f28a4..4599b2b828e364cc5163d2d3b030530c4c56d49c 100644 --- a/core/includes/bootstrap.inc +++ b/core/includes/bootstrap.inc @@ -9,8 +9,6 @@ use Drupal\Component\Utility\SafeMarkup; use Drupal\Component\Utility\Unicode; use Drupal\Core\Logger\RfcLogLevel; -use Drupal\Core\Render\Markup; -use Drupal\Component\Render\MarkupInterface; use Drupal\Core\Session\AccountInterface; use Drupal\Core\Site\Settings; use Drupal\Core\Utility\Error; @@ -437,30 +435,15 @@ function watchdog_exception($type, Exception $exception, $message = NULL, $varia * * @see drupal_get_messages() * @see status-messages.html.twig + * + * @deprecated Deprecated as of Drupal 8.2. + * Use \Drupal::service('messenger')->addMessage() instead. */ function drupal_set_message($message = NULL, $type = 'status', $repeat = FALSE) { - if (isset($message)) { - if (!isset($_SESSION['messages'][$type])) { - $_SESSION['messages'][$type] = array(); - } - - // Convert strings which are safe to the simplest Markup objects. - if (!($message instanceof Markup) && $message instanceof MarkupInterface) { - $message = Markup::create((string) $message); - } - - // Do not use strict type checking so that equivalent string and - // MarkupInterface objects are detected. - if ($repeat || !in_array($message, $_SESSION['messages'][$type])) { - $_SESSION['messages'][$type][] = $message; - } - - // Mark this page as being uncacheable. - \Drupal::service('page_cache_kill_switch')->trigger(); - } - - // Messages not set when DB connection fails. - return isset($_SESSION['messages']) ? $_SESSION['messages'] : NULL; + /* @var \Drupal\Core\Messenger\MessengerInterface $messenger */ + $messenger = \Drupal::service('messenger'); + $messenger->addMessage($message, $type, $repeat); + return $messenger->getMessages(); } /** @@ -487,12 +470,19 @@ function drupal_set_message($message = NULL, $type = 'status', $repeat = FALSE) * * @see drupal_set_message() * @see status-messages.html.twig + * + * @deprecated Deprecated as of Drupal 8.2. + * Use \Drupal::service('messenger')->getMessages() or + * \Drupal::service('messenger')->getMessagesByType() instead. */ function drupal_get_messages($type = NULL, $clear_queue = TRUE) { - if ($messages = drupal_set_message()) { + /** @var \Drupal\Core\Messenger\MessengerInterface $messenger */ + $messenger = \Drupal::service('messenger'); + + if ($messages = $messenger->getMessages()) { if ($type) { if ($clear_queue) { - unset($_SESSION['messages'][$type]); + $messenger->deleteMessagesByType($type); } if (isset($messages[$type])) { return array($type => $messages[$type]); @@ -500,7 +490,7 @@ function drupal_get_messages($type = NULL, $clear_queue = TRUE) { } else { if ($clear_queue) { - unset($_SESSION['messages']); + $messenger->deleteMessages(); } return $messages; } diff --git a/core/lib/Drupal/Core/EventSubscriber/ExceptionJsonSubscriber.php b/core/lib/Drupal/Core/EventSubscriber/ExceptionJsonSubscriber.php index 01f36202de9093bb91598c7c7505e0ba130f5b3b..34eda1f8674fbd13b238d6790cdafb4c1592dfb5 100644 --- a/core/lib/Drupal/Core/EventSubscriber/ExceptionJsonSubscriber.php +++ b/core/lib/Drupal/Core/EventSubscriber/ExceptionJsonSubscriber.php @@ -27,6 +27,17 @@ protected static function getPriority() { return -75; } + /** + * Handles a 400 error for JSON. + * + * @param \Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent $event + * The event to process. + */ + public function on400(GetResponseForExceptionEvent $event) { + $response = new JsonResponse(array('message' => $event->getException()->getMessage()), Response::HTTP_BAD_REQUEST); + $event->setResponse($response); + } + /** * Handles a 403 error for JSON. * diff --git a/core/lib/Drupal/Core/EventSubscriber/MaintenanceModeSubscriber.php b/core/lib/Drupal/Core/EventSubscriber/MaintenanceModeSubscriber.php index 57d43ec0be11109ab209f1dba42686fc14fe9835..2e9f6fd5abc2ce291138f39fcc1b0ccff1b37a4a 100644 --- a/core/lib/Drupal/Core/EventSubscriber/MaintenanceModeSubscriber.php +++ b/core/lib/Drupal/Core/EventSubscriber/MaintenanceModeSubscriber.php @@ -5,6 +5,7 @@ use Drupal\Component\Utility\SafeMarkup; use Drupal\Core\Config\ConfigFactoryInterface; use Drupal\Core\Render\BareHtmlPageRendererInterface; +use Drupal\Core\Messenger\MessengerInterface; use Drupal\Core\Routing\RouteMatch; use Drupal\Core\Routing\UrlGeneratorInterface; use Drupal\Core\Session\AccountInterface; @@ -58,6 +59,13 @@ class MaintenanceModeSubscriber implements EventSubscriberInterface { */ protected $bareHtmlPageRenderer; + /** + * The messenger. + * + * @var \Drupal\Core\Messenger\MessengerInterface + */ + protected $messenger; + /** * Constructs a new MaintenanceModeSubscriber. * @@ -73,14 +81,17 @@ class MaintenanceModeSubscriber implements EventSubscriberInterface { * The current user. * @param \Drupal\Core\Render\BareHtmlPageRendererInterface $bare_html_page_renderer * The bare HTML page renderer. + * @param \Drupal\Core\Messenger\MessengerInterface $messenger + * The messenger. */ - public function __construct(MaintenanceModeInterface $maintenance_mode, ConfigFactoryInterface $config_factory, TranslationInterface $translation, UrlGeneratorInterface $url_generator, AccountInterface $account, BareHtmlPageRendererInterface $bare_html_page_renderer) { + public function __construct(MaintenanceModeInterface $maintenance_mode, ConfigFactoryInterface $config_factory, TranslationInterface $translation, UrlGeneratorInterface $url_generator, AccountInterface $account, BareHtmlPageRendererInterface $bare_html_page_renderer, MessengerInterface $messenger) { $this->maintenanceMode = $maintenance_mode; $this->config = $config_factory; $this->stringTranslation = $translation; $this->urlGenerator = $url_generator; $this->account = $account; $this->bareHtmlPageRenderer = $bare_html_page_renderer; + $this->messenger = $messenger; } /** @@ -118,10 +129,10 @@ public function onKernelRequestMaintenance(GetResponseEvent $event) { // settings page. if ($route_match->getRouteName() != 'system.site_maintenance_mode') { if ($this->account->hasPermission('administer site configuration')) { - $this->drupalSetMessage($this->t('Operating in maintenance mode. <a href=":url">Go online.</a>', array(':url' => $this->urlGenerator->generate('system.site_maintenance_mode'))), 'status', FALSE); + $this->messenger->addMessage($this->t('Operating in maintenance mode. <a href=":url">Go online.</a>', [':url' => $this->urlGenerator->generate('system.site_maintenance_mode')]), 'status', FALSE); } else { - $this->drupalSetMessage($this->t('Operating in maintenance mode.'), 'status', FALSE); + $this->messenger->addMessage($this->t('Operating in maintenance mode.'), 'status', FALSE); } } } @@ -140,13 +151,6 @@ protected function getSiteMaintenanceMessage() { )); } - /** - * Wraps the drupal_set_message function. - */ - protected function drupalSetMessage($message = NULL, $type = 'status', $repeat = FALSE) { - return drupal_set_message($message, $type, $repeat); - } - /** * {@inheritdoc} */ diff --git a/core/lib/Drupal/Core/Installer/InstallerServiceProvider.php b/core/lib/Drupal/Core/Installer/InstallerServiceProvider.php index 1ace991de83c30fefb697d384005d277ec29d938..f5fcf786a5a6534156efbcb21a5827eb2dd7f8d3 100644 --- a/core/lib/Drupal/Core/Installer/InstallerServiceProvider.php +++ b/core/lib/Drupal/Core/Installer/InstallerServiceProvider.php @@ -5,6 +5,7 @@ use Drupal\Core\DependencyInjection\ContainerBuilder; use Drupal\Core\DependencyInjection\ServiceProviderInterface; use Drupal\Core\DependencyInjection\ServiceModifierInterface; +use Drupal\Core\Messenger\StaticMessenger; use Symfony\Component\DependencyInjection\Reference; /** @@ -34,6 +35,9 @@ public function register(ContainerBuilder $container) { ->register('keyvalue', 'Drupal\Core\KeyValueStore\KeyValueMemoryFactory'); $container ->register('keyvalue.expirable', 'Drupal\Core\KeyValueStore\KeyValueNullExpirableFactory'); + $definition = $container->getDefinition('messenger'); + $definition->setClass(StaticMessenger::class); + $definition->setArguments([new Reference('page_cache_kill_switch')]); // Replace services with no-op implementations. $container diff --git a/core/lib/Drupal/Core/Messenger/MessengerInterface.php b/core/lib/Drupal/Core/Messenger/MessengerInterface.php new file mode 100644 index 0000000000000000000000000000000000000000..da36f04216cda2666c721ce5c60b64029878425c --- /dev/null +++ b/core/lib/Drupal/Core/Messenger/MessengerInterface.php @@ -0,0 +1,84 @@ +<?php + +namespace Drupal\Core\Messenger; + +/** + * Stores runtime messages sent out to individual users on the page. + * + * An example for these messages is for example: "Content X got saved". + */ +interface MessengerInterface { + + /** + * A status message. + */ + const TYPE_STATUS = 'status'; + + /** + * A warning. + */ + const TYPE_WARNING = 'warning'; + + /** + * An error. + */ + const TYPE_ERROR = 'error'; + + /** + * Adds a new message to the queue. + * + * @param string|\Drupal\Component\Render\MarkupInterface $message + * (optional) The translated message to be displayed to the user. For + * consistency with other messages, it should begin with a capital letter + * and end with a period. + * @param string $type + * (optional) The message's type. Either self::TYPE_STATUS, + * self::TYPE_WARNING, or self::TYPE_ERROR. + * @param bool $repeat + * (optional) If this is FALSE and the message is already set, then the + * message won't be repeated. Defaults to FALSE. + * + * @return $this + */ + public function addMessage($message, $type = self::TYPE_STATUS, $repeat = FALSE); + + /** + * Gets all messages. + * + * @return string[][]|\Drupal\Component\Render\MarkupInterface[][] + * Keys are message types and values are indexed arrays of messages. Message + * types are either self::TYPE_STATUS, self::TYPE_WARNING, or + * self::TYPE_ERROR. + */ + public function getMessages(); + + /** + * Gets all messages of a certain type. + * + * @param string $type + * The messages' type. Either self::TYPE_STATUS, self::TYPE_WARNING, + * or self::TYPE_ERROR. + * + * @return string[]|\Drupal\Component\Render\MarkupInterface[] + */ + public function getMessagesByType($type); + + /** + * Deletes all messages. + * + * @return $this + */ + public function deleteMessages(); + + /** + * Deletes all messages of a certain type. + * + * @param string $type + * The messages' type. Either self::TYPE_STATUS, self::TYPE_WARNING, or + * self::TYPE_ERROR. + * + * @return $this + */ + public function deleteMessagesByType($type); + +} diff --git a/core/lib/Drupal/Core/Messenger/SessionMessenger.php b/core/lib/Drupal/Core/Messenger/SessionMessenger.php new file mode 100644 index 0000000000000000000000000000000000000000..fc858fda513f250b2d4ba7a7572374642a5315c7 --- /dev/null +++ b/core/lib/Drupal/Core/Messenger/SessionMessenger.php @@ -0,0 +1,94 @@ +<?php + +namespace Drupal\Core\Messenger; + +use Drupal\Component\Render\MarkupInterface; +use Drupal\Core\PageCache\ResponsePolicy\KillSwitch; +use Drupal\Core\Render\Markup; + +/** + * Provides a session-based messenger. + */ +class SessionMessenger implements MessengerInterface { + + /** + * The page caching kill switch. + * + * @var \Drupal\Core\PageCache\ResponsePolicy\KillSwitch + */ + protected $pageCacheKillSwitch; + + /** + * Constructs a new instance. + * + * @param \Drupal\Core\Session\SessionManagerInterface + * @param \Drupal\Core\PageCache\ResponsePolicy\KillSwitch $page_cache_kill_switch + * The page caching kill switch. + */ + public function __construct(KillSwitch $page_cache_kill_switch) { + $this->pageCacheKillSwitch = $page_cache_kill_switch; + } + + /** + * {@inheritdoc} + */ + public function addMessage($message, $type = self::TYPE_STATUS, $repeat = FALSE) { + if (!isset($_SESSION['messages'][$type])) { + $_SESSION['messages'][$type] = []; + } + + // Convert strings which are safe to the simplest Markup objects. + if (!($message instanceof Markup) && $message instanceof MarkupInterface) { + $message = Markup::create((string) $message); + } + + // Do not use strict type checking so that equivalent string and + // \Drupal\Core\Render\Markup objects are detected. + if ($repeat || !in_array($message, $_SESSION['messages'][$type])) { + $_SESSION['messages'][$type][] = $message; + $this->pageCacheKillSwitch->trigger(); + } + + return $this; + } + + /** + * {@inheritdoc} + */ + public function getMessages() { + $messages = isset($_SESSION['messages']) ? $_SESSION['messages'] : []; + foreach ($messages as $type => $messages_by_type) { + $messages[$type] = $messages_by_type; + } + + return $messages; + } + + /** + * {@inheritdoc} + */ + public function getMessagesByType($type) { + $messages = isset($_SESSION['messages']) && isset($_SESSION['messages'][$type]) ? $_SESSION['messages'][$type] : []; + + return $messages; + } + + /** + * {@inheritdoc} + */ + public function deleteMessages() { + unset($_SESSION['messages']); + + return $this; + } + + /** + * {@inheritdoc} + */ + public function deleteMessagesByType($type) { + unset($_SESSION['messages'][$type]); + + return $this; + } + +} diff --git a/core/lib/Drupal/Core/Messenger/StaticMessenger.php b/core/lib/Drupal/Core/Messenger/StaticMessenger.php new file mode 100644 index 0000000000000000000000000000000000000000..f2e0f51101aa3f9b4feb45517e3d6bb3246c738d --- /dev/null +++ b/core/lib/Drupal/Core/Messenger/StaticMessenger.php @@ -0,0 +1,104 @@ +<?php + +namespace Drupal\Core\Messenger; + +use Drupal\Component\Render\MarkupInterface; +use Drupal\Core\PageCache\ResponsePolicy\KillSwitch; +use Drupal\Core\Render\Markup; + +/** + * Provides a messenger that stores messages for this request only. + */ +class StaticMessenger implements MessengerInterface { + + /** + * The messages that have been set. + * + * @var array[] + * Keys are either self::TYPE_STATUS, self::TYPE_WARNING, or + * self::TYPE_ERROR. Values are arrays of arrays with the following keys: + * - message (string): the message. + * - safe (bool): whether the message is marked as safe markup. + */ + protected $messages = []; + + /** + * The page caching kill switch. + * + * @var \Drupal\Core\PageCache\ResponsePolicy\KillSwitch + */ + protected $pageCacheKillSwitch; + + /** + * Constructs a new instance. + * + * @param \Drupal\Core\PageCache\ResponsePolicy\KillSwitch $page_cache_kill_switch + * The page caching kill switch. + */ + public function __construct(KillSwitch $page_cache_kill_switch) { + $this->pageCacheKillSwitch = $page_cache_kill_switch; + } + + /** + * {@inheritdoc} + */ + public function addMessage($message, $type = self::TYPE_STATUS, $repeat = FALSE) { + if (!isset($this->messages[$type])) { + $this->messages[$type] = []; + } + + // Convert strings which are safe to the simplest Markup objects. + if (!($message instanceof Markup) && $message instanceof MarkupInterface) { + $message = Markup::create((string) $message); + } + + // Do not use strict type checking so that equivalent string and + // MarkupInterface objects are detected. + if ($repeat || !in_array($message, $this->messages[$type])) { + $this->messages[$type][] = $message; + $this->pageCacheKillSwitch->trigger(); + } + + return $this; + } + + /** + * {@inheritdoc} + */ + public function getMessages() { + $messages = isset($this->messages) ? $this->messages : []; + foreach ($messages as $type => $messages_by_type) { + $messages[$type] = $messages_by_type; + } + + return $messages; + } + + /** + * {@inheritdoc} + */ + public function getMessagesByType($type) { + $messages = isset($this->messages) && isset($this->messages[$type]) ? $this->messages[$type] : []; + + return $messages; + } + + /** + * {@inheritdoc} + */ + public function deleteMessages() { + unset($this->messages); + + return $this; + } + + /** + * {@inheritdoc} + */ + public function deleteMessagesByType($type) { + unset($this->messages[$type]); + + return $this; + } + +} diff --git a/core/modules/serialization/serialization.services.yml b/core/modules/serialization/serialization.services.yml index c510ab16190695ecb9d76b5a24a384dbf8aea5e9..8b570c076308abff5f3948b3468102b5cb235db5 100644 --- a/core/modules/serialization/serialization.services.yml +++ b/core/modules/serialization/serialization.services.yml @@ -64,3 +64,13 @@ services: class: Drupal\serialization\EntityResolver\TargetIdResolver tags: - { name: entity_resolver} + serialization.exception.default: + class: Drupal\serialization\EventSubscriber\DefaultExceptionSubscriber + tags: + - { name: event_subscriber } + arguments: ['@serializer', '%serializer.formats%'] + serialization.user_route_alter_subscriber: + class: Drupal\serialization\EventSubscriber\UserRouteAlterSubscriber + tags: + - { name: event_subscriber } + arguments: ['@serializer', '%serializer.formats%'] diff --git a/core/modules/serialization/src/EventSubscriber/DefaultExceptionSubscriber.php b/core/modules/serialization/src/EventSubscriber/DefaultExceptionSubscriber.php index 753d3c10b8bce7df1dc0e1a0b864f64f0b62da4a..ba91836377ecfb8e4bf589be859912e64f2eb328 100644 --- a/core/modules/serialization/src/EventSubscriber/DefaultExceptionSubscriber.php +++ b/core/modules/serialization/src/EventSubscriber/DefaultExceptionSubscriber.php @@ -115,6 +115,16 @@ public function on422(GetResponseForExceptionEvent $event) { $this->setEventResponse($event, Response::HTTP_UNPROCESSABLE_ENTITY); } + /** + * Handles a 429 error for HTTP. + * + * @param \Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent $event + * The event to process. + */ + public function on429(GetResponseForExceptionEvent $event) { + $this->setEventResponse($event, Response::HTTP_TOO_MANY_REQUESTS); + } + /** * Sets the Response for the exception event. * diff --git a/core/modules/serialization/src/EventSubscriber/UserRouteAlterSubscriber.php b/core/modules/serialization/src/EventSubscriber/UserRouteAlterSubscriber.php new file mode 100644 index 0000000000000000000000000000000000000000..a8b71c7bbb6963623aa4bc29eb54788a98a6d4da --- /dev/null +++ b/core/modules/serialization/src/EventSubscriber/UserRouteAlterSubscriber.php @@ -0,0 +1,72 @@ +<?php + +namespace Drupal\serialization\EventSubscriber; + +use Drupal\Core\Routing\RouteBuildEvent; +use Drupal\Core\Routing\RoutingEvents; +use Symfony\Component\EventDispatcher\EventSubscriberInterface; +use Symfony\Component\Serializer\SerializerInterface; + +/** + * Alters user authentication routes to support additional serialization formats. + */ +class UserRouteAlterSubscriber implements EventSubscriberInterface { + + /** + * The serializer. + * + * @var \Symfony\Component\Serializer\Serializer + */ + protected $serializer; + + /** + * The available serialization formats. + * + * @var array + */ + protected $serializerFormats = []; + + /** + * UserRouteAlterSubscriber constructor. + * + * @param \Symfony\Component\Serializer\SerializerInterface $serializer + * The serializer service. + * @param array $serializer_formats + * The available serializer formats. + */ + public function __construct(SerializerInterface $serializer, array $serializer_formats) { + $this->serializer = $serializer; + $this->serializerFormats = $serializer_formats; + } + + /** + * {@inheritdoc} + */ + public static function getSubscribedEvents() { + $events[RoutingEvents::ALTER][] = 'onRoutingAlterAddFormats'; + return $events; + } + + /** + * Adds supported formats to the user authentication HTTP routes. + * + * @param \Drupal\Core\Routing\RouteBuildEvent $event + * The event to process. + */ + public function onRoutingAlterAddFormats(RouteBuildEvent $event) { + $route_names = [ + 'user.login_status.http', + 'user.login.http', + 'user.logout.http', + ]; + $routes = $event->getRouteCollection(); + foreach ($route_names as $route_name) { + if ($route = $routes->get($route_name)) { + $formats = explode('|', $route->getRequirement('_format')); + $formats = array_unique($formats + $this->serializerFormats); + $route->setRequirement('_format', implode('|', $formats)); + } + } + } + +} diff --git a/core/modules/user/src/Controller/UserAuthenticationController.php b/core/modules/user/src/Controller/UserAuthenticationController.php new file mode 100644 index 0000000000000000000000000000000000000000..569a1211ac078455b33882c5c6fe40710f580b44 --- /dev/null +++ b/core/modules/user/src/Controller/UserAuthenticationController.php @@ -0,0 +1,345 @@ +<?php + +namespace Drupal\user\Controller; + +use Drupal\Core\Access\CsrfTokenGenerator; +use Drupal\Core\Controller\ControllerBase; +use Drupal\Core\DependencyInjection\ContainerInjectionInterface; +use Drupal\Core\Flood\FloodInterface; +use Drupal\Core\Routing\RouteProviderInterface; +use Drupal\user\UserAuthInterface; +use Drupal\user\UserInterface; +use Drupal\user\UserStorageInterface; +use Symfony\Component\DependencyInjection\ContainerInterface; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException; +use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; +use Symfony\Component\Serializer\Encoder\JsonEncoder; +use Symfony\Component\Serializer\Serializer; + +/** + * Provides controllers for login, login status and logout via HTTP requests. + */ +class UserAuthenticationController extends ControllerBase implements ContainerInjectionInterface { + + /** + * String sent in responses, to describe the user as being logged in. + * + * @var string + */ + const LOGGED_IN = 1; + + /** + * String sent in responses, to describe the user as being logged out. + * + * @var string + */ + const LOGGED_OUT = 0; + + /** + * The flood controller. + * + * @var \Drupal\Core\Flood\FloodInterface + */ + protected $flood; + + /** + * The user storage. + * + * @var \Drupal\user\UserStorageInterface + */ + protected $userStorage; + + /** + * The CSRF token generator. + * + * @var \Drupal\Core\Access\CsrfTokenGenerator + */ + protected $csrfToken; + + /** + * The user authentication. + * + * @var \Drupal\user\UserAuthInterface + */ + protected $userAuth; + + /** + * The route provider. + * + * @var \Drupal\Core\Routing\RouteProviderInterface + */ + protected $routeProvider; + + /** + * The serializer. + * + * @var \Symfony\Component\Serializer\Serializer + */ + protected $serializer; + + /** + * The available serialization formats. + * + * @var array + */ + protected $serializerFormats = []; + + /** + * Constructs a new UserAuthenticationController object. + * + * @param \Drupal\Core\Flood\FloodInterface $flood + * The flood controller. + * @param \Drupal\user\UserStorageInterface $user_storage + * The user storage. + * @param \Drupal\Core\Access\CsrfTokenGenerator $csrf_token + * The CSRF token generator. + * @param \Drupal\user\UserAuthInterface $user_auth + * The user authentication. + * @param \Drupal\Core\Routing\RouteProviderInterface $route_provider + * The route provider. + * @param \Symfony\Component\Serializer\Serializer $serializer + * The serializer. + * @param array $serializer_formats + * The available serialization formats. + */ + public function __construct(FloodInterface $flood, UserStorageInterface $user_storage, CsrfTokenGenerator $csrf_token, UserAuthInterface $user_auth, RouteProviderInterface $route_provider, Serializer $serializer, array $serializer_formats) { + $this->flood = $flood; + $this->userStorage = $user_storage; + $this->csrfToken = $csrf_token; + $this->userAuth = $user_auth; + $this->serializer = $serializer; + $this->serializerFormats = $serializer_formats; + $this->routeProvider = $route_provider; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + if ($container->hasParameter('serializer.formats') && $container->has('serializer')) { + $serializer = $container->get('serializer'); + $formats = $container->getParameter('serializer.formats'); + } + else { + $formats = ['json']; + $encoders = [new JsonEncoder()]; + $serializer = new Serializer([], $encoders); + } + + return new static( + $container->get('flood'), + $container->get('entity_type.manager')->getStorage('user'), + $container->get('csrf_token'), + $container->get('user.auth'), + $container->get('router.route_provider'), + $serializer, + $formats + ); + } + + /** + * Logs in a user. + * + * @param \Symfony\Component\HttpFoundation\Request $request + * The request. + * + * @return \Symfony\Component\HttpFoundation\Response + * A response which contains the ID and CSRF token. + */ + public function login(Request $request) { + $format = $this->getRequestFormat($request); + + $content = $request->getContent(); + $credentials = $this->serializer->decode($content, $format); + if (!isset($credentials['name']) && !isset($credentials['pass'])) { + throw new BadRequestHttpException('Missing credentials.'); + } + + if (!isset($credentials['name'])) { + throw new BadRequestHttpException('Missing credentials.name.'); + } + if (!isset($credentials['pass'])) { + throw new BadRequestHttpException('Missing credentials.pass.'); + } + + $this->floodControl($request, $credentials['name']); + + if ($this->userIsBlocked($credentials['name'])) { + throw new BadRequestHttpException('The user has not been activated or is blocked.'); + } + + if ($uid = $this->userAuth->authenticate($credentials['name'], $credentials['pass'])) { + $this->flood->clear('user.http_login', $this->getLoginFloodIdentifier($request, $credentials['name'])); + /** @var \Drupal\user\UserInterface $user */ + $user = $this->userStorage->load($uid); + $this->userLoginFinalize($user); + + // Send basic metadata about the logged in user. + $response_data = []; + if ($user->get('uid')->access('view', $user)) { + $response_data['current_user']['uid'] = $user->id(); + } + if ($user->get('roles')->access('view', $user)) { + $response_data['current_user']['roles'] = $user->getRoles(); + } + if ($user->get('name')->access('view', $user)) { + $response_data['current_user']['name'] = $user->getAccountName(); + } + $response_data['csrf_token'] = $this->csrfToken->get('rest'); + + $logout_route = $this->routeProvider->getRouteByName('user.logout.http'); + // Trim '/' off path to match \Drupal\Core\Access\CsrfAccessCheck. + $logout_path = ltrim($logout_route->getPath(), '/'); + $response_data['logout_token'] = $this->csrfToken->get($logout_path); + + $encoded_response_data = $this->serializer->encode($response_data, $format); + return new Response($encoded_response_data); + } + + $flood_config = $this->config('user.flood'); + if ($identifier = $this->getLoginFloodIdentifier($request, $credentials['name'])) { + $this->flood->register('user.http_login', $flood_config->get('user_window'), $identifier); + } + // Always register an IP-based failed login event. + $this->flood->register('user.failed_login_ip', $flood_config->get('ip_window')); + throw new BadRequestHttpException('Sorry, unrecognized username or password.'); + } + + /** + * Verifies if the user is blocked. + * + * @param string $name + * The username. + * + * @return bool + * TRUE if the user is blocked, otherwise FALSE. + */ + protected function userIsBlocked($name) { + return user_is_blocked($name); + } + + /** + * Finalizes the user login. + * + * @param \Drupal\user\UserInterface $user + * The user. + */ + protected function userLoginFinalize(UserInterface $user) { + user_login_finalize($user); + } + + /** + * Logs out a user. + * + * @return \Drupal\rest\ResourceResponse + * The response object. + */ + public function logout() { + $this->userLogout(); + return new Response(NULL, 204); + } + + /** + * Logs the user out. + */ + protected function userLogout() { + user_logout(); + } + + /** + * Checks whether a user is logged in or not. + * + * @return \Symfony\Component\HttpFoundation\Response + * The response. + */ + public function loginStatus() { + if ($this->currentUser()->isAuthenticated()) { + $response = new Response(self::LOGGED_IN); + } + else { + $response = new Response(self::LOGGED_OUT); + } + $response->headers->set('Content-Type', 'text/plain'); + return $response; + } + + /** + * Gets the format of the current request. + * + * @param \Symfony\Component\HttpFoundation\Request $request + * The current request. + * + * @return string + * The format of the request. + */ + protected function getRequestFormat(Request $request) { + $format = $request->getRequestFormat(); + if (!in_array($format, $this->serializerFormats)) { + throw new BadRequestHttpException("Unrecognized format: $format."); + } + return $format; + } + + /** + * Enforces flood control for the current login request. + * + * @param \Symfony\Component\HttpFoundation\Request $request + * The current request. + * @param string $username + * The user name sent for login credentials. + */ + protected function floodControl(Request $request, $username) { + $flood_config = $this->config('user.flood'); + if (!$this->flood->isAllowed('user.failed_login_ip', $flood_config->get('ip_limit'), $flood_config->get('ip_window'))) { + throw new AccessDeniedHttpException('Access is blocked because of IP based flood prevention.', NULL, Response::HTTP_TOO_MANY_REQUESTS); + } + + if ($identifier = $this->getLoginFloodIdentifier($request, $username)) { + // Don't allow login if the limit for this user has been reached. + // Default is to allow 5 failed attempts every 6 hours. + if (!$this->flood->isAllowed('user.http_login', $flood_config->get('user_limit'), $flood_config->get('user_window'), $identifier)) { + if ($flood_config->get('uid_only')) { + $error_message = sprintf('There have been more than %s failed login attempts for this account. It is temporarily blocked. Try again later or request a new password.', $flood_config->get('user_limit')); + } + else { + $error_message = 'Too many failed login attempts from your IP address. This IP address is temporarily blocked.'; + } + throw new AccessDeniedHttpException($error_message, NULL, Response::HTTP_TOO_MANY_REQUESTS); + } + } + } + + /** + * Gets the login identifier for user login flood control. + * + * @param \Symfony\Component\HttpFoundation\Request $request + * The current request. + * @param string $username + * The username supplied in login credentials. + * + * @return string + * The login identifier or if the user does not exist an empty string. + */ + protected function getLoginFloodIdentifier(Request $request, $username) { + $flood_config = $this->config('user.flood'); + $accounts = $this->userStorage->loadByProperties(['name' => $username, 'status' => 1]); + if ($account = reset($accounts)) { + if ($flood_config->get('uid_only')) { + // Register flood events based on the uid only, so they apply for any + // IP address. This is the most secure option. + $identifier = $account->id(); + } + else { + // The default identifier is a combination of uid and IP address. This + // is less secure but more resistant to denial-of-service attacks that + // could lock out all users with public user names. + $identifier = $account->id() . '-' . $request->getClientIp(); + } + return $identifier; + } + return ''; + } + +} diff --git a/core/modules/user/tests/src/Functional/UserLoginHttpTest.php b/core/modules/user/tests/src/Functional/UserLoginHttpTest.php new file mode 100644 index 0000000000000000000000000000000000000000..356544119b66c58e1bfa78e320de2531bd415ff5 --- /dev/null +++ b/core/modules/user/tests/src/Functional/UserLoginHttpTest.php @@ -0,0 +1,421 @@ +<?php + +namespace Drupal\Tests\user\Functional; + +use Drupal\Core\Flood\DatabaseBackend; +use Drupal\Core\Url; +use Drupal\Tests\BrowserTestBase; +use Drupal\user\Controller\UserAuthenticationController; +use GuzzleHttp\Cookie\CookieJar; +use Psr\Http\Message\ResponseInterface; +use Symfony\Component\Serializer\Encoder\JsonEncoder; +use Symfony\Component\Serializer\Encoder\XmlEncoder; +use Symfony\Component\Serializer\Serializer; + +/** + * Tests login via direct HTTP. + * + * @group user + */ +class UserLoginHttpTest extends BrowserTestBase { + + /** + * The cookie jar. + * + * @var \GuzzleHttp\Cookie\CookieJar + */ + protected $cookies; + + /** + * The serializer. + * + * @var \Symfony\Component\Serializer\Serializer + */ + protected $serializer; + + /** + * {@inheritdoc} + */ + protected function setUp() { + parent::setUp(); + $this->cookies = new CookieJar(); + $encoders = [new JsonEncoder(), new XmlEncoder()]; + $this->serializer = new Serializer([], $encoders); + } + + /** + * Executes a login HTTP request. + * + * @param string $name + * The username. + * @param string $pass + * The user password. + * @param string $format + * The format to use to make the request. + * + * @return \Psr\Http\Message\ResponseInterface The HTTP response. + * The HTTP response. + */ + protected function loginRequest($name, $pass, $format = 'json') { + $user_login_url = Url::fromRoute('user.login.http') + ->setRouteParameter('_format', $format) + ->setAbsolute(); + + $request_body = []; + if (isset($name)) { + $request_body['name'] = $name; + } + if (isset($pass)) { + $request_body['pass'] = $pass; + } + + $result = \Drupal::httpClient()->post($user_login_url->toString(), [ + 'body' => $this->serializer->encode($request_body, $format), + 'headers' => [ + 'Accept' => "application/$format", + ], + 'http_errors' => FALSE, + 'cookies' => $this->cookies, + ]); + return $result; + } + + /** + * Tests user session life cycle. + */ + public function testLogin() { + $client = \Drupal::httpClient(); + foreach ([FALSE, TRUE] as $serialization_enabled_option) { + if ($serialization_enabled_option) { + /** @var \Drupal\Core\Extension\ModuleInstaller $module_installer */ + $module_installer = $this->container->get('module_installer'); + $module_installer->install(['serialization']); + $formats = ['json', 'xml']; + } + else { + // Without the serialization module only JSON is supported. + $formats = ['json']; + } + foreach ($formats as $format) { + // Create new user for each iteration to reset flood. + // Grant the user administer users permissions to they can see the + // 'roles' field. + $account = $this->drupalCreateUser(['administer users']); + $name = $account->getUsername(); + $pass = $account->passRaw; + + $login_status_url = $this->getLoginStatusUrlString($format); + $response = $client->get($login_status_url); + $this->assertHttpResponse($response, 200, UserAuthenticationController::LOGGED_OUT); + + // Flooded. + $this->config('user.flood') + ->set('user_limit', 3) + ->save(); + + $response = $this->loginRequest($name, 'wrong-pass', $format); + $this->assertHttpResponseWithMessage($response, 400, 'Sorry, unrecognized username or password.', $format); + + $response = $this->loginRequest($name, 'wrong-pass', $format); + $this->assertHttpResponseWithMessage($response, 400, 'Sorry, unrecognized username or password.', $format); + + $response = $this->loginRequest($name, 'wrong-pass', $format); + $this->assertHttpResponseWithMessage($response, 400, 'Sorry, unrecognized username or password.', $format); + + $response = $this->loginRequest($name, 'wrong-pass', $format); + $this->assertHttpResponseWithMessage($response, 403, 'Too many failed login attempts from your IP address. This IP address is temporarily blocked.', $format); + + // After testing the flood control we can increase the limit. + $this->config('user.flood') + ->set('user_limit', 100) + ->save(); + + $response = $this->loginRequest(NULL, NULL, $format); + $this->assertHttpResponseWithMessage($response, 400, 'Missing credentials.', $format); + + $response = $this->loginRequest(NULL, $pass, $format); + $this->assertHttpResponseWithMessage($response, 400, 'Missing credentials.name.', $format); + + $response = $this->loginRequest($name, NULL, $format); + $this->assertHttpResponseWithMessage($response, 400, 'Missing credentials.pass.', $format); + + // Blocked. + $account + ->block() + ->save(); + + $response = $this->loginRequest($name, $pass, $format); + $this->assertHttpResponseWithMessage($response, 400, 'The user has not been activated or is blocked.', $format); + + $account + ->activate() + ->save(); + + $response = $this->loginRequest($name, 'garbage', $format); + $this->assertHttpResponseWithMessage($response, 400, 'Sorry, unrecognized username or password.', $format); + + $response = $this->loginRequest('garbage', $pass, $format); + $this->assertHttpResponseWithMessage($response, 400, 'Sorry, unrecognized username or password.', $format); + + $response = $this->loginRequest($name, $pass, $format); + $this->assertEquals(200, $response->getStatusCode()); + $result_data = $this->serializer->decode($response->getBody(), $format); + $this->assertEquals($name, $result_data['current_user']['name']); + $this->assertEquals($account->id(), $result_data['current_user']['uid']); + $this->assertEquals($account->getRoles(), $result_data['current_user']['roles']); + $logout_token = $result_data['logout_token']; + + $response = $client->get($login_status_url, ['cookies' => $this->cookies]); + $this->assertHttpResponse($response, 200, UserAuthenticationController::LOGGED_IN); + + $response = $this->logoutRequest($format, $logout_token); + $this->assertEquals(204, $response->getStatusCode()); + + $response = $client->get($login_status_url, ['cookies' => $this->cookies]); + $this->assertHttpResponse($response, 200, UserAuthenticationController::LOGGED_OUT); + + $this->resetFlood(); + } + } + } + + /** + * Gets a value for a given key from the response. + * + * @param \Psr\Http\Message\ResponseInterface $response + * The response object. + * @param string $key + * The key for the value. + * @param string $format + * The encoded format. + * + * @return mixed + * The value for the key. + */ + protected function getResultValue(ResponseInterface $response, $key, $format) { + $decoded = $this->serializer->decode((string) $response->getBody(), $format); + if (is_array($decoded)) { + return $decoded[$key]; + } + else { + return $decoded->{$key}; + } + } + + /** + * Resets all flood entries. + */ + protected function resetFlood() { + $this->container->get('database')->delete(DatabaseBackend::TABLE_NAME)->execute(); + } + + /** + * Tests the global login flood control. + * + * @see \Drupal\basic_auth\Tests\Authentication\BasicAuthTest::testGlobalLoginFloodControl + * @see \Drupal\user\Tests\UserLoginTest::testGlobalLoginFloodControl + */ + public function testGlobalLoginFloodControl() { + $this->config('user.flood') + ->set('ip_limit', 2) + // Set a high per-user limit out so that it is not relevant in the test. + ->set('user_limit', 4000) + ->save(); + + $user = $this->drupalCreateUser([]); + $incorrect_user = clone $user; + $incorrect_user->passRaw .= 'incorrect'; + + // Try 2 failed logins. + for ($i = 0; $i < 2; $i++) { + $response = $this->loginRequest($incorrect_user->getUsername(), $incorrect_user->passRaw); + $this->assertEquals('400', $response->getStatusCode()); + } + + // IP limit has reached to its limit. Even valid user credentials will fail. + $response = $this->loginRequest($user->getUsername(), $user->passRaw); + $this->assertHttpResponseWithMessage($response, '403', 'Access is blocked because of IP based flood prevention.'); + } + + /** + * Checks a response for status code and body. + * + * @param \Psr\Http\Message\ResponseInterface $response + * The response object. + * @param int $expected_code + * The expected status code. + * @param mixed $expected_body + * The expected response body. + */ + protected function assertHttpResponse(ResponseInterface $response, $expected_code, $expected_body) { + $this->assertEquals($expected_code, $response->getStatusCode()); + $this->assertEquals($expected_body, (string) $response->getBody()); + } + + /** + * Checks a response for status code and message. + * + * @param \Psr\Http\Message\ResponseInterface $response + * The response object. + * @param int $expected_code + * The expected status code. + * @param string $expected_message + * The expected message encoded in response. + * @param string $format + * The format that the response is encoded in. + */ + protected function assertHttpResponseWithMessage(ResponseInterface $response, $expected_code, $expected_message, $format = 'json') { + $this->assertEquals($expected_code, $response->getStatusCode()); + $this->assertEquals($expected_message, $this->getResultValue($response, 'message', $format)); + } + + /** + * Test the per-user login flood control. + * + * @see \Drupal\user\Tests\UserLoginTest::testPerUserLoginFloodControl + * @see \Drupal\basic_auth\Tests\Authentication\BasicAuthTest::testPerUserLoginFloodControl + */ + public function testPerUserLoginFloodControl() { + foreach ([TRUE, FALSE] as $uid_only_setting) { + $this->config('user.flood') + // Set a high global limit out so that it is not relevant in the test. + ->set('ip_limit', 4000) + ->set('user_limit', 3) + ->set('uid_only', $uid_only_setting) + ->save(); + + $user1 = $this->drupalCreateUser([]); + $incorrect_user1 = clone $user1; + $incorrect_user1->passRaw .= 'incorrect'; + + $user2 = $this->drupalCreateUser([]); + + // Try 2 failed logins. + for ($i = 0; $i < 2; $i++) { + $response = $this->loginRequest($incorrect_user1->getUsername(), $incorrect_user1->passRaw); + $this->assertHttpResponseWithMessage($response, 400, 'Sorry, unrecognized username or password.'); + } + + // A successful login will reset the per-user flood control count. + $response = $this->loginRequest($user1->getUsername(), $user1->passRaw); + $result_data = $this->serializer->decode($response->getBody(), 'json'); + $this->logoutRequest('json', $result_data['logout_token']); + + // Try 3 failed logins for user 1, they will not trigger flood control. + for ($i = 0; $i < 3; $i++) { + $response = $this->loginRequest($incorrect_user1->getUsername(), $incorrect_user1->passRaw); + $this->assertHttpResponseWithMessage($response, 400, 'Sorry, unrecognized username or password.'); + } + + // Try one successful attempt for user 2, it should not trigger any + // flood control. + $this->drupalLogin($user2); + $this->drupalLogout(); + + // Try one more attempt for user 1, it should be rejected, even if the + // correct password has been used. + $response = $this->loginRequest($user1->getUsername(), $user1->passRaw); + // Depending on the uid_only setting the error message will be different. + if ($uid_only_setting) { + $excepted_message = 'There have been more than 3 failed login attempts for this account. It is temporarily blocked. Try again later or request a new password.'; + } + else { + $excepted_message = 'Too many failed login attempts from your IP address. This IP address is temporarily blocked.'; + } + $this->assertHttpResponseWithMessage($response, 403, $excepted_message); + } + + } + + /** + * Executes a logout HTTP request. + * + * @param string $format + * The format to use to make the request. + * @param string $logout_token + * The csrf token for user logout. + * + * @return \Psr\Http\Message\ResponseInterface The HTTP response. + * The HTTP response. + */ + protected function logoutRequest($format = 'json', $logout_token = '') { + /** @var \GuzzleHttp\Client $client */ + $client = $this->container->get('http_client'); + $user_logout_url = Url::fromRoute('user.logout.http') + ->setRouteParameter('_format', $format) + ->setAbsolute(); + if ($logout_token) { + $user_logout_url->setOption('query', ['token' => $logout_token]); + } + $post_options = [ + 'headers' => [ + 'Accept' => "application/$format", + ], + 'http_errors' => FALSE, + 'cookies' => $this->cookies, + ]; + + $response = $client->post($user_logout_url->toString(), $post_options); + return $response; + } + + /** + * Test csrf protection of User Logout route. + */ + public function testLogoutCsrfProtection() { + $client = \Drupal::httpClient(); + $login_status_url = $this->getLoginStatusUrlString(); + $account = $this->drupalCreateUser(); + $name = $account->getUsername(); + $pass = $account->passRaw; + + $response = $this->loginRequest($name, $pass); + $this->assertEquals(200, $response->getStatusCode()); + $result_data = $this->serializer->decode($response->getBody(), 'json'); + + $logout_token = $result_data['logout_token']; + + // Test third party site posting to current site with logout request. + // This should not logout the current user because it lacks the CSRF + // token. + $response = $this->logoutRequest('json'); + $this->assertEquals(403, $response->getStatusCode()); + + // Ensure still logged in. + $response = $client->get($login_status_url, ['cookies' => $this->cookies]); + $this->assertHttpResponse($response, 200, UserAuthenticationController::LOGGED_IN); + + // Try with an incorrect token. + $response = $this->logoutRequest('json', 'not-the-correct-token'); + $this->assertEquals(403, $response->getStatusCode()); + + // Ensure still logged in. + $response = $client->get($login_status_url, ['cookies' => $this->cookies]); + $this->assertHttpResponse($response, 200, UserAuthenticationController::LOGGED_IN); + + // Try a logout request with correct token. + $response = $this->logoutRequest('json', $logout_token); + $this->assertEquals(204, $response->getStatusCode()); + + // Ensure actually logged out. + $response = $client->get($login_status_url, ['cookies' => $this->cookies]); + $this->assertHttpResponse($response, 200, UserAuthenticationController::LOGGED_OUT); + } + + /** + * Gets the URL string for checking login. + * + * @param string $format + * The format to use to make the request. + * + * @return string + * The URL string. + */ + protected function getLoginStatusUrlString($format = 'json') { + $user_login_status_url = Url::fromRoute('user.login_status.http'); + $user_login_status_url->setRouteParameter('_format', $format); + $user_login_status_url->setAbsolute(); + return $user_login_status_url->toString(); + } + +} diff --git a/core/modules/user/user.routing.yml b/core/modules/user/user.routing.yml index 6eea7ececfe8974d419e507cc702616ca071766b..caea9795bcef51f6eab36548952863962a7c38c3 100644 --- a/core/modules/user/user.routing.yml +++ b/core/modules/user/user.routing.yml @@ -129,6 +129,34 @@ user.login: options: _maintenance_access: TRUE +user.login.http: + path: '/user/login' + defaults: + _controller: \Drupal\user\Controller\UserAuthenticationController::login + methods: [POST] + requirements: + _user_is_logged_in: 'FALSE' + _format: 'json' + +user.login_status.http: + path: '/user/login_status' + defaults: + _controller: \Drupal\user\Controller\UserAuthenticationController::loginStatus + methods: [GET] + requirements: + _access: 'TRUE' + _format: 'json' + +user.logout.http: + path: '/user/logout' + defaults: + _controller: \Drupal\user\Controller\UserAuthenticationController::logout + methods: [POST] + requirements: + _user_is_logged_in: 'TRUE' + _format: 'json' + _csrf_token: 'TRUE' + user.cancel_confirm: path: '/user/{user}/cancel/confirm/{timestamp}/{hashed_pass}' defaults: diff --git a/core/tests/Drupal/Tests/Core/Messenger/SessionMessengerTest.php b/core/tests/Drupal/Tests/Core/Messenger/SessionMessengerTest.php new file mode 100644 index 0000000000000000000000000000000000000000..2da979c0a20f4f504b6e62c1488f1e2eb11e20f0 --- /dev/null +++ b/core/tests/Drupal/Tests/Core/Messenger/SessionMessengerTest.php @@ -0,0 +1,100 @@ +<?php + +namespace Drupal\Tests\Core\Messenger; + +use Drupal\Core\Messenger\SessionMessenger; +use Drupal\Core\PageCache\ResponsePolicy\KillSwitch; +use Drupal\Tests\UnitTestCase; + +/** + * @coversDefaultClass \Drupal\Core\Messenger\SessionMessenger + * @group messenger + */ +class SessionMessengerTest extends UnitTestCase { + + /** + * A copy of any existing session data to restore after the test. + * + * @var array + */ + protected $existingSession; + + /** + * The messenger under test. + * + * @var \Drupal\Core\Messenger\SessionMessenger + */ + protected $messenger; + + /** + * The page caching kill switch. + * + * @var \Drupal\Core\PageCache\ResponsePolicy\KillSwitch|\PHPUnit_Framework_MockObject_MockObject + */ + protected $pageCacheKillSwitch; + + /** + * {@inheritdoc} + */ + public function setUp() { + $this->pageCacheKillSwitch = $this->prophesize(KillSwitch::class); + + $this->existingSession = isset($_SESSION) ? $_SESSION : NULL; + $_SESSION = []; + } + + /** + * {@inheritdoc} + */ + public function tearDown() { + if ($this->existingSession !== NULL) { + $_SESSION = $this->existingSession; + } + else { + unset($_SESSION); + } + } + + /** + * @covers ::addMessage + * @covers ::getMessages + * @covers ::getMessagesByType + * @covers ::deleteMessages + * @covers ::deleteMessagesByType + */ + public function testMessenger() { + $this->pageCacheKillSwitch->trigger()->shouldBeCalled(); + + $this->messenger = new SessionMessenger($this->pageCacheKillSwitch->reveal()); + + $message_a = $this->randomMachineName(); + $type_a = $this->randomMachineName(); + $message_b = $this->randomMachineName(); + $type_b = $this->randomMachineName(); + + // Test that if there are no messages, the default is an empty array. + $this->assertEquals($this->messenger->getMessages(), []); + + // Test that adding a message returns the messenger and that the message can + // be retrieved. + $this->assertEquals($this->messenger->addMessage($message_a, $type_a), $this->messenger); + $this->messenger->addMessage($message_a, $type_a); + $this->messenger->addMessage($message_a, $type_a, TRUE); + $this->messenger->addMessage($message_b, $type_b, TRUE); + $this->assertEquals($this->messenger->getMessages(), [ + $type_a => [$message_a, $message_a], + $type_b => [$message_b], + ]); + + // Test deleting messages of a certain type. + $this->assertEquals($this->messenger->deleteMessagesByType($type_a), $this->messenger); + $this->assertEquals($this->messenger->getMessages(), [ + $type_b => [$message_b], + ]); + + // Test deleting all messages. + $this->assertEquals($this->messenger->deleteMessages(), $this->messenger); + $this->assertEquals($this->messenger->getMessages(), []); + } + +} diff --git a/core/tests/Drupal/Tests/Core/Messenger/StaticMessengerTest.php b/core/tests/Drupal/Tests/Core/Messenger/StaticMessengerTest.php new file mode 100644 index 0000000000000000000000000000000000000000..d3158954497d81d304b1c5b4e24829c96e006692 --- /dev/null +++ b/core/tests/Drupal/Tests/Core/Messenger/StaticMessengerTest.php @@ -0,0 +1,80 @@ +<?php + +namespace Drupal\Tests\Core\Messenger; + +use Drupal\Core\Messenger\StaticMessenger; +use Drupal\Core\PageCache\ResponsePolicy\KillSwitch; +use Drupal\Tests\RandomGeneratorTrait; + +/** + * @coversDefaultClass \Drupal\Core\Messenger\StaticMessenger + * @group messenger + */ +class StaticMessengerTest extends \PHPUnit_Framework_TestCase { + + use RandomGeneratorTrait; + + /** + * The messenger under test. + * + * @var \Drupal\Core\Messenger\StaticMessenger + */ + protected $messenger; + + /** + * The page caching kill switch. + * + * @var \Drupal\Core\PageCache\ResponsePolicy\KillSwitch|\PHPUnit_Framework_MockObject_MockObject + */ + protected $pageCacheKillSwitch; + + /** + * {@inheritdoc} + */ + public function setUp() { + $this->pageCacheKillSwitch = $this->prophesize(KillSwitch::class); + } + + /** + * @covers ::addMessage + * @covers ::getMessages + * @covers ::getMessagesByType + * @covers ::deleteMessages + * @covers ::deleteMessagesByType + */ + public function testMessenger() { + $message_a = $this->randomMachineName(); + $type_a = $this->randomMachineName(); + $message_b = $this->randomMachineName(); + $type_b = $this->randomMachineName(); + + $this->pageCacheKillSwitch->trigger()->shouldBeCalled(); + + $this->messenger = new StaticMessenger($this->pageCacheKillSwitch->reveal()); + + // Test that if there are no messages, the default is an empty array. + $this->assertEquals($this->messenger->getMessages(), []); + + // Test that adding a message returns the messenger and that the message can + // be retrieved. + $this->assertSame($this->messenger->addMessage($message_a, $type_a), $this->messenger); + $this->messenger->addMessage($message_a, $type_a); + $this->messenger->addMessage($message_a, $type_a, TRUE); + $this->messenger->addMessage($message_b, $type_b, TRUE); + $this->assertEquals([ + $type_a => [$message_a, $message_a], + $type_b => [$message_b], + ], $this->messenger->getMessages()); + + // Test deleting messages of a certain type. + $this->assertEquals($this->messenger->deleteMessagesByType($type_a), $this->messenger); + $this->assertEquals([ + $type_b => [$message_b], + ], $this->messenger->getMessages()); + + // Test deleting all messages. + $this->assertEquals($this->messenger->deleteMessages(), $this->messenger); + $this->assertEquals([], $this->messenger->getMessages()); + } + +}