diff --git a/core/lib/Drupal/Core/EventSubscriber/ExceptionJsonSubscriber.php b/core/lib/Drupal/Core/EventSubscriber/ExceptionJsonSubscriber.php index 34eda1f..3accdbf 100644 --- a/core/lib/Drupal/Core/EventSubscriber/ExceptionJsonSubscriber.php +++ b/core/lib/Drupal/Core/EventSubscriber/ExceptionJsonSubscriber.php @@ -34,8 +34,7 @@ protected static function getPriority() { * The event to process. */ public function on400(GetResponseForExceptionEvent $event) { - $response = new JsonResponse(array('message' => $event->getException()->getMessage()), Response::HTTP_BAD_REQUEST); - $event->setResponse($response); + $this->setEventResponse($event, Response::HTTP_BAD_REQUEST); } /** @@ -45,8 +44,7 @@ public function on400(GetResponseForExceptionEvent $event) { * The event to process. */ public function on403(GetResponseForExceptionEvent $event) { - $response = new JsonResponse(array('message' => $event->getException()->getMessage()), Response::HTTP_FORBIDDEN); - $event->setResponse($response); + $this->setEventResponse($event, Response::HTTP_FORBIDDEN); } /** @@ -56,8 +54,7 @@ public function on403(GetResponseForExceptionEvent $event) { * The event to process. */ public function on404(GetResponseForExceptionEvent $event) { - $response = new JsonResponse(array('message' => $event->getException()->getMessage()), Response::HTTP_NOT_FOUND); - $event->setResponse($response); + $this->setEventResponse($event, Response::HTTP_NOT_FOUND); } /** @@ -67,8 +64,7 @@ public function on404(GetResponseForExceptionEvent $event) { * The event to process. */ public function on405(GetResponseForExceptionEvent $event) { - $response = new JsonResponse(array('message' => $event->getException()->getMessage()), Response::HTTP_METHOD_NOT_ALLOWED); - $event->setResponse($response); + $this->setEventResponse($event, Response::HTTP_METHOD_NOT_ALLOWED); } /** @@ -78,7 +74,19 @@ public function on405(GetResponseForExceptionEvent $event) { * The event to process. */ public function on406(GetResponseForExceptionEvent $event) { - $response = new JsonResponse(['message' => $event->getException()->getMessage()], Response::HTTP_NOT_ACCEPTABLE); + $this->setEventResponse($event, Response::HTTP_NOT_ACCEPTABLE); + } + + /** + * Sets the Response for the exception event. + * + * @param \Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent $event + * The current exception event. + * @param int $status + * The HTTP status code to set for the response. + */ + protected function setEventResponse(GetResponseForExceptionEvent $event, $status) { + $response = new JsonResponse(['message' => $event->getException()->getMessage()], $status); $event->setResponse($response); } diff --git a/core/modules/serialization/serialization.services.yml b/core/modules/serialization/serialization.services.yml index c510ab1..3a1ea4a 100644 --- a/core/modules/serialization/serialization.services.yml +++ b/core/modules/serialization/serialization.services.yml @@ -64,3 +64,8 @@ services: class: Drupal\serialization\EntityResolver\TargetIdResolver tags: - { name: entity_resolver} + serialization.exception.default_http: + class: Drupal\serialization\EventSubscriber\ExceptionHttpSubscriber + tags: + - { name: event_subscriber } + arguments: ['@serializer', '%serializer.formats%'] diff --git a/core/modules/serialization/src/EventSubscriber/ExceptionHttpSubscriber.php b/core/modules/serialization/src/EventSubscriber/ExceptionHttpSubscriber.php new file mode 100644 index 0000000..23a72b6 --- /dev/null +++ b/core/modules/serialization/src/EventSubscriber/ExceptionHttpSubscriber.php @@ -0,0 +1,124 @@ +serializer = $serializer; + $this->serializerFormats = $serializer_formats; + } + + /** + * {@inheritdoc} + */ + protected function getHandledFormats() { + return $this->serializerFormats; + } + + /** + * {@inheritdoc} + */ + protected static function getPriority() { + // This will fire after the most common HTML handler, since HTML requests + // are still more common than HTTP requests. + return -75; + } + + /** + * Handles a 400 error for HTTP. + * + * @param \Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent $event + * The event to process. + */ + public function on400(GetResponseForExceptionEvent $event) { + $this->setEventResponse($event, Response::HTTP_BAD_REQUEST); + } + + /** + * Handles a 403 error for HTTP. + * + * @param \Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent $event + * The event to process. + */ + public function on403(GetResponseForExceptionEvent $event) { + $this->setEventResponse($event, Response::HTTP_FORBIDDEN); + } + + /** + * Handles a 404 error for HTTP. + * + * @param \Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent $event + * The event to process. + */ + public function on404(GetResponseForExceptionEvent $event) { + $this->setEventResponse($event, Response::HTTP_NOT_FOUND); + } + + /** + * Handles a 405 error for HTTP. + * + * @param \Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent $event + * The event to process. + */ + public function on405(GetResponseForExceptionEvent $event) { + $this->setEventResponse($event, Response::HTTP_METHOD_NOT_ALLOWED); + } + + /** + * Handles a 406 error for HTTP. + * + * @param \Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent $event + * The event to process. + */ + public function on406(GetResponseForExceptionEvent $event) { + $this->setEventResponse($event, Response::HTTP_NOT_ACCEPTABLE); + } + + /** + * Sets the Response for the exception event. + * + * @param \Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent $event + * The current exception event. + * @param int $status + * The HTTP status code to set for the response. + */ + protected function setEventResponse(GetResponseForExceptionEvent $event, $status) { + $format = $event->getRequest()->getRequestFormat(); + $content = ['message' => $event->getException()->getMessage()]; + $encoded_content = $this->serializer->serialize($content, $format); + $response = new Response($encoded_content, $status); + $event->setResponse($response); + } + +} diff --git a/core/modules/user/src/Controller/UserAuthenticationController.php b/core/modules/user/src/Controller/UserAuthenticationController.php new file mode 100644 index 0000000..3027fb9 --- /dev/null +++ b/core/modules/user/src/Controller/UserAuthenticationController.php @@ -0,0 +1,276 @@ +flood = $flood; + $this->userStorage = $user_storage; + $this->csrfToken = $csrf_token; + $this->userAuth = $user_auth; + $this->serializer = $serializer; + $this->serializerFormats = $serializer_formats; + } + + /** + * {@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'), + $serializer, + $formats + ); + } + + /** + * Logs in a user. + * + * @param \Symfony\Component\HttpFoundation\Request $request + * The request. + * + * @return \Symfony\Component\HttpFoundation\Response + * Returns 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.'); + } + + if (!$this->isFloodBlocked()) { + throw new BadRequestHttpException('Blocked.'); + } + + 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'])) { + /** @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')) { + $response_data['current_user']['uid'] = $user->id(); + } + if ($user->get('roles')->access('view')) { + $response_data['current_user']['roles'] = $user->getRoles(); + } + if ($user->get('name')->access('view')) { + $response_data['current_user']['name'] = $user->getAccountName(); + } + $response_data['csrf_token'] = $this->csrfToken->get('rest'); + + $encoded_response_data = $this->serializer->encode($response_data, $format); + return new Response($encoded_response_data); + } + + $this->flood->register('rest.login_cookie', $this->configFactory->get('user.flood')->get('user_window')); + throw new BadRequestHttpException('Sorry, unrecognized username or password.'); + } + + /** + * Verifies if the user is blocked. + * + * @param string $name + * The username. + * + * @return bool + * Returns 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); + } + + /** + * Checks for flooding. + * + * @return bool + * TRUE if the user is allowed to proceed, FALSE otherwise. + */ + protected function isFloodBlocked() { + $config = $this->config('user.flood'); + $limit = $config->get('user_limit'); + $interval = $config->get('user_window'); + + return $this->flood->isAllowed('rest.login_cookie', $limit, $interval); + } + + /** + * Logs out a user. + * + * @return \Drupal\rest\ResourceResponse + */ + 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; + } + +} diff --git a/core/modules/user/src/Controller/UserLoginController.php b/core/modules/user/src/Controller/UserLoginController.php deleted file mode 100644 index 52b5673..0000000 --- a/core/modules/user/src/Controller/UserLoginController.php +++ /dev/null @@ -1,277 +0,0 @@ -flood = $flood; - $this->userStorage = $user_storage; - $this->csrfToken = $csrf_token; - $this->userAuth = $user_auth; - $this->serializerFormats = $serializer_formats; - $this->serializer = $serializer; - - } - - /** - * {@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', 'xml']; - $encoders = [new JsonEncoder(), new XmlEncoder()]; - $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'), - $serializer, - $formats - ); - } - - /** - * Controller to login a user. - * - * @param \Symfony\Component\HttpFoundation\Request $request - * The request. - * - * @return \Symfony\Component\HttpFoundation\Response - * Returns 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.'); - } - - if (!$this->isFloodBlocked()) { - throw new BadRequestHttpException('Blocked.'); - } - - 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'])) { - /** @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')) { - $response_data['current_user']['uid'] = $user->id(); - } - if ($user->get('roles')->access('view')) { - $response_data['current_user']['roles'] = $user->getRoles(); - } - if ($user->get('name')->access('view')) { - $response_data['current_user']['name'] = $user->getAccountName(); - } - $response_data['csrf_token'] = $this->csrfToken->get('rest'); - - $encoded_response_data = $this->serializer->encode($response_data, $format); - return new Response($encoded_response_data); - } - - $this->flood->register('rest.login_cookie', $this->configFactory->get('user.flood')->get('user_window')); - throw new BadRequestHttpException('Sorry, unrecognized username or password.'); - } - - /** - * Verifies if the user is blocked. - * - * @param string $name - * The username. - * - * @return bool - * Returns 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); - } - - /** - * Checks for flooding. - * - * @return bool - * TRUE if the user is allowed to proceed, FALSE otherwise. - */ - protected function isFloodBlocked() { - $config = $this->config('user.flood'); - $limit = $config->get('user_limit'); - $interval = $config->get('user_window'); - - return $this->flood->isAllowed('rest.login_cookie', $limit, $interval); - } - - - /** - * Controller to logout a user. - * - * @return \Drupal\rest\ResourceResponse - */ - public function logout() { - $this->userLogout(); - return new Response(NULL, 204); - } - - /** - * Logs the user out. - */ - protected function userLogout() { - user_logout(); - } - - /** - * Controller to show 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; - } - -} diff --git a/core/modules/user/tests/src/Functional/UserLoginHttpTest.php b/core/modules/user/tests/src/Functional/UserLoginHttpTest.php index 2fb1322..5e5d8a5 100644 --- a/core/modules/user/tests/src/Functional/UserLoginHttpTest.php +++ b/core/modules/user/tests/src/Functional/UserLoginHttpTest.php @@ -4,7 +4,7 @@ use Drupal\Core\Url; use Drupal\Tests\BrowserTestBase; -use Drupal\user\Controller\UserLoginController; +use Drupal\user\Controller\UserAuthenticationController; use GuzzleHttp\Cookie\CookieJar; use Symfony\Component\Serializer\Encoder\JsonEncoder; use Symfony\Component\Serializer\Serializer; @@ -17,12 +17,14 @@ class UserLoginHttpTest extends BrowserTestBase { /** + * The cookie jar. + * * @var \GuzzleHttp\Cookie\CookieJar */ protected $cookies; /** - * The Serializer. + * The serializer. * * @var \Symfony\Component\Serializer\Serializer */ @@ -44,11 +46,12 @@ protected function setUp() { * * @param string $name * The username. - * @param $pass + * @param string $pass * The user password. - * @ + * @param string $format + * The format to use to make the request. * - * @return \Psr\Http\Message\ResponseInterface + * @return \Psr\Http\Message\ResponseInterface The HTTP response. * The HTTP response. */ protected function loginRequest($name, $pass, $format) { @@ -76,14 +79,13 @@ protected function loginRequest($name, $pass, $format) { } /** - * Test user session life cycle. + * Tests user session life cycle. */ public function testLogin() { $account = $this->drupalCreateUser(); $name = $account->getUsername(); $pass = $account->passRaw; - $client = \Drupal::httpClient(); $formats = ['json']; @@ -95,7 +97,7 @@ public function testLogin() { $result = $client->post($user_login_status_url->toString()); $this->assertEquals(200, $result->getStatusCode()); - $this->assertEquals(UserLoginController::LOGGED_OUT, (string) $result->getBody()); + $this->assertEquals(UserAuthenticationController::LOGGED_OUT, (string) $result->getBody()); // Flooded. \Drupal::configFactory()->getEditable('user.flood') @@ -163,7 +165,7 @@ public function testLogin() { $result = $client->post($user_login_status_url->toString(), ['cookies' => $this->cookies]); $this->assertEquals(200, $result->getStatusCode()); - $this->assertEquals(UserLoginController::LOGGED_IN, (string) $result->getBody()); + $this->assertEquals(UserAuthenticationController::LOGGED_IN, (string) $result->getBody()); $user_logout_url = Url::fromRoute('user.logout.http')->setRouteParameter('_format', $format)->setAbsolute(); $result = $client->post($user_logout_url->toString(), [ @@ -177,7 +179,7 @@ public function testLogin() { $result = $client->post($user_login_status_url->toString(), ['cookies' => $this->cookies]); $this->assertEquals(200, $result->getStatusCode()); - $this->assertEquals(UserLoginController::LOGGED_OUT, (string) $result->getBody()); + $this->assertEquals(UserAuthenticationController::LOGGED_OUT, (string) $result->getBody()); } } @@ -185,15 +187,23 @@ public function testLogin() { /** * Encodes data for a request into a given format. * - * @param $data - * The data to be encoded - * @param $format + * @param mixed $data + * The data to be encoded. + * @param string $format * The format to be encoded into. */ protected function encode($data, $format) { return $this->serializer->encode($data, $format); } + /** + * Decodes data for a request from a given format. + * + * @param mixed $data + * The data to be encoded. + * @param string $format + * The format to be encoded into. + */ protected function decode($data, $format) { return $this->serializer->decode($data, $format); } diff --git a/core/modules/user/user.routing.yml b/core/modules/user/user.routing.yml index 27f98ae..09ac772 100644 --- a/core/modules/user/user.routing.yml +++ b/core/modules/user/user.routing.yml @@ -130,28 +130,31 @@ user.login: _maintenance_access: TRUE user.login.http: - path: '/user/login/http' + path: '/user/login' defaults: - _controller: \Drupal\user\Controller\UserLoginController::login + _controller: \Drupal\user\Controller\UserAuthenticationController::login methods: [POST] requirements: _user_is_logged_in: 'FALSE' + _format: 'json' user.login_status.http: - path: '/user/login/status' + path: '/user/login_status' defaults: - _controller: \Drupal\user\Controller\UserLoginController::loginStatus + _controller: \Drupal\user\Controller\UserAuthenticationController::loginStatus methods: [POST] requirements: _access: 'TRUE' + _format: 'json' user.logout.http: - path: '/user/logout/http' + path: '/user/logout' defaults: - _controller: \Drupal\user\Controller\UserLoginController::logout + _controller: \Drupal\user\Controller\UserAuthenticationController::logout methods: [POST] requirements: _user_is_logged_in: 'TRUE' + _format: 'json' user.cancel_confirm: path: '/user/{user}/cancel/confirm/{timestamp}/{hashed_pass}'