diff --git a/core/modules/rest/src/Plugin/rest/resource/UserRegistrationResource.php b/core/modules/rest/src/Plugin/rest/resource/UserRegistrationResource.php new file mode 100644 index 0000000..9836a5e --- /dev/null +++ b/core/modules/rest/src/Plugin/rest/resource/UserRegistrationResource.php @@ -0,0 +1,200 @@ +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') + ); + } + + /** + * Responds to user registration POST request and saves new user account + * entities. + * + * @param \Drupal\user\Entity\User $account + * The user account entity. + * + * @return \Drupal\rest\ResourceResponse + * The HTTP response object. + * + * @throws \Symfony\Component\HttpKernel\Exception\HttpException + */ + public function post(User $account = NULL) { + if ($account == NULL) { + throw new BadRequestHttpException('No user account data for registration received.'); + } + + // POSTed user accounts must not have an ID set, because we always + // want to create new entities here. + if (!$account->isNew()) { + throw new BadRequestHttpException('Only new user accounts can be registered.'); + } + + $approvalSettings = $this->configFactory->get('user.settings')->get('register'); + // Verify that the current user can register a user account. + if ($approvalSettings == USER_REGISTER_ADMINISTRATORS_ONLY) { + throw new AccessDeniedHttpException('Only administrators can register users.'); + } + // If current user can register accounts then let's block the new registered user if admin approval is needed. + elseif ($approvalSettings == USER_REGISTER_VISITORS_ADMINISTRATIVE_APPROVAL) { + $account->block(); + } + + // Make sure that the user entity is valid (email and name are valid). + $this->validate($account); + + // Client cannot submit additional user roles. + $roles = $account->getRoles(); + foreach ($roles as $role) { + if ($role != 'authenticated' && $role != 'anonymous') { + throw new BadRequestHttpException(SafeMarkup::format('Anonymous user cannot assign roles when registering a new user account and by default' . + ' authenticated is added, so you cannot assign @role role.', array('@role' => $role))); + } + } + + $account->save(); + // Send emails from a render context to add bubbleable_metadata to the response. + $context = new RenderContext(); + $renderer = \Drupal::service('renderer'); + $renderer->executeInRenderContext($context, function() use ($account) { + $register = $this->configFactory->get('user.settings')->get('register'); + // No email verification is required. Activating the user. + if ($register == 'visitors') { + if (!$this->configFactory->get('user.settings')->get('verify_mail')) { + // Notification will be sent if activated. + $account->status = 1; + $account->save(); + } + // No administrator approval required. + else { + _user_mail_notify('register_no_approval_required', $account); + } + } + // Administrator approval required. + elseif ($register == 'visitors_admin_approval') { + _user_mail_notify('register_pending_approval', $account); + } + }); + + $response = new ResourceResponse(NULL, 201); + if (!$context->isEmpty()) { + $response->addCacheableDependency($context->pop()); + } + + return $response; + } + + /** + * {@inheritdoc} + */ + public function routes() { + $collection = new RouteCollection(); + + $route_name = strtr($this->pluginId, ':', '.'); + $route = $this->getBaseRoute('/entity/user/register', 'POST'); + $route->setPath('/entity/user/register'); + // Restrict the incoming HTTP Content-type header to the known + // serialization formats. + $route->addRequirements(array('_content_type_format' => implode('|', $this->serializerFormats))); + $collection->add("$route_name.POST", $route); + + return $collection; + } + + /** + * Verifies that the whole entity does not violate any validation constraints. + * + * @param \Drupal\Core\Entity\ContentEntityBase $entity + * The entity object. + * + * @throws \Symfony\Component\HttpKernel\Exception\HttpException + * If validation errors are found. + */ + protected function validate(ContentEntityBase $entity) { + $violations = $entity->validate(); + + // Remove violations of inaccessible fields as they cannot stem from our + // changes. + $violations->filterByFieldAccess(); + + if (count($violations) > 0) { + $message = "Unprocessable Entity: validation failed.\n"; + foreach ($violations as $violation) { + $message .= $violation->getPropertyPath() . ': ' . $violation->getMessage() . "\n"; + } + // Instead of returning a generic 400 response we use the more specific + // 422 Unprocessable Entity code from RFC 4918. That way clients can + // distinguish between general syntax errors in bad serializations (code + // 400) and semantic errors in well-formed requests (code 422). + throw new HttpException(422, $message); + } + } + +} diff --git a/core/modules/rest/src/RequestHandler.php b/core/modules/rest/src/RequestHandler.php index ee4b890..51b6aac 100644 --- a/core/modules/rest/src/RequestHandler.php +++ b/core/modules/rest/src/RequestHandler.php @@ -61,8 +61,13 @@ public function handle(RouteMatchInterface $route_match, Request $request) { if (empty($method_settings['supported_formats']) || in_array($format, $method_settings['supported_formats'])) { $definition = $resource->getPluginDefinition(); $class = $definition['serialization_class']; + $context = array(); + + // Always add the resource ID to the deserialization context. + $context['resource_id'] = $plugin; + $context['request_method'] = $method; try { - $unserialized = $serializer->deserialize($received, $class, $format, array('request_method' => $method)); + $unserialized = $serializer->deserialize($received, $class, $format, $context); } catch (UnexpectedValueException $e) { $error['error'] = $e->getMessage(); diff --git a/core/modules/rest/tests/src/Unit/UserRegistrationResourceTest.php b/core/modules/rest/tests/src/Unit/UserRegistrationResourceTest.php new file mode 100644 index 0000000..2c1d2fc --- /dev/null +++ b/core/modules/rest/tests/src/Unit/UserRegistrationResourceTest.php @@ -0,0 +1,212 @@ +configStub = $this->getConfigFactoryStub(array( + 'user.settings' => array( + 'register' => USER_REGISTER_VISITORS, + 'verify_mail' => FALSE + ), + )); + $this->logger = $this->getMock('Psr\Log\LoggerInterface'); + $this->testClass = new UserRegistrationResource([], 'plugin_id', '', [], $this->logger, $this->configStub); + $this->reflection = new \ReflectionClass($this->testClass); + } + + public function getProtectedMethod($method) { + $method = $this->reflection->getMethod($method); + $method->setAccessible(TRUE); + + return $method; + } + + public function testValidate() { + + $violations = $this->getMockBuilder('Drupal\Core\Entity\EntityConstraintViolationList') + ->setMethods(['filterByFieldAccess']) + ->disableOriginalConstructor() + ->getMock(); + + $violations->expects($this->once()) + ->method('filterByFieldAccess') + ->will($this->returnValue(array())); + + $entity = $this->getMockBuilder('Drupal\user\Entity\User') + ->disableOriginalConstructor() + ->getMock(); + $entity->expects($this->once()) + ->method('validate') + ->will($this->returnValue($violations)); + + $method = $this->getProtectedMethod('validate'); + // No exception is thrown. + $method->invokeArgs($this->testClass, array($entity)); + } + + /** + * @expectedException \Symfony\Component\HttpKernel\Exception\HttpException + * @expectedException UserRegistrationResourceTest::ERROR_MESSAGE + */ + public function testFailedValidate() { + $violation1 = $this->getMock('Symfony\Component\Validator\ConstraintViolationInterface'); + $violation1->expects($this->once()) + ->method('getPropertyPath') + ->will($this->returnValue('property_path')); + $violation1->expects($this->once()) + ->method('getMessage') + ->will($this->returnValue('message')); + $violation2 = $this->getMock('Symfony\Component\Validator\ConstraintViolationInterface'); + $violation2->expects($this->once()) + ->method('getPropertyPath') + ->will($this->returnValue('property_path_2')); + $violation2->expects($this->once()) + ->method('getMessage') + ->will($this->returnValue('message_2')); + $entity = $this->getMockBuilder('Drupal\user\Entity\User') + ->disableOriginalConstructor() + ->getMock(); + + $violations = $this->getMockBuilder('Drupal\Core\Entity\EntityConstraintViolationList') + ->setConstructorArgs([$entity, [$violation1, $violation2]]) + ->setMethods(['filterByFieldAccess']) + ->getMock(); + + $violations->expects($this->once()) + ->method('filterByFieldAccess') + ->will($this->returnValue([])); + + $entity->expects($this->once()) + ->method('validate') + ->will($this->returnValue($violations)); + + $method = $this->getProtectedMethod('validate'); + // No exception is thrown. + $method->invoke($this->testClass, $entity); + } + + /** + * @expectedException \Symfony\Component\HttpKernel\Exception\BadRequestHttpException + * @expectedExceptionMessage No user account data for registration received. + */ + public function testEmptyPost() { + $this->testClass->post(NULL); + } + + /** + * @expectedException \Symfony\Component\HttpKernel\Exception\BadRequestHttpException + * @expectedExceptionMessage Only new user accounts can be registered. + */ + public function testExistedEntityPost() { + $entity = $this->getMockBuilder('Drupal\user\Entity\User') + ->disableOriginalConstructor() + ->getMock(); + + $entity->expects($this->once()) + ->method('isNew') + ->will($this->returnValue(FALSE)); + + $this->testClass->post($entity); + } + + /** + * @expectedException \Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException + * @expectedExceptionMessage Only administrators can register users. + */ + public function testRegistrationAdminOnlyPost() { + $this->configStub = $this->getConfigFactoryStub(array( + 'user.settings' => array( + 'register' => USER_REGISTER_ADMINISTRATORS_ONLY, + 'verify_mail' => FALSE + ), + )); + $this->testClass = new UserRegistrationResource([], 'plugin_id', '', [], $this->logger, $this->configStub); + $entity = $this->getMockBuilder('Drupal\user\Entity\User') + ->disableOriginalConstructor() + ->getMock(); + $entity->expects($this->once()) + ->method('isNew') + ->will($this->returnValue(TRUE)); + $this->testClass->post($entity); + } + + /** + * @expectedException \Symfony\Component\HttpKernel\Exception\BadRequestHttpException + */ + public function testInvalidRolesPost() { + $this->configStub = $this->getConfigFactoryStub(array( + 'user.settings' => array( + 'register' => USER_REGISTER_VISITORS_ADMINISTRATIVE_APPROVAL, + 'verify_mail' => FALSE + ), + )); + $this->testClass = new UserRegistrationResource([], 'plugin_id', '', [], $this->logger, $this->configStub); + $entity = $this->getMockBuilder('Drupal\user\Entity\User') + ->disableOriginalConstructor() + ->getMock(); + $entity->expects($this->once()) + ->method('isNew') + ->will($this->returnValue(TRUE)); + $entity->expects($this->once()) + ->method('getRoles') + ->will($this->returnValue(array('administrator'))); + + $violations = $this->getMockBuilder('Drupal\Core\Entity\EntityConstraintViolationList') + ->setMethods(['filterByFieldAccess']) + ->disableOriginalConstructor() + ->getMock(); + + $violations->expects($this->any()) + ->method('filterByFieldAccess') + ->will($this->returnValue(array())); + + $entity->expects($this->once()) + ->method('validate') + ->will($this->returnValue($violations)); + + $this->testClass->post($entity); + } + +} diff --git a/core/modules/user/user.module b/core/modules/user/user.module index 4f8289b..555218e 100644 --- a/core/modules/user/user.module +++ b/core/modules/user/user.module @@ -1209,7 +1209,7 @@ function user_role_revoke_permissions($rid, array $permissions = array()) { * language. * * @return array - * An array containint various information about the message. + * An array containing various information about the message. * See \Drupal\Core\Mail\MailManagerInterface::mail() for details. * * @see user_mail_tokens()