diff --git a/core/modules/hal/lib/Drupal/hal/Normalizer/EntityNormalizer.php b/core/modules/hal/lib/Drupal/hal/Normalizer/EntityNormalizer.php index 73596c8..c47a72c 100644 --- a/core/modules/hal/lib/Drupal/hal/Normalizer/EntityNormalizer.php +++ b/core/modules/hal/lib/Drupal/hal/Normalizer/EntityNormalizer.php @@ -89,6 +89,15 @@ public function denormalize($data, $class, $format = NULL, array $context = arra $entity = entity_create($typed_data_ids['entity_type'], array('langcode' => $langcode, 'type' => $typed_data_ids['bundle'])); + // Special handling for PATCH: destroy all possible default values that + // might have been set on entity creation. We want an "empty" entity that + // will only get filled with fields from the data array. + if (isset($context['request_method']) && $context['request_method'] == 'patch') { + foreach ($entity as $field_name => $field) { + $entity->set($field_name, NULL); + } + } + // Get links and remove from data array. $links = $data['_links']; unset($data['_links']); diff --git a/core/modules/node/lib/Drupal/node/NodeStorageController.php b/core/modules/node/lib/Drupal/node/NodeStorageController.php index c3ccc3c..5efde56 100644 --- a/core/modules/node/lib/Drupal/node/NodeStorageController.php +++ b/core/modules/node/lib/Drupal/node/NodeStorageController.php @@ -160,7 +160,10 @@ public function baseFieldDefinitions() { 'label' => t('User ID'), 'description' => t('The user ID of the node author.'), 'type' => 'entity_reference_field', - 'settings' => array('target_type' => 'user'), + 'settings' => array( + 'target_type' => 'user', + 'default_value' => 0, + ), ); $properties['status'] = array( 'label' => t('Publishing status'), diff --git a/core/modules/rest/lib/Drupal/rest/Plugin/rest/resource/EntityResource.php b/core/modules/rest/lib/Drupal/rest/Plugin/rest/resource/EntityResource.php index 504fc59..b4b9127 100644 --- a/core/modules/rest/lib/Drupal/rest/Plugin/rest/resource/EntityResource.php +++ b/core/modules/rest/lib/Drupal/rest/Plugin/rest/resource/EntityResource.php @@ -142,24 +142,21 @@ public function patch($id, EntityInterface $entity = NULL) { if (!$original_entity->access('update')) { throw new AccessDeniedHttpException(); } - $info = $original_entity->entityInfo(); - // Make sure that the entity ID is the one provided in the URL. - $entity->{$info['entity_keys']['id']} = $id; // Overwrite the received properties. foreach ($entity as $field_name => $field) { if (isset($entity->{$field_name})) { if (empty($entity->{$field_name})) { - if (!$original_entity->{$field_name}->access('delete')) { + if (!$original_entity->get($field_name)->access('delete')) { throw new AccessDeniedHttpException(t('Access denied on deleting field @field.', array('@field' => $field_name))); } } else { - if (!$original_entity->{$field_name}->access('update')) { + if (!$original_entity->get($field_name)->access('update')) { throw new AccessDeniedHttpException(t('Access denied on updating field @field.', array('@field' => $field_name))); } } - $original_entity->{$field_name} = $field; + $original_entity->set($field_name, $field->getValue()); } } try { diff --git a/core/modules/rest/lib/Drupal/rest/RequestHandler.php b/core/modules/rest/lib/Drupal/rest/RequestHandler.php index c7c1c62..521d09e 100644 --- a/core/modules/rest/lib/Drupal/rest/RequestHandler.php +++ b/core/modules/rest/lib/Drupal/rest/RequestHandler.php @@ -56,7 +56,7 @@ public function handle(Request $request, $id = NULL) { $definition = $resource->getPluginDefinition(); $class = $definition['serialization_class']; try { - $unserialized = $serializer->deserialize($received, $class, $format); + $unserialized = $serializer->deserialize($received, $class, $format, array('request_method' => $method)); } catch (UnexpectedValueException $e) { $error['error'] = $e->getMessage(); diff --git a/core/modules/rest/lib/Drupal/rest/Tests/CreateTest.php b/core/modules/rest/lib/Drupal/rest/Tests/CreateTest.php index 4ccc0b6..64be56d 100644 --- a/core/modules/rest/lib/Drupal/rest/Tests/CreateTest.php +++ b/core/modules/rest/lib/Drupal/rest/Tests/CreateTest.php @@ -34,83 +34,85 @@ public static function getInfo() { */ public function testCreate() { $serializer = drupal_container()->get('serializer'); - // @todo once EntityNG is implemented for other entity types test all other - // entity types here as well. - $entity_type = 'entity_test'; - - $this->enableService('entity:' . $entity_type, 'POST'); - // Create a user account that has the required permissions to create - // resources via the REST API. - $permissions = $this->entityPermissions($entity_type, 'create'); - $permissions[] = 'restful post entity:' . $entity_type; - $account = $this->drupalCreateUser($permissions); - $this->drupalLogin($account); - - $entity_values = $this->entityValues($entity_type); - $entity = entity_create($entity_type, $entity_values); - $serialized = $serializer->serialize($entity, $this->defaultFormat); - // Create the entity over the REST API. - $this->httpRequest('entity/' . $entity_type, 'POST', $serialized, $this->defaultMimeType); - $this->assertResponse(201); - - // Get the new entity ID from the location header and try to read it from - // the database. - $location_url = $this->drupalGetHeader('location'); - $url_parts = explode('/', $location_url); - $id = end($url_parts); - $loaded_entity = entity_load($entity_type, $id); - $this->assertNotIdentical(FALSE, $loaded_entity, 'The new ' . $entity_type . ' was found in the database.'); - $this->assertEqual($entity->uuid(), $loaded_entity->uuid(), 'UUID of created entity is correct.'); - // @todo Remove the user reference field for now until deserialization for - // entity references is implemented. - unset($entity_values['user_id']); - foreach ($entity_values as $property => $value) { - $actual_value = $loaded_entity->get($property)->value; - $send_value = $entity->get($property)->value; - $this->assertEqual($send_value, $actual_value, 'Created property ' . $property . ' expected: ' . $send_value . ', actual: ' . $actual_value); + $entity_types = array('entity_test', 'node'); + foreach ($entity_types as $entity_type) { + + $this->enableService('entity:' . $entity_type, 'POST'); + // Create a user account that has the required permissions to create + // resources via the REST API. + $permissions = $this->entityPermissions($entity_type, 'create'); + $permissions[] = 'restful post entity:' . $entity_type; + $account = $this->drupalCreateUser($permissions); + $this->drupalLogin($account); + + $entity_values = $this->entityValues($entity_type); + $entity = entity_create($entity_type, $entity_values); + $serialized = $serializer->serialize($entity, $this->defaultFormat); + // Create the entity over the REST API. + $this->httpRequest('entity/' . $entity_type, 'POST', $serialized, $this->defaultMimeType); + $this->assertResponse(201); + + // Get the new entity ID from the location header and try to read it from + // the database. + $location_url = $this->drupalGetHeader('location'); + $url_parts = explode('/', $location_url); + $id = end($url_parts); + $loaded_entity = entity_load($entity_type, $id); + $this->assertNotIdentical(FALSE, $loaded_entity, 'The new ' . $entity_type . ' was found in the database.'); + $this->assertEqual($entity->uuid(), $loaded_entity->uuid(), 'UUID of created entity is correct.'); + // @todo Remove the user reference field for now until deserialization for + // entity references is implemented. + unset($entity_values['user_id']); + foreach ($entity_values as $property => $value) { + $actual_value = $loaded_entity->get($property)->value; + $send_value = $entity->get($property)->value; + $this->assertEqual($send_value, $actual_value, 'Created property ' . $property . ' expected: ' . $send_value . ', actual: ' . $actual_value); + } + + $loaded_entity->delete(); + + // 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); + $this->assertResponse(403); + $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->value = $entity_values['field_test_text'][0]['value']; + $serialized = $serializer->serialize($entity, $this->defaultFormat); + } + + // Try to send invalid data that cannot be correctly deserialized. + $this->httpRequest('entity/' . $entity_type, 'POST', 'kaboom!', $this->defaultMimeType); + $this->assertResponse(400); + + // Try to send no data at all, which does not make sense on POST requests. + $this->httpRequest('entity/' . $entity_type, 'POST', NULL, $this->defaultMimeType); + $this->assertResponse(400); + + // Try to create an entity without the CSRF token. + $this->curlExec(array( + CURLOPT_HTTPGET => FALSE, + CURLOPT_POST => TRUE, + CURLOPT_CUSTOMREQUEST => 'POST', + CURLOPT_POSTFIELDS => $serialized, + CURLOPT_URL => url('entity/' . $entity_type, array('absolute' => TRUE)), + CURLOPT_NOBODY => FALSE, + CURLOPT_HTTPHEADER => array('Content-Type: ' . $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 an entity without proper permissions. + $this->drupalLogout(); + $this->httpRequest('entity/' . $entity_type, 'POST', $serialized, $this->defaultMimeType); + $this->assertResponse(403); + $this->assertFalse(entity_load_multiple($entity_type, NULL, TRUE), 'No entity has been created in the database.'); } - $loaded_entity->delete(); - - // Try to create an entity with an access protected field. - // @see entity_test_entity_field_access() - $entity->field_test_text->value = 'no access value'; - $serialized = $serializer->serialize($entity, $this->defaultFormat); - $this->httpRequest('entity/' . $entity_type, 'POST', $serialized, $this->defaultMimeType); - $this->assertResponse(403); - $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->value = $entity_values['field_test_text'][0]['value']; - $serialized = $serializer->serialize($entity, $this->defaultFormat); - - // Try to send invalid data that cannot be correctly deserialized. - $this->httpRequest('entity/' . $entity_type, 'POST', 'kaboom!', $this->defaultMimeType); - $this->assertResponse(400); - - // Try to send no data at all, which does not make sense on POST requests. - $this->httpRequest('entity/' . $entity_type, 'POST', NULL, $this->defaultMimeType); - $this->assertResponse(400); - - // Try to create an entity without the CSRF token. - $this->curlExec(array( - CURLOPT_HTTPGET => FALSE, - CURLOPT_POST => TRUE, - CURLOPT_CUSTOMREQUEST => 'POST', - CURLOPT_POSTFIELDS => $serialized, - CURLOPT_URL => url('entity/' . $entity_type, array('absolute' => TRUE)), - CURLOPT_NOBODY => FALSE, - CURLOPT_HTTPHEADER => array('Content-Type: ' . $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 an entity without proper permissions. - $this->drupalLogout(); - $this->httpRequest('entity/' . $entity_type, 'POST', $serialized, $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 resource which is not REST API enabled. $this->enableService(FALSE); $this->drupalLogin($account); diff --git a/core/modules/rest/lib/Drupal/rest/Tests/DeleteTest.php b/core/modules/rest/lib/Drupal/rest/Tests/DeleteTest.php index d85cf71..10695c4 100644 --- a/core/modules/rest/lib/Drupal/rest/Tests/DeleteTest.php +++ b/core/modules/rest/lib/Drupal/rest/Tests/DeleteTest.php @@ -34,9 +34,9 @@ public static function getInfo() { */ public function testDelete() { // Define the entity types we want to test. - // @todo expand this test to at least nodes and users once their access + // @todo expand this test to at least users once their access // controllers are implemented. - $entity_types = array('entity_test'); + $entity_types = array('entity_test', 'node'); foreach ($entity_types as $entity_type) { $this->enableService('entity:' . $entity_type, 'DELETE'); // Create a user account that has the required permissions to delete diff --git a/core/modules/rest/lib/Drupal/rest/Tests/NodeTest.php b/core/modules/rest/lib/Drupal/rest/Tests/NodeTest.php new file mode 100644 index 0000000..1e02c70 --- /dev/null +++ b/core/modules/rest/lib/Drupal/rest/Tests/NodeTest.php @@ -0,0 +1,76 @@ + 'Node resource', + 'description' => 'Test special cases for node entities.', + 'group' => 'REST', + ); + } + + /** + * Enables node specific REST API configuration and authentication. + * + * @param string $method + * The HTTP method to be tested. + * @param string $operation + * The operation, one of 'view', 'create', 'update' or 'delete'. + */ + protected function enableNodeConfiguration($method, $operation) { + $this->enableService('entity:node', $method); + $permissions = $this->entityPermissions('node', $operation); + $permissions[] = 'restful ' . strtolower($method) . ' entity:node'; + $account = $this->drupalCreateUser($permissions); + $this->drupalLogin($account); + } + + /** + * Performs various tests on nodes and their REST API. + */ + public function testNodes() { + // Tests that the node resource works with comment module enabled. + \Drupal::moduleHandler()->enable(array('comment')); + $this->enableNodeConfiguration('GET', 'view'); + + $node = $this->entityCreate('node'); + $node->save(); + $this->httpRequest('entity/node/' . $node->id(), 'GET', NULL, $this->defaultMimeType); + $this->assertResponse(200); + + // Check that a simple PATCH update to the node title works as expected. + $this->enableNodeConfiguration('PATCH', 'update'); + + // Create a PATCH request body that only updates the title field. + $new_title = $this->randomString(); + $serialized = '{"_links":{"type":{"href":"' + . url('rest/type/node/resttest', array('absolute' => TRUE)) + . '"}},"title":[{"value":"' . $new_title . '"}]}'; + $this->httpRequest('entity/node/' . $node->id(), 'PATCH', $serialized, $this->defaultMimeType); + $this->assertResponse(204); + + // Reload the node from the DB and check if the title was correctly updated. + $node = entity_load('node', $node->id(), TRUE); + $this->assertEqual($node->get('title')->get('value')->getValue(), $new_title); + } +} diff --git a/core/modules/rest/lib/Drupal/rest/Tests/RESTTestBase.php b/core/modules/rest/lib/Drupal/rest/Tests/RESTTestBase.php index f1817d7..24352fc 100644 --- a/core/modules/rest/lib/Drupal/rest/Tests/RESTTestBase.php +++ b/core/modules/rest/lib/Drupal/rest/Tests/RESTTestBase.php @@ -32,6 +32,8 @@ protected function setUp() { parent::setUp(); $this->defaultFormat = 'hal_json'; $this->defaultMimeType = 'application/hal+json'; + // Create a test content type for node testing. + $this->drupalCreateContentType(array('name' => 'resttest', 'type' => 'resttest')); } /** @@ -60,6 +62,7 @@ protected function httpRequest($url, $method, $body = NULL, $mime_type = NULL) { $options = isset($body) ? array('absolute' => TRUE, 'query' => $body) : array('absolute' => TRUE); $curl_options = array( CURLOPT_HTTPGET => TRUE, + CURLOPT_CUSTOMREQUEST => 'GET', CURLOPT_URL => url($url, $options), CURLOPT_NOBODY => FALSE, CURLOPT_HTTPHEADER => array('Accept: ' . $mime_type), @@ -165,7 +168,7 @@ protected function entityValues($entity_type) { 'field_test_text' => array(0 => array('value' => $this->randomString())), ); case 'node': - return array('title' => $this->randomString(), 'type' => $this->randomString()); + return array('title' => $this->randomString(), 'type' => 'resttest'); case 'node_type': return array( 'type' => 'article', @@ -269,6 +272,17 @@ protected function entityPermissions($entity_type, $operation) { case 'delete': return array('administer entity_test content'); } + case 'node': + switch ($operation) { + case 'view': + return array('access content'); + case 'create': + return array('create resttest content'); + case 'update': + return array('edit any resttest content'); + case 'delete': + return array('delete any resttest content'); + } } } } diff --git a/core/modules/rest/lib/Drupal/rest/Tests/ReadTest.php b/core/modules/rest/lib/Drupal/rest/Tests/ReadTest.php index 3ef6594..f0c4d86 100644 --- a/core/modules/rest/lib/Drupal/rest/Tests/ReadTest.php +++ b/core/modules/rest/lib/Drupal/rest/Tests/ReadTest.php @@ -35,9 +35,9 @@ public static function getInfo() { */ public function testRead() { // @todo once EntityNG is implemented for other entity types expand this at - // least to nodes and users. + // least to users. // Define the entity types we want to test. - $entity_types = array('entity_test'); + $entity_types = array('entity_test', 'node'); foreach ($entity_types as $entity_type) { $this->enableService('entity:' . $entity_type, 'GET'); // Create a user account that has the required permissions to read @@ -70,15 +70,17 @@ public function testRead() { $this->assertEqual($decoded['error'], 'Entity with ID 9999 not found', 'Response message is correct.'); // Make sure that field level access works and that the according field is - // not available in the response. + // not available in the response. Only applies to entity_test. // @see entity_test_entity_field_access() - $entity->field_test_text->value = 'no access value'; - $entity->save(); - $response = $this->httpRequest('entity/' . $entity_type . '/' . $entity->id(), 'GET', NULL, $this->defaultMimeType); - $this->assertResponse(200); - $this->assertHeader('content-type', $this->defaultMimeType); - $data = drupal_json_decode($response); - $this->assertFalse(isset($data['field_test_text']), 'Field access protexted field is not visible in the response.'); + if ($entity_type == 'entity_test') { + $entity->field_test_text->value = 'no access value'; + $entity->save(); + $response = $this->httpRequest('entity/' . $entity_type . '/' . $entity->id(), 'GET', NULL, $this->defaultMimeType); + $this->assertResponse(200); + $this->assertHeader('content-type', $this->defaultMimeType); + $data = drupal_json_decode($response); + $this->assertFalse(isset($data['field_test_text']), 'Field access protected field is not visible in the response.'); + } // Try to read an entity without proper permissions. $this->drupalLogout();