diff --git a/core/modules/hal/src/Normalizer/ContentEntityNormalizer.php b/core/modules/hal/src/Normalizer/ContentEntityNormalizer.php index 6a24454925f..8ae74defe2e 100644 --- a/core/modules/hal/src/Normalizer/ContentEntityNormalizer.php +++ b/core/modules/hal/src/Normalizer/ContentEntityNormalizer.php @@ -63,6 +63,11 @@ class ContentEntityNormalizer extends NormalizerBase { * Implements \Symfony\Component\Serializer\Normalizer\NormalizerInterface::normalize() */ public function normalize($entity, $format = NULL, array $context = array()) { + $context += array( + 'account' => NULL, + 'included_fields' => NULL, + ); + // Create the array of normalized fields, starting with the URI. /** @var $entity \Drupal\Core\Entity\ContentEntityInterface */ $normalized = array( @@ -90,9 +95,12 @@ class ContentEntityNormalizer extends NormalizerBase { // Ignore the entity ID and revision ID. $exclude = array($entity->getEntityType()->getKey('id'), $entity->getEntityType()->getKey('revision')); foreach ($fields as $field) { - if (in_array($field->getFieldDefinition()->getName(), $exclude)) { + // Continue if this is an excluded field or the current user does not have + // access to view it. + if (in_array($field->getFieldDefinition()->getName(), $exclude) || !$field->access('view', $context['account'])) { continue; } + $normalized_property = $this->serializer->normalize($field, $format, $context); $normalized = NestedArray::mergeDeep($normalized, $normalized_property); } diff --git a/core/modules/hal/src/Tests/EntityTest.php b/core/modules/hal/src/Tests/EntityTest.php index 91d917c1ba8..ef20228088b 100644 --- a/core/modules/hal/src/Tests/EntityTest.php +++ b/core/modules/hal/src/Tests/EntityTest.php @@ -92,6 +92,9 @@ class EntityTest extends NormalizerTestBase { $vocabulary = entity_create('taxonomy_vocabulary', array('vid' => 'example_vocabulary')); $vocabulary->save(); + $account = entity_create('user', array('name' => $this->randomMachineName())); + $account->save(); + // @todo Until https://www.drupal.org/node/2327935 is fixed, if no parent is // set, the test fails because target_id => 0 is reserialized to NULL. $term_parent = entity_create('taxonomy_term', array( @@ -113,9 +116,9 @@ class EntityTest extends NormalizerTestBase { $original_values = $term->toArray(); unset($original_values['tid']); - $normalized = $this->serializer->normalize($term, $this->format); + $normalized = $this->serializer->normalize($term, $this->format, ['account' => $account]); - $denormalized_term = $this->serializer->denormalize($normalized, 'Drupal\taxonomy\Entity\Term', $this->format); + $denormalized_term = $this->serializer->denormalize($normalized, 'Drupal\taxonomy\Entity\Term', $this->format, ['account' => $account]); // Verify that the ID and revision ID were skipped by the normalizer. $this->assertEqual(NULL, $denormalized_term->id()); @@ -133,8 +136,8 @@ class EntityTest extends NormalizerTestBase { $node_type = entity_create('node_type', array('type' => 'example_type')); $node_type->save(); - $user = entity_create('user', array('name' => $this->randomMachineName())); - $user->save(); + $account = entity_create('user', array('name' => $this->randomMachineName())); + $account->save(); // Add comment type. $this->container->get('entity.manager')->getStorage('comment_type')->create(array( @@ -147,7 +150,7 @@ class EntityTest extends NormalizerTestBase { $node = entity_create('node', array( 'title' => $this->randomMachineName(), - 'uid' => $user->id(), + 'uid' => $account->id(), 'type' => $node_type->id(), 'status' => NODE_PUBLISHED, 'promote' => 1, @@ -160,7 +163,7 @@ class EntityTest extends NormalizerTestBase { $node->save(); $parent_comment = entity_create('comment', array( - 'uid' => $user->id(), + 'uid' => $account->id(), 'subject' => $this->randomMachineName(), 'comment_body' => [ 'value' => $this->randomMachineName(), @@ -173,7 +176,7 @@ class EntityTest extends NormalizerTestBase { $parent_comment->save(); $comment = entity_create('comment', array( - 'uid' => $user->id(), + 'uid' => $account->id(), 'subject' => $this->randomMachineName(), 'comment_body' => [ 'value' => $this->randomMachineName(), @@ -189,10 +192,16 @@ class EntityTest extends NormalizerTestBase { $comment->save(); $original_values = $comment->toArray(); - unset($original_values['cid']); + // cid will not exist and hostname will always be denied view access. + unset($original_values['cid'], $original_values['hostname']); - $normalized = $this->serializer->normalize($comment, $this->format); - $denormalized_comment = $this->serializer->denormalize($normalized, 'Drupal\comment\Entity\Comment', $this->format); + $normalized = $this->serializer->normalize($comment, $this->format, ['account' => $account]); + + // Assert that the hostname field does not appear at all in the normalized + // data. + $this->assertFalse(array_key_exists('hostname', $normalized), 'Hostname was not found in normalized comment data.'); + + $denormalized_comment = $this->serializer->denormalize($normalized, 'Drupal\comment\Entity\Comment', $this->format, ['account' => $account]); // Verify that the ID and revision ID were skipped by the normalizer. $this->assertEqual(NULL, $denormalized_comment->id()); diff --git a/core/modules/rest/src/Tests/CreateTest.php b/core/modules/rest/src/Tests/CreateTest.php index affdfb512f4..e547c2c2bdf 100644 --- a/core/modules/rest/src/Tests/CreateTest.php +++ b/core/modules/rest/src/Tests/CreateTest.php @@ -41,7 +41,7 @@ class CreateTest extends RESTTestBase { $entity_values = $this->entityValues($entity_type); $entity = entity_create($entity_type, $entity_values); - $serialized = $serializer->serialize($entity, $this->defaultFormat); + $serialized = $serializer->serialize($entity, $this->defaultFormat, ['account' => $account]); // Create the entity over the REST API. $this->httpRequest('entity/' . $entity_type, 'POST', $serialized, $this->defaultMimeType); $this->assertResponse(201); @@ -68,23 +68,24 @@ class CreateTest extends RESTTestBase { // Try to create an entity with an access protected field. // @see entity_test_entity_field_access() if ($entity_type == 'entity_test') { - $entity->field_test_text->value = 'no access value'; - $serialized = $serializer->serialize($entity, $this->defaultFormat); - $this->httpRequest('entity/' . $entity_type, 'POST', $serialized, $this->defaultMimeType); + $context = ['account' => $account]; + $normalized = $serializer->normalize($entity, $this->defaultFormat, $context); + $normalized['field_test_text'][0]['value'] = 'no access value'; + $this->httpRequest('entity/' . $entity_type, 'POST', $serializer->serialize($normalized, $this->defaultFormat, $context), $this->defaultMimeType); $this->assertResponse(403); $this->assertFalse(entity_load_multiple($entity_type, NULL, TRUE), 'No entity has been created in the database.'); // Try to create a field with a text format this user has no access to. $entity->field_test_text->value = $entity_values['field_test_text'][0]['value']; $entity->field_test_text->format = 'full_html'; - $serialized = $serializer->serialize($entity, $this->defaultFormat); + $serialized = $serializer->serialize($entity, $this->defaultFormat, $context); $this->httpRequest('entity/' . $entity_type, 'POST', $serialized, $this->defaultMimeType); $this->assertResponse(422); $this->assertFalse(entity_load_multiple($entity_type, NULL, TRUE), 'No entity has been created in the database.'); // Restore the valid test value. $entity->field_test_text->format = 'plain_text'; - $serialized = $serializer->serialize($entity, $this->defaultFormat); + $serialized = $serializer->serialize($entity, $this->defaultFormat, $context); } // Try to send invalid data that cannot be correctly deserialized. @@ -98,7 +99,7 @@ class CreateTest extends RESTTestBase { // Try to send invalid data to trigger the entity validation constraints. // Send a UUID that is too long. $entity->set('uuid', $this->randomMachineName(129)); - $invalid_serialized = $serializer->serialize($entity, $this->defaultFormat); + $invalid_serialized = $serializer->serialize($entity, $this->defaultFormat, $context); $response = $this->httpRequest('entity/' . $entity_type, 'POST', $invalid_serialized, $this->defaultMimeType); $this->assertResponse(422); $error = Json::decode($response); diff --git a/core/modules/rest/src/Tests/UpdateTest.php b/core/modules/rest/src/Tests/UpdateTest.php index 57bb0abf6f9..6f588449095 100644 --- a/core/modules/rest/src/Tests/UpdateTest.php +++ b/core/modules/rest/src/Tests/UpdateTest.php @@ -40,6 +40,8 @@ class UpdateTest extends RESTTestBase { $account = $this->drupalCreateUser($permissions); $this->drupalLogin($account); + $context = ['account' => $account]; + // Create an entity and save it to the database. $entity = $this->entityCreate($entity_type); $entity->save(); @@ -52,7 +54,7 @@ class UpdateTest extends RESTTestBase { $patch_entity = entity_create($entity_type, $patch_values); // We don't want to overwrite the UUID. unset($patch_entity->uuid); - $serialized = $serializer->serialize($patch_entity, $this->defaultFormat); + $serialized = $serializer->serialize($patch_entity, $this->defaultFormat, $context); // Update the entity over the REST API. $this->httpRequest($entity->getSystemPath(), 'PATCH', $serialized, $this->defaultMimeType); @@ -64,7 +66,7 @@ class UpdateTest extends RESTTestBase { // Make sure that the field does not get deleted if it is not present in the // PATCH request. - $normalized = $serializer->normalize($patch_entity, $this->defaultFormat); + $normalized = $serializer->normalize($patch_entity, $this->defaultFormat, $context); unset($normalized['field_test_text']); $serialized = $serializer->encode($normalized, $this->defaultFormat); $this->httpRequest($entity->getSystemPath(), 'PATCH', $serialized, $this->defaultMimeType); @@ -100,8 +102,9 @@ class UpdateTest extends RESTTestBase { $this->assertEqual($entity->field_test_text->value, 'no delete access value', 'Text field was not deleted.'); // Try to update an access protected field. - $patch_entity->get('field_test_text')->value = 'no access value'; - $serialized = $serializer->serialize($patch_entity, $this->defaultFormat); + $normalized = $serializer->normalize($patch_entity, $this->defaultFormat, $context); + $normalized['field_test_text'][0]['value'] = 'no access value'; + $serialized = $serializer->serialize($normalized, $this->defaultFormat, $context); $this->httpRequest($entity->getSystemPath(), 'PATCH', $serialized, $this->defaultMimeType); $this->assertResponse(403); @@ -114,7 +117,7 @@ class UpdateTest extends RESTTestBase { 'value' => 'test', 'format' => 'full_html', )); - $serialized = $serializer->serialize($patch_entity, $this->defaultFormat); + $serialized = $serializer->serialize($patch_entity, $this->defaultFormat, $context); $this->httpRequest($entity->getSystemPath(), 'PATCH', $serialized, $this->defaultMimeType); $this->assertResponse(422); @@ -139,7 +142,7 @@ class UpdateTest extends RESTTestBase { // Try to send invalid data to trigger the entity validation constraints. // Send a UUID that is too long. $entity->set('uuid', $this->randomMachineName(129)); - $invalid_serialized = $serializer->serialize($entity, $this->defaultFormat); + $invalid_serialized = $serializer->serialize($entity, $this->defaultFormat, $context); $response = $this->httpRequest($entity->getSystemPath(), 'PATCH', $invalid_serialized, $this->defaultMimeType); $this->assertResponse(422); $error = Json::decode($response); diff --git a/core/modules/serialization/serialization.services.yml b/core/modules/serialization/serialization.services.yml index 389ac0564c6..f32fac0f2aa 100644 --- a/core/modules/serialization/serialization.services.yml +++ b/core/modules/serialization/serialization.services.yml @@ -7,6 +7,11 @@ services: tags: - { name: normalizer } arguments: ['@entity.manager'] + serializer.normalizer.content_entity: + class: Drupal\serialization\Normalizer\ContentEntityNormalizer + tags: + - { name: normalizer } + arguments: ['@entity.manager'] serializer.normalizer.entity: class: Drupal\serialization\Normalizer\EntityNormalizer tags: diff --git a/core/modules/serialization/src/Normalizer/ContentEntityNormalizer.php b/core/modules/serialization/src/Normalizer/ContentEntityNormalizer.php new file mode 100644 index 00000000000..8ed3c918e1a --- /dev/null +++ b/core/modules/serialization/src/Normalizer/ContentEntityNormalizer.php @@ -0,0 +1,40 @@ + NULL, + ); + + $attributes = []; + foreach ($object as $name => $field) { + if ($field->access('view', $context['account'])) { + $attributes[$name] = $this->serializer->normalize($field, $format, $context); + } + } + + return $attributes; + } + +} diff --git a/core/modules/serialization/src/Tests/EntitySerializationTest.php b/core/modules/serialization/src/Tests/EntitySerializationTest.php index 849b271c505..d45c2a630a4 100644 --- a/core/modules/serialization/src/Tests/EntitySerializationTest.php +++ b/core/modules/serialization/src/Tests/EntitySerializationTest.php @@ -9,6 +9,7 @@ namespace Drupal\serialization\Tests; use Drupal\Core\Language\LanguageInterface; use Drupal\Component\Utility\String; +use Drupal\user\Entity\User; /** * Tests that entities can be serialized to supported core formats. @@ -48,6 +49,8 @@ class EntitySerializationTest extends NormalizerTestBase { protected function setUp() { parent::setUp(); + // User create needs sequence table. + $this->installSchema('system', array('sequences')); // Create a test entity to serialize. $this->values = array( 'name' => $this->randomMachineName(), @@ -105,6 +108,17 @@ class EntitySerializationTest extends NormalizerTestBase { $this->assertEqual($expected[$fieldName], $normalized[$fieldName], "ComplexDataNormalizer produces expected array for $fieldName."); } $this->assertEqual(array_diff_key($normalized, $expected), array(), 'No unexpected data is added to the normalized array.'); + + // Test password isn't available. + $account = User::create([ + 'name' => 'foo', + 'mail' => 'foo@example.com', + 'pass' => '123456', + ]); + $account->save(); + $normalized = $this->serializer->normalize($account); + $this->assertTrue(empty($normalized['pass'])); + $this->assertTrue(empty($normalized['mail'])); } /** diff --git a/core/modules/serialization/tests/src/Unit/Normalizer/ContentEntityNormalizerTest.php b/core/modules/serialization/tests/src/Unit/Normalizer/ContentEntityNormalizerTest.php new file mode 100644 index 00000000000..d9ca3e85429 --- /dev/null +++ b/core/modules/serialization/tests/src/Unit/Normalizer/ContentEntityNormalizerTest.php @@ -0,0 +1,155 @@ +entityManager = $this->getMock('Drupal\Core\Entity\EntityManagerInterface'); + $this->contentEntityNormalizer = new ContentEntityNormalizer($this->entityManager); + $this->serializer = $this->getMockBuilder('Symfony\Component\Serializer\Serializer') + ->disableOriginalConstructor() + ->setMethods(array('normalize')) + ->getMock(); + $this->contentEntityNormalizer->setSerializer($this->serializer); + } + + /** + * @covers ::supportsNormalization + */ + public function testSupportsNormalization() { + $content_mock = $this->getMock('Drupal\Core\Entity\ContentEntityInterface'); + $config_mock = $this->getMock('Drupal\Core\Entity\ConfigEntityInterface'); + $this->assertTrue($this->contentEntityNormalizer->supportsNormalization($content_mock)); + $this->assertFalse($this->contentEntityNormalizer->supportsNormalization($config_mock)); + } + + /** + * Tests the normalize() method. + * + * @covers ::normalize + */ + public function testNormalize() { + $this->serializer->expects($this->any()) + ->method('normalize') + ->with($this->containsOnlyInstancesOf('Drupal\Core\Field\FieldItemListInterface'), 'test_format', ['account' => NULL]) + ->will($this->returnValue('test')); + + $definitions = array( + 'field_1' => $this->createMockFieldListItem(), + 'field_2' => $this->createMockFieldListItem(FALSE), + ); + $content_entity_mock = $this->createMockForContentEntity($definitions); + + $normalized = $this->contentEntityNormalizer->normalize($content_entity_mock, 'test_format'); + + $this->assertArrayHasKey('field_1', $normalized); + $this->assertEquals('test', $normalized['field_1']); + $this->assertArrayNotHasKey('field_2', $normalized); + } + + /** + * Tests the normalize() method with account context passed. + * + * @covers ::normalize + */ + public function testNormalizeWithAccountContext() { + $mock_account = $this->getMock('Drupal\Core\Session\AccountInterface'); + + $context = [ + 'account' => $mock_account, + ]; + + $this->serializer->expects($this->any()) + ->method('normalize') + ->with($this->containsOnlyInstancesOf('Drupal\Core\Field\FieldItemListInterface'), 'test_format', $context) + ->will($this->returnValue('test')); + + // The mock account should get passed directly into the access() method on + // field items from $context['account']. + $definitions = array( + 'field_1' => $this->createMockFieldListItem(TRUE, $mock_account), + 'field_2' => $this->createMockFieldListItem(FALSE, $mock_account), + ); + $content_entity_mock = $this->createMockForContentEntity($definitions); + + $normalized = $this->contentEntityNormalizer->normalize($content_entity_mock, 'test_format', $context); + + $this->assertArrayHasKey('field_1', $normalized); + $this->assertEquals('test', $normalized['field_1']); + $this->assertArrayNotHasKey('field_2', $normalized); + } + + /** + * Creates a mock content entity. + * + * @param $definitions + * + * @return \PHPUnit_Framework_MockObject_MockObject + */ + public function createMockForContentEntity($definitions) { + $content_entity_mock = $this->getMockBuilder('Drupal\Core\Entity\ContentEntityBase') + ->disableOriginalConstructor() + ->setMethods(array('getFields')) + ->getMockForAbstractClass(); + $content_entity_mock->expects($this->once()) + ->method('getFields') + ->will($this->returnValue($definitions)); + + return $content_entity_mock; + } + + /** + * Creates a mock field list item. + * + * @param bool $access + * + * @return \Drupal\Core\Field\FieldItemListInterface|\PHPUnit_Framework_MockObject_MockObject + */ + protected function createMockFieldListItem($access = TRUE, $user_context = NULL) { + $mock = $this->getMock('Drupal\Core\Field\FieldItemListInterface'); + $mock->expects($this->once()) + ->method('access') + ->with('view', $user_context) + ->will($this->returnValue($access)); + + return $mock; + } + +}