diff --git a/core/modules/rest/src/Plugin/ResourceBase.php b/core/modules/rest/src/Plugin/ResourceBase.php
index 5f8065e..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;
@@ -42,6 +43,11 @@
protected $logger;
/**
+ * @var \Drupal\Core\Config\ImmutableConfig
+ */
+ protected $flood;
+
+ /**
* Constructs a Drupal\rest\Plugin\ResourceBase object.
*
* @param array $configuration
@@ -54,11 +60,14 @@
* 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) {
+ 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;
+ $this->flood = $flood;
}
/**
@@ -70,7 +79,8 @@ public static function create(ContainerInterface $container, array $configuratio
$plugin_id,
$plugin_definition,
$container->getParameter('serializer.formats'),
- $container->get('logger.factory')->get('rest')
+ $container->get('logger.factory')->get('rest'),
+ $container->get('flood')
);
}
@@ -214,4 +224,21 @@ protected function getBaseRoute($canonical_path, $method) {
return $route;
}
+ /**
+ * Checks for flooding.
+ *
+ * @param \Drupal\Core\Config\ImmutableConfig $config
+ * @param $name
+ * @return bool
+ */
+ protected function restFloodControl($config, $name) {
+ $limit = $config->get('user_limit');
+ $interval = $config->get('user_window');
+ 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
new file mode 100644
index 0000000..6faa9c2
--- /dev/null
+++ b/core/modules/rest/src/Plugin/rest/resource/UserLoginResource.php
@@ -0,0 +1,201 @@
+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
+ * array(
+ * 'op' => 'login', 'logout'
+ * 'credentials' => array(
+ * 'name' => 'your-name',
+ * 'pass' => 'your-password',
+ * ),
+ * )
+ *
+ * The operation and username + pass for the login op.
+ *
+ * @return \Drupal\rest\ResourceResponse
+ * The HTTP response object.
+ */
+ public function post(array $operation = array()) {
+
+ if (array_key_exists('op', $operation)) {
+ switch ($operation['op']) {
+
+ case 'login':
+ if (!array_key_exists('credentials', $operation)) {
+ $operation['credentials'] = array();
+ }
+ return $this->login($operation['credentials']);
+
+ case 'status':
+ return $this->status();
+
+ case 'logout':
+ return $this->logout();
+
+ default:
+ // TODO: do we have to escape?
+ throw new BadRequestHttpException('Unsupported op '. Html::escape($operation['op']) . '.');
+
+ }
+ }
+ else {
+ throw new BadRequestHttpException('No op found. Use: status, login, logout.');
+ }
+ }
+
+ /**
+ * User login.
+ *
+ * @param array $credentials
+ * The username and pass for the user.
+ *
+ * @return \Drupal\rest\ResourceResponse
+ * The HTTP response object
+ */
+ protected function login(array $credentials = array()) {
+// if ($this->userIsAuthenticated()) {
+// throw new BadRequestHttpException('You need to logout first.');
+// }
+
+ if (empty($credentials)) {
+ throw new BadRequestHttpException('Missing credentials.');
+ }
+
+ // Verify that the username is filled.
+ if (!array_key_exists('name', $credentials)) {
+ throw new BadRequestHttpException('Missing credentials.name.');
+ }
+ // Verify that the password is filled.
+ if (!array_key_exists('pass', $credentials)) {
+ throw new BadRequestHttpException('Missing credentials.pass.');
+ }
+
+ // Flood control.
+ 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.
+ if ($uid = \Drupal::service('user.auth')->authenticate($credentials['name'], $credentials['pass'])) {
+ $user = User::load($uid);
+ user_login_finalize($user);
+ return new ResourceResponse('You are logged in as ' . $credentials['name'] . '.', 200, array());
+ }
+
+ $this->flood->register('rest.login_cookie', $this->configFactory->get('user.flood')->get('user_window'));
+ throw new BadRequestHttpException('Sorry, unrecognized username or password.');
+ }
+
+ protected function userIsAuthenticated() {
+ return \Drupal::currentUser()->isAuthenticated();
+
+ }
+ protected function status() {
+// if (\Drupal::currentUser()->isAuthenticated()) {
+// return new ResourceResponse('You are logged in.', 200, array());
+// }
+ return new ResourceResponse('You are not logged in.', 200, array());
+ }
+
+ /**
+ * User Logout.
+ *
+ * @return ResourceResponse
+ */
+ protected function logout() {
+ if (!\Drupal::currentUser()->isAuthenticated()) {
+ throw new BadRequestHttpException('You cannot logout as you are not logged in.');
+ }
+
+ user_logout();
+ return new ResourceResponse('You are 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/src/RequestHandler.php b/core/modules/rest/src/RequestHandler.php
index ee4b890..801e0cd 100644
--- a/core/modules/rest/src/RequestHandler.php
+++ b/core/modules/rest/src/RequestHandler.php
@@ -60,9 +60,15 @@ public function handle(RouteMatchInterface $route_match, Request $request) {
$method_settings = $config[$plugin][$request->getMethod()];
if (empty($method_settings['supported_formats']) || in_array($format, $method_settings['supported_formats'])) {
$definition = $resource->getPluginDefinition();
- $class = $definition['serialization_class'];
+ $class = isset($definition['serialization_class']) ? $definition['serialization_class'] : NULL;
try {
- $unserialized = $serializer->deserialize($received, $class, $format, array('request_method' => $method));
+ if ($class) {
+ $unserialized = $serializer->deserialize($received, $class, $format, array('request_method' => $method));
+ }
+ // Avoid denormalization because we need to instantiate a class.
+ else {
+ $unserialized = $serializer->decode($received, $format, array('request_method' => $method));
+ }
}
catch (UnexpectedValueException $e) {
$error['error'] = $e->getMessage();
diff --git a/core/modules/rest/src/Tests/RESTTestBase.php b/core/modules/rest/src/Tests/RESTTestBase.php
index 5e84827..a2da3a7 100644
--- a/core/modules/rest/src/Tests/RESTTestBase.php
+++ b/core/modules/rest/src/Tests/RESTTestBase.php
@@ -83,7 +83,7 @@ protected function setUp() {
* @return string
* The content returned from the request.
*/
- protected function httpRequest($url, $method, $body = NULL, $mime_type = NULL) {
+ protected function httpRequest($url, $method, $body = NULL, $mime_type = NULL, $request_headers = []) {
if (!isset($mime_type)) {
$mime_type = $this->defaultMimeType;
}
@@ -113,10 +113,11 @@ protected function httpRequest($url, $method, $body = NULL, $mime_type = NULL) {
CURLOPT_POSTFIELDS => $body,
CURLOPT_URL => $url,
CURLOPT_NOBODY => FALSE,
- CURLOPT_HTTPHEADER => array(
+ CURLOPT_HTTPHEADER => array_merge(
+ array(
'Content-Type: ' . $mime_type,
'X-CSRF-Token: ' . $token,
- ),
+ ), $request_headers),
);
break;
@@ -168,6 +169,9 @@ protected function httpRequest($url, $method, $body = NULL, $mime_type = NULL) {
$this->verbose($method . ' request to: ' . $url .
'
Code: ' . curl_getinfo($this->curlHandle, CURLINFO_HTTP_CODE) .
+ '
Request headers: ' . nl2br(print_r($curl_options[CURLOPT_HTTPHEADER], TRUE)) .
+ '
Extra headers: ' . nl2br(print_r($request_headers, TRUE)) .
+ '
Request body: ' . nl2br(print_r($body, TRUE)) .
'
Response headers: ' . nl2br(print_r($headers, TRUE)) .
'
Response body: ' . $this->responseBody);
diff --git a/core/modules/rest/src/Tests/UserTest.php b/core/modules/rest/src/Tests/UserTest.php
new file mode 100644
index 0000000..4c2d48a
--- /dev/null
+++ b/core/modules/rest/src/Tests/UserTest.php
@@ -0,0 +1,116 @@
+defaultAuth = array('basic_auth');
+
+ $this->enableService('user_login', 'POST');
+
+ $permissions[] = 'restful post user_login';
+ $account = $this->drupalCreateUser($permissions);
+
+ $name = $account->getUsername();
+ $pass = $account->pass_raw;
+
+ $basic_auth = ['Authorization: Basic ' . base64_encode("$name:$pass")];
+
+ $payload = array();
+ $this->httpRequest('user_login', 'POST', json_encode($payload), $this->defaultMimeType, $basic_auth);
+ $this->assertResponseAndText(400, 'No op found. Use: status, login, logout.');
+
+ $payload = $this->getPayload('garbage');
+ $this->httpRequest('user_login', 'POST', json_encode($payload), $this->defaultMimeType, $basic_auth);
+ $this->assertResponseAndText(400, 'Unsupported op garbage.');
+
+ $payload = $this->getPayload('status');
+ $this->httpRequest('user_login', 'POST', json_encode($payload), $this->defaultMimeType, $basic_auth);
+ $this->assertResponseAndText(200, 'You are logged in.');
+
+ $payload = $this->getPayload('logout');
+ $this->httpRequest('user_login', 'POST', json_encode($payload), $this->defaultMimeType, $basic_auth);
+ $this->assertResponseAndText(200, 'You are logged out.', $basic_auth);
+
+ $payload = $this->getPayload('login');
+ $this->httpRequest('user_login', 'POST', json_encode($payload), $this->defaultMimeType, $basic_auth);
+ $this->assertResponseAndText(400, 'Missing credentials.');
+
+ $payload = $this->getPayload('login', $name);
+ $this->httpRequest('user_login', 'POST', json_encode($payload), $this->defaultMimeType, $basic_auth);
+ $this->assertResponseAndText(400, 'Missing credentials.pass.');
+
+ $payload = $this->getPayload('login', NULL, $pass);
+ $this->httpRequest('user_login', 'POST', json_encode($payload), $this->defaultMimeType, $basic_auth);
+ $this->assertResponseAndText(400, 'Missing credentials.name.');
+
+ $payload = $this->getPayload('login', $name, 'garbage');
+ $this->httpRequest('user_login', 'POST', json_encode($payload), $this->defaultMimeType, $basic_auth);
+ $this->assertResponseAndText(400, 'Sorry, unrecognized username or password.');
+
+ $payload = $this->getPayload('login', 'garbage', $pass);
+ $this->httpRequest('user_login', 'POST', json_encode($payload), $this->defaultMimeType, $basic_auth);
+ $this->assertResponseAndText(400, 'Sorry, unrecognized username or password.');
+
+ $payload = $this->getPayload('login', $name, $pass);
+ $this->httpRequest('user_login', 'POST', json_encode($payload), $this->defaultMimeType, $basic_auth);
+ $this->assertResponseAndText(200, "You are logged in as $name");
+
+ $payload = $this->getPayload('status');
+ $this->httpRequest('user_login', 'POST', json_encode($payload), $this->defaultMimeType, $basic_auth);
+ $this->assertResponseAndText(200, 'You are logged in.');
+ }
+
+ protected function assertResponseAndText($code, $text) {
+ $this->assertResponse($code);
+ $this->assertText($text);
+ }
+
+ /**
+ * Helper function to build the payload.
+ *
+ * @param string $op
+ * @param string|null $user
+ * @param string|null $pass
+ * @return array
+ *
+ * @see UserLoginResource.php
+ */
+ private function getPayload( $op, $name = NULL, $pass = NULL) {
+ $result = array('op' => $op);
+
+ if ($op == 'login') {
+ $result['credentials'] = array();
+ if (isset($name)) {
+ $result['credentials']['name'] = $name;
+ }
+ if (isset($pass)) {
+ $result['credentials']['pass'] = $pass;
+ }
+ }
+ return $result;
+ }
+}
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..fdd4113
--- /dev/null
+++ b/core/modules/rest/tests/src/Unit/UserLoginResourceTest.php
@@ -0,0 +1,173 @@
+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 No op found. Use: status, login, logout.
+ */
+ public function testEmptyPayload() {
+ $this->testClass->post([]);
+ }
+
+ /**
+ * @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 UnsuportedOp.
+ */
+ public function testUnsupportedOp() {
+ $this->testClass->post(['op'=>'UnsuportedOp']);
+ }
+
+ /**
+ * @expectedException \Symfony\Component\HttpKernel\Exception\BadRequestHttpException
+ * @expectedExceptionMessage Missing credentials.
+ */
+ 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']]);
+ }
+}
+