diff --git a/core/modules/rest/src/Plugin/ResourceBase.php b/core/modules/rest/src/Plugin/ResourceBase.php index 5b37a8f..2038fa9 100644 --- a/core/modules/rest/src/Plugin/ResourceBase.php +++ b/core/modules/rest/src/Plugin/ResourceBase.php @@ -8,6 +8,7 @@ namespace Drupal\rest\Plugin; use Drupal\Core\Access\AccessManagerInterface; +use Drupal\Core\Flood\FloodInterface; use Drupal\Core\Plugin\ContainerFactoryPluginInterface; use Drupal\Core\Plugin\PluginBase; use Psr\Log\LoggerInterface; @@ -59,8 +60,10 @@ * The available serialization formats. * @param \Psr\Log\LoggerInterface $logger * A logger instance. + * @param \Drupal\Core\Flood\FloodInterface $flood + * The flood control mechanism. */ - public function __construct(array $configuration, $plugin_id, $plugin_definition, array $serializer_formats, LoggerInterface $logger, $flood) { + public function __construct(array $configuration, $plugin_id, $plugin_definition, array $serializer_formats, LoggerInterface $logger, FloodInterface $flood) { parent::__construct($configuration, $plugin_id, $plugin_definition); $this->serializerFormats = $serializer_formats; $this->logger = $logger; @@ -77,8 +80,7 @@ public static function create(ContainerInterface $container, array $configuratio $plugin_definition, $container->getParameter('serializer.formats'), $container->get('logger.factory')->get('rest'), - // FIX ME - \Drupal::flood() + $container->get('flood') ); } @@ -232,10 +234,11 @@ protected function getBaseRoute($canonical_path, $method) { protected function restFloodControl($config, $name) { $limit = $config->get('user_limit'); $interval = $config->get('user_window'); - if (!\Drupal::flood()->isAllowed($name, $limit, $interval)) { + if (!$this->flood->isAllowed($name, $limit, $interval)) { return TRUE; } return FALSE; } + } diff --git a/core/modules/rest/src/Plugin/rest/resource/UserLoginResource.php b/core/modules/rest/src/Plugin/rest/resource/UserLoginResource.php index 14dadbf..ed9b0c5 100644 --- a/core/modules/rest/src/Plugin/rest/resource/UserLoginResource.php +++ b/core/modules/rest/src/Plugin/rest/resource/UserLoginResource.php @@ -7,9 +7,14 @@ namespace Drupal\rest\Plugin\rest\resource; +use Drupal\Core\Config\ConfigFactoryInterface; +use Drupal\Core\Flood\FloodInterface; use Drupal\rest\ResourceResponse; use Drupal\rest\Plugin\ResourceBase; use Drupal\user\Entity\User; +use Psr\Log\LoggerInterface; +use Symfony\Component\DependencyInjection\ContainerInterface; +use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; /** * Allows user logins by setting session cookies. @@ -22,6 +27,49 @@ class UserLoginResource extends ResourceBase { /** + * The config factory. + * + * @var \Drupal\Core\Config\ConfigFactoryInterface + */ + protected $configFactory; + + /** + * Constructs a new RestPermissions instance. + * + * @param array $configuration + * A configuration array containing information about the plugin instance. + * @param string $plugin_id + * The plugin_id for the plugin instance. + * @param mixed $plugin_definition + * The plugin implementation definition. + * @param array $serializer_formats + * The available serialization formats. + * @param LoggerInterface $loggery + * A logger instance. + * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory + * The config factory. + */ + public function __construct(array $configuration, $plugin_id, $plugin_definition, array $serializer_formats, LoggerInterface $loggery, ConfigFactoryInterface $config_factory, FloodInterface $flood) { + parent::__construct($configuration, $plugin_id, $plugin_definition, $serializer_formats, $loggery, $flood); + $this->configFactory = $config_factory; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { + return new static( + $configuration, + $plugin_id, + $plugin_definition, + $container->getParameter('serializer.formats'), + $container->get('logger.factory')->get('rest'), + $container->get('config.factory'), + $container->get('flood') + ); + } + + /** * Responds to the user login POST requests and log in a user. * * @param string[] $operation @@ -46,13 +94,13 @@ public function post(array $operation = array()) { if (!empty($operation['credentials'])) { return $this->login($operation['credentials']); } - return new ResourceResponse('Missing credentials.', 400, array()); + throw new BadRequestHttpException('Missing credentials.'); case 'logout': return $this->logout(); default: - return new ResourceResponse('Unsupported op.', 400, array()); + throw new BadRequestHttpException('Unsupported op.'); } } @@ -69,16 +117,21 @@ public function post(array $operation = array()) { protected function login(array $credentials = array()) { // Verify that the username is filled. if (!array_key_exists('name', $credentials)) { - return new ResourceResponse('Missing credentials.name.', 400, array()); + throw new BadRequestHttpException('Missing credentials.name.'); } - // Verify that the username is filled. + // Verify that the password is filled. if (!array_key_exists('pass', $credentials)) { - return new ResourceResponse('Missing credentials.pass.', 400, array()); + throw new BadRequestHttpException('Missing credentials.pass.'); } // Flood control. - if ($this->restFloodControl(\Drupal::config('user.flood'), 'rest.login_cookie')) { - return new ResourceResponse('Blocked.', 400, array()); + if ($this->restFloodControl($this->configFactory->get('user.flood'), 'rest.login_cookie')) { + throw new BadRequestHttpException('Blocked.'); + } + + // Verify that the user is not blocked. + if ($this->userIsBlocked($credentials['name'])) { + throw new BadRequestHttpException('The user has not been activated or is blocked.'); } // Log in the user. @@ -87,8 +140,9 @@ protected function login(array $credentials = array()) { user_login_finalize($user); return new ResourceResponse('You are logged in as ' . $credentials['name'], 200, array()); } - $this->flood->register('rest.login_cookie', \Drupal::config('user.flood')->get('user_window')); - return new ResourceResponse('Sorry, unrecognized username or password.', 400, array()); + + $this->flood->register('rest.login_cookie', $this->configFactory->get('user.flood')->get('user_window')); + throw new BadRequestHttpException('Sorry, unrecognized username or password.'); } /** @@ -101,4 +155,14 @@ protected function logout() { return new ResourceResponse('Logged out!', 200, array()); } + /** + * Verifies if the user is blocked. + * + * @param string $name + * @return bool + */ + protected function userIsBlocked($name) { + return user_is_blocked($name); + } + } diff --git a/core/modules/rest/tests/src/Unit/UserLoginResourceTest.php b/core/modules/rest/tests/src/Unit/UserLoginResourceTest.php new file mode 100644 index 0000000..3bd3786 --- /dev/null +++ b/core/modules/rest/tests/src/Unit/UserLoginResourceTest.php @@ -0,0 +1,165 @@ +getMock('Drupal\user\UserAuthInterface'); + $user_auth_service->expects($this->any()) + ->method('authenticate') + ->will($this->returnValue(FALSE)); + + $container = new ContainerBuilder(); + $container->set('user.auth', $user_auth_service); + \Drupal::setContainer($container); + + $this->flood = $this->getMock('\Drupal\Core\Flood\FloodInterface'); + + $this->config = $this->getMockBuilder('\Drupal\Core\Config\ConfigFactory') + ->disableOriginalConstructor() + ->setMethods(['get']) + ->getMock(); + + $immutableConfig = $this->getMockBuilder('\Drupal\Core\Config\ConfigFactory') + ->disableOriginalConstructor() + ->setMethods(['get']) + ->getMock(); + + $this->config->expects($this->any()) + ->method('get') + ->will($this->returnValue($immutableConfig)); + + $this->logger = $this->getMock('Psr\Log\LoggerInterface'); + + $this->testClass = new UserLoginResource([], 'plugin_id', '', [], $this->logger, $this->config, $this->flood); + + $this->testClassMock = $this->getMockBuilder('\Drupal\rest\Plugin\rest\resource\UserLoginResource') + ->setMethods(['restFloodControl', 'login', 'logout', 'post', 'userIsBlocked']) + ->setConstructorArgs([[], 'plugin_id', '', [], $this->logger, $this->config, $this->flood]) + ->getMock(); + + $this->reflection = new \ReflectionClass($this->testClass); + } + + /** + * Gets a protected method from current class using reflection. + * + * @param $method + * @return mixed + */ + public function getProtectedMethod($method) { + $method = $this->reflection->getMethod($method); + $method->setAccessible(TRUE); + + return $method; + } + + /** + * @expectedException \Symfony\Component\HttpKernel\Exception\BadRequestHttpException + * @expectedExceptionMessage Missing credentials. + */ + public function testMissingCredentials() { + $this->testClass->post(['op'=>'login']); + } + + /** + * @expectedException \Symfony\Component\HttpKernel\Exception\BadRequestHttpException + * @expectedExceptionMessage Unsupported op. + */ + public function testUnsupportedOp() { + $this->testClass->post(['op'=>'UnsuportedOp']); + } + + /** + * @expectedException \Symfony\Component\HttpKernel\Exception\BadRequestHttpException + * @expectedExceptionMessage Missing credentials.name. + */ + public function testLoginMissingCredentialName() { + $method = $this->getProtectedMethod('login'); + $method->invokeArgs($this->testClass, ['credentials' => []]); + } + + /** + * @expectedException \Symfony\Component\HttpKernel\Exception\BadRequestHttpException + * @expectedExceptionMessage Missing credentials.pass. + */ + public function testLoginMissingCredentialPass() { + $method = $this->getProtectedMethod('login'); + $method->invokeArgs($this->testClass, ['credentials' => ['name' => 'Druplicon']]); + } + + /** + * @expectedException \Symfony\Component\HttpKernel\Exception\BadRequestHttpException + * @expectedExceptionMessage Blocked. + */ + public function testLoginBlockedUserByFloodControl() { + $this->testClassMock->expects($this->once()) + ->method('restFloodControl') + ->will($this->returnValue(TRUE)); + + $method = $this->getProtectedMethod('login'); + $method->invokeArgs($this->testClassMock, ['credentials' => ['name' => 'Druplicon', 'pass' => 'SuperSecret']]); + } + + /** + * @expectedException \Symfony\Component\HttpKernel\Exception\BadRequestHttpException + * @expectedExceptionMessage The user has not been activated or is blocked. + */ + public function testLoginBlockedUser() { + $this->testClassMock->expects($this->once()) + ->method('restFloodControl') + ->will($this->returnValue(FALSE)); + + $this->testClassMock->expects($this->once()) + ->method('userIsBlocked') + ->will($this->returnValue(TRUE)); + + $method = $this->getProtectedMethod('login'); + $method->invokeArgs($this->testClassMock, ['credentials' => ['name' => 'Druplicon', 'pass' => 'SuperSecret']]); + } + + /** + * @expectedException \Symfony\Component\HttpKernel\Exception\BadRequestHttpException + * @expectedExceptionMessage Sorry, unrecognized username or password. + */ + public function testLoginUnrecognizedUsernameOrPassword() { + $this->testClassMock->expects($this->once()) + ->method('restFloodControl') + ->will($this->returnValue(FALSE)); + + $this->testClassMock->expects($this->once()) + ->method('userIsBlocked') + ->will($this->returnValue(FALSE)); + + $method = $this->getProtectedMethod('login'); + $method->invokeArgs($this->testClassMock, ['credentials' => ['name' => 'Druplicon', 'pass' => 'SuperSecret']]); + } +} +