diff --git a/core/modules/rest/src/Plugin/rest/resource/UserLoginResource.php b/core/modules/rest/src/Plugin/rest/resource/UserLoginResource.php index 842d878..aebbb32 100644 --- a/core/modules/rest/src/Plugin/rest/resource/UserLoginResource.php +++ b/core/modules/rest/src/Plugin/rest/resource/UserLoginResource.php @@ -13,6 +13,7 @@ use Drupal\rest\ResourceResponse; use Drupal\rest\Plugin\ResourceBase; use Drupal\user\Entity\User; +use Drupal\user\UserStorageInterface; use Psr\Log\LoggerInterface; use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; @@ -40,6 +41,20 @@ class UserLoginResource extends ResourceBase { protected $flood; /** + * The user storage. + * + * @var \Drupal\user\UserStorageInterface + */ + protected $userStorage; + + /** + * A logger instance. + * + * @var \Drupal\Core\Logger\LoggerChannelInterface + */ + protected $logger; + + /** * Constructs a new RestPermissions instance. * * @param array $configuration @@ -50,17 +65,22 @@ class UserLoginResource extends ResourceBase { * The plugin implementation definition. * @param array $serializer_formats * The available serialization formats. - * @param LoggerInterface $loggery + * @param LoggerInterface $logger * A logger instance. * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory * The config factory. * @param \Drupal\Core\Flood\FloodInterface $flood * The flood control mechanism. + * @param \Drupal\user\UserStorageInterface $user_storage + * The user storage. + * */ - public function __construct(array $configuration, $plugin_id, $plugin_definition, array $serializer_formats, LoggerInterface $logger, ConfigFactoryInterface $config_factory, FloodInterface $flood) { + public function __construct(array $configuration, $plugin_id, $plugin_definition, array $serializer_formats, LoggerInterface $logger, ConfigFactoryInterface $config_factory, FloodInterface $flood, UserStorageInterface $user_storage) { parent::__construct($configuration, $plugin_id, $plugin_definition, $serializer_formats, $logger, $flood); $this->configFactory = $config_factory; $this->flood = $flood; + $this->userStorage = $user_storage; + $this->logger = $logger; } /** @@ -74,7 +94,8 @@ public static function create(ContainerInterface $container, array $configuratio $container->getParameter('serializer.formats'), $container->get('logger.factory')->get('rest'), $container->get('config.factory'), - $container->get('flood') + $container->get('flood'), + $container->get('entity.manager')->getStorage('user') ); } @@ -112,6 +133,9 @@ public function post(array $operation = array()) { case 'logout': return $this->logout(); + case 'password_reset': + return $this->requestNewPassword($operation['reset_info']); + default: // TODO: do we have to escape? throw new BadRequestHttpException('Unsupported op '. Html::escape($operation['op']) . '.'); @@ -196,6 +220,47 @@ protected function userIsBlocked($name) { } /** + * Sends the replacement login information by email. + * + * @param array $reset_info + * + * @return \Drupal\rest\ResourceResponse + * The HTTP response object + */ + protected function requestNewPassword(array $reset_info) { + $name = $reset_info[0]['name']; + // Verify that the email or username is filled. + if (!$name) { + throw new BadRequestHttpException('Missing Email or Username.'); + } + + $name = trim($name); + + // Try to load by email. + $accounts = $this->userStorage->loadByProperties(['mail' => $name, 'status' => '1']); + $account = reset($accounts); + + if (!$account instanceof User) { + // No success, try to load by name. + $accounts = $this->userStorage->loadByProperties(['name' => $name, 'status' => '1']); + $account = reset($accounts); + } + + if (!$account instanceof User) { + // No success, the user does not exist. + throw new BadRequestHttpException("Sorry, $name is not recognized as a user name or an e-mail address."); + } + + // @TODO get the current language. + $mail = _user_mail_notify('password_reset', $account, $reset_info[0]['lang']); + if (!empty($mail)) { + $this->logger->notice('Password reset instructions mailed to %name at %email.', ['%name' => $account->getUsername(), '%email' => $account->getEmail()]); + return new ResourceResponse('Further instructions have been sent to your email address.', 200, []); + } + throw new BadRequestHttpException('The email with the instructions was not sent as expected.'); + } + + /** * Checks for flooding. * * @param \Drupal\Core\Config\ImmutableConfig $config diff --git a/core/modules/rest/src/Tests/NodeTest.php b/core/modules/rest/src/Tests/NodeTest.php index 4948900..b55cd0b 100644 --- a/core/modules/rest/src/Tests/NodeTest.php +++ b/core/modules/rest/src/Tests/NodeTest.php @@ -180,7 +180,7 @@ public function testInvalidBundle() { // Make sure the response is "Bad Request". $this->assertResponse(400); - $this->assertResponseBody('{"error":"\"bad_bundle_name\" is not a valid bundle type for denormalization."}'); + $this->assertResponseBody(NULL, '{"error":"\"bad_bundle_name\" is not a valid bundle type for denormalization."}'); } /** @@ -197,6 +197,6 @@ public function testMissingBundle() { // Make sure the response is "Bad Request". $this->assertResponse(400); - $this->assertResponseBody('{"error":"A string must be provided as a bundle value."}'); + $this->assertResponseBody(NULL, '{"error":"A string must be provided as a bundle value."}'); } } diff --git a/core/modules/rest/src/Tests/RESTTestBase.php b/core/modules/rest/src/Tests/RESTTestBase.php index 272213c..2abe6ad 100644 --- a/core/modules/rest/src/Tests/RESTTestBase.php +++ b/core/modules/rest/src/Tests/RESTTestBase.php @@ -403,6 +403,10 @@ protected function removeNodeFieldsForNonAdminUsers(NodeInterface $node) { * Check to see if the HTTP request response body is identical to the expected * value. * + * + * @param $code + * (optional) Response code. For example 200 is a successful page request. For a list + * of all codes see http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html. * @param $expected * The first value to check. * @param $message @@ -419,7 +423,11 @@ protected function removeNodeFieldsForNonAdminUsers(NodeInterface $node) { * @return bool * TRUE if the assertion succeeded, FALSE otherwise. */ - protected function assertResponseBody($expected, $message = '', $group = 'REST Response') { + protected function assertResponseBody($code = NULL, $expected, $message = '', $group = 'REST Response') { + if ($code) { + $this->assertResponse($code); + } return $this->assertIdentical($expected, $this->responseBody, $message ? $message : strtr('Response body @expected (expected) is equal to @response (actual).', array('@expected' => var_export($expected, TRUE), '@response' => var_export($this->responseBody, TRUE))), $group); } + } diff --git a/core/modules/rest/src/Tests/UserLoginTest.php b/core/modules/rest/src/Tests/UserLoginTest.php index 443d151..ffe00a3 100644 --- a/core/modules/rest/src/Tests/UserLoginTest.php +++ b/core/modules/rest/src/Tests/UserLoginTest.php @@ -39,58 +39,58 @@ public function testLogin() { $name = $account->getUsername(); $pass = $account->pass_raw; - // Add registration permission to anonymous user. + // Add login permission to anonymous user. Role::load(RoleInterface::ANONYMOUS_ID) ->grantPermission('restful post user_login') ->save(); $payload = array(); $this->httpRequest('user_login', 'POST', json_encode($payload), 'application/json'); - $this->assertResponse('400', 'No op found. Use: status, login, logout.'); + $this->assertResponseBody('400', '{"error":"No op found. Use: status, login, logout."}'); $payload = $this->getPayload('garbage'); $this->httpRequest('user_login', 'POST', json_encode($payload), 'application/json'); - $this->assertResponse('400', 'Unsupported op garbage.'); + $this->assertResponseBody('400', '{"error":"Unsupported op garbage."}'); $payload = $this->getPayload('status'); $this->httpRequest('user_login', 'POST', json_encode($payload), 'application/json'); - $this->assertResponse('200', 'You are logged in.'); + $this->assertResponseBody('200', '"You are not logged in."'); $payload = $this->getPayload('login'); $this->httpRequest('user_login', 'POST', json_encode($payload), 'application/json'); - $this->assertResponse('400', 'Missing credentials.'); + $this->assertResponseBody('400', '{"error":"Missing credentials."}'); $payload = $this->getPayload('login', $name); $this->httpRequest('user_login', 'POST', json_encode($payload), 'application/json'); - $this->assertResponse('400', 'Missing credentials.pass.'); + $this->assertResponseBody('400', '{"error":"Missing credentials.pass."}'); $payload = $this->getPayload('login', NULL, $pass); $this->httpRequest('user_login', 'POST', json_encode($payload), 'application/json'); - $this->assertResponse('400', 'Missing credentials.name.'); + $this->assertResponseBody('400', '{"error":"Missing credentials.name."}'); $payload = $this->getPayload('login', $name, 'garbage'); $this->httpRequest('user_login', 'POST', json_encode($payload), 'application/json'); - $this->assertResponse('400', 'Sorry, unrecognized username or password.'); + $this->assertResponseBody('400', '{"error":"Sorry, unrecognized username or password."}'); $payload = $this->getPayload('login', 'garbage', $pass); $this->httpRequest('user_login', 'POST', json_encode($payload), 'application/json'); - $this->assertResponse('400', 'Sorry, unrecognized username or password.'); + $this->assertResponseBody('400', '{"error":"Sorry, unrecognized username or password."}'); $payload = $this->getPayload('login', $name, $pass); $this->httpRequest('user_login', 'POST', json_encode($payload), 'application/json'); - $this->assertResponse('200', "You are logged in as $name."); + $this->assertResponseBody('200', '"You are logged in as ' . $name . '."'); $payload = $this->getPayload('status'); $this->httpRequest('user_login', 'POST', json_encode($payload), 'application/json'); - $this->assertResponse('200', 'You are logged in.'); + $this->assertResponseBody('200', '"You are logged in."'); $payload = $this->getPayload('logout'); $this->httpRequest('user_login', 'POST', json_encode($payload), 'application/json'); - $this->assertResponse('200', 'You are logged out.'); + $this->assertResponseBody('200', '"You are logged out."'); $payload = $this->getPayload('status'); $this->httpRequest('user_login', 'POST', json_encode($payload), 'application/json'); - $this->assertResponse('200', 'You are logged out.'); + $this->assertResponseBody('200', '"You are not logged in."'); } diff --git a/core/modules/rest/tests/src/Unit/UserLoginResourceTest.php b/core/modules/rest/tests/src/Unit/UserLoginResourceTest.php index fdd4113..b3cce3f 100644 --- a/core/modules/rest/tests/src/Unit/UserLoginResourceTest.php +++ b/core/modules/rest/tests/src/Unit/UserLoginResourceTest.php @@ -24,6 +24,7 @@ class UserLoginResourceTest extends UnitTestCase { protected $reflection; protected $config; protected $testClassMock; + protected $userStorage; /** * {@inheritdoc} @@ -42,6 +43,10 @@ protected function setUp() { $this->flood = $this->getMock('\Drupal\Core\Flood\FloodInterface'); + $this->userStorage = $this->getMockBuilder('\Drupal\user\UserStorage') + ->disableOriginalConstructor() + ->getMock(); + $this->config = $this->getMockBuilder('\Drupal\Core\Config\ConfigFactory') ->disableOriginalConstructor() ->setMethods(['get']) @@ -58,11 +63,11 @@ protected function setUp() { $this->logger = $this->getMock('Psr\Log\LoggerInterface'); - $this->testClass = new UserLoginResource([], 'plugin_id', '', [], $this->logger, $this->config, $this->flood); + $this->testClass = new UserLoginResource([], 'plugin_id', '', [], $this->logger, $this->config, $this->flood, $this->userStorage); $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]) + ->setConstructorArgs([[], 'plugin_id', '', [], $this->logger, $this->config, $this->flood, $this->userStorage]) ->getMock(); $this->reflection = new \ReflectionClass($this->testClass); @@ -169,5 +174,27 @@ public function testLoginUnrecognizedUsernameOrPassword() { $method = $this->getProtectedMethod('login'); $method->invokeArgs($this->testClassMock, ['credentials' => ['name' => 'Druplicon', 'pass' => 'SuperSecret']]); } + + /** + * @expectedException \Symfony\Component\HttpKernel\Exception\BadRequestHttpException + * @expectedExceptionMessage Missing Email or Username. + */ + public function testResetPasswordMissingNameOrEmail() { + $method = $this->getProtectedMethod('requestNewPassword'); + $method->invokeArgs($this->testClass, [['name' => '']]); + } + + /** + * @expectedException \Symfony\Component\HttpKernel\Exception\BadRequestHttpException + * @expectedExceptionMessage Sorry, Druplicon is not recognized as a user name or an e-mail address. + */ + public function testResetPasswordNotRecognizedNameOrEmail() { + $this->userStorage->expects($this->any()) + ->method('loadByProperties') + ->will($this->returnValue([])); + + $method = $this->getProtectedMethod('requestNewPassword'); + $method->invokeArgs($this->testClass, [['name' => 'Druplicon']]); + } }