diff --git a/core/modules/rest/lib/Drupal/rest/Plugin/ResourceBase.php b/core/modules/rest/lib/Drupal/rest/Plugin/ResourceBase.php index 962af71..4682542 100644 --- a/core/modules/rest/lib/Drupal/rest/Plugin/ResourceBase.php +++ b/core/modules/rest/lib/Drupal/rest/Plugin/ResourceBase.php @@ -54,24 +54,41 @@ public function permissions() { public function routes() { $collection = new RouteCollection(); + $name = strtr($this->plugin_id, ':', '.'); + $prefix = strtr($this->plugin_id, ':', '/'); $methods = $this->requestMethods(); foreach ($methods as $method) { // Only expose routes where the HTTP request method exists on the plugin. if (method_exists($this, strtolower($method))) { - $prefix = strtr($this->plugin_id, ':', '/'); - $route = new Route("/$prefix/{id}", array( - '_controller' => 'Drupal\rest\RequestHandler::handle', - // @todo Once http://drupal.org/node/1793520 is committed we will have - // route object avaialble in the controller so 'plugin' property - // should be changed to '_plugin'. - // @see RequestHandler::handle(). - 'plugin' => $this->plugin_id, - ), array( - // The HTTP method is a requirement for this route. - '_method' => $method, - )); - - $name = strtr($this->plugin_id, ':', '.'); + // Special case for resource creation via POST: Add a route that does + // not require an ID. + if ($method == 'POST') { + $route = new Route("/$prefix", array( + '_controller' => 'Drupal\rest\RequestHandler::handle', + // @todo Once http://drupal.org/node/1793520 is committed we will have + // route object available in the controller so 'plugin' property + // should be changed to '_plugin'. + // @see RequestHandler::handle(). + 'plugin' => $this->plugin_id, + 'id' => NULL, + ), array( + // The HTTP method is a requirement for this route. + '_method' => $method, + )); + } + else { + $route = new Route("/$prefix/{id}", array( + '_controller' => 'Drupal\rest\RequestHandler::handle', + // @todo Once http://drupal.org/node/1793520 is committed we will have + // route object available in the controller so 'plugin' property + // should be changed to '_plugin'. + // @see RequestHandler::handle(). + 'plugin' => $this->plugin_id, + ), array( + // The HTTP method is a requirement for this route. + '_method' => $method, + )); + } $collection->add("$name.$method", $route); } } diff --git a/core/modules/rest/lib/Drupal/rest/Plugin/rest/resource/DBLogResource.php b/core/modules/rest/lib/Drupal/rest/Plugin/rest/resource/DBLogResource.php index 5a1fe31..13af57b 100644 --- a/core/modules/rest/lib/Drupal/rest/Plugin/rest/resource/DBLogResource.php +++ b/core/modules/rest/lib/Drupal/rest/Plugin/rest/resource/DBLogResource.php @@ -47,14 +47,14 @@ public function routes() { */ public function get($id = NULL) { if ($id) { - $result = db_query("SELECT * FROM {watchdog} WHERE wid = :wid", array(':wid' => $id)) + $record = db_query("SELECT * FROM {watchdog} WHERE wid = :wid", array(':wid' => $id)) ->fetchObject(); - if (!empty($result)) { + if (!empty($record)) { // Serialization is done here, so we indicate with NULL that there is no // subsequent serialization necessary. $response = new ResourceResponse(NULL, 200, array('Content-Type' => 'application/json')); // @todo remove hard coded format here. - $response->setContent(drupal_json_encode($result)); + $response->setContent(drupal_json_encode($record)); return $response; } } 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 689fd84..18a4dd9 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 @@ -9,6 +9,7 @@ use Drupal\Core\Annotation\Plugin; use Drupal\Core\Annotation\Translation; +use Drupal\Core\Entity\EntityInterface; use Drupal\Core\Entity\EntityStorageException; use Drupal\rest\Plugin\ResourceBase; use Drupal\rest\ResourceResponse; @@ -47,6 +48,61 @@ public function get($id) { } /** + * Responds to entity POST requests. + * + * @param mixed $id + * Ignored. + * @param \Drupal\Core\Entity\EntityInterface $entity + * The entity. + * + * @return \Drupal\rest\ResourceResponse + * The HTTP response object. + * + * @throws \Symfony\Component\HttpKernel\Exception\HttpException + */ + public function post($id, EntityInterface $entity) { + try { + $entity->save(); + $url = url(strtr($this->plugin_id, ':', '/') . '/' . $entity->id(), array('absolute' => TRUE)); + // 201 Created responses have an empty body. + return new ResourceResponse(NULL, 201, array('Location' => $url)); + } + catch (EntityStorageException $e) { + throw new HttpException(500, 'Internal Server Error', $e); + } + } + + /** + * Responds to entity PUT requests. + * + * @param mixed $id + * The entity ID. + * @param \Drupal\Core\Entity\EntityInterface $entity + * The entity. + * + * @return \Drupal\rest\ResourceResponse + * The HTTP response object. + * + * @throws \Symfony\Component\HttpKernel\Exception\HttpException + */ + public function put($id, EntityInterface $entity) { + if (empty($id)) { + throw new NotFoundHttpException(); + } + $info = $entity->entityInfo(); + // Make sure that the entity ID is the one provided in the URL. + $entity->{$info['entity_keys']['id']} = $id; + try { + $entity->save(); + // Update responses have an empty body. + return new ResourceResponse(NULL, 200); + } + catch (EntityStorageException $e) { + throw new HttpException(500, 'Internal Server Error', $e); + } + } + + /** * Responds to entity DELETE requests. * * @param mixed $id diff --git a/core/modules/rest/lib/Drupal/rest/RequestHandler.php b/core/modules/rest/lib/Drupal/rest/RequestHandler.php index a374d14..2b4b034 100644 --- a/core/modules/rest/lib/Drupal/rest/RequestHandler.php +++ b/core/modules/rest/lib/Drupal/rest/RequestHandler.php @@ -38,11 +38,17 @@ public function handle($plugin, Request $request, $id = NULL) { $resource = $this->container ->get('plugin.manager.rest') ->getInstance(array('id' => $plugin)); + $serializer = $this->container->get('serializer'); $received = $request->getContent(); - // @todo De-serialization should happen here if the request is supposed - // to carry incoming data. + if (!empty($received)) { + // @todo what do we pass here as "type" to the serializer? + // De-serialization does not work yet, see http://drupal.org/node/1838596 + // $received = $serializer->deserialize($received, $plugin, 'drupal_jsonld'); + // @todo Remove hard-coded test entity here. + $received = entity_create('entity_test', array('name' => 'test', 'user_id' => 1)); + } try { - $response = $resource->{$method}($id, $received); + $response = $resource->{$method}($id, $received, $request); } catch (HttpException $e) { return new Response($e->getMessage(), $e->getStatusCode(), $e->getHeaders()); diff --git a/core/modules/rest/lib/Drupal/rest/ResourceResponse.php b/core/modules/rest/lib/Drupal/rest/ResourceResponse.php index a93f25e..d79653a 100644 --- a/core/modules/rest/lib/Drupal/rest/ResourceResponse.php +++ b/core/modules/rest/lib/Drupal/rest/ResourceResponse.php @@ -11,6 +11,11 @@ /** * Contains data for serialization before sending the response. + * + * We do not want to abuse the $content property on the Response class to store + * our response data. $content implies that the provided data must either be a + * string or an object with a __toString() method, which is not a requirement + * for data used here. */ class ResourceResponse extends Response { diff --git a/core/modules/rest/lib/Drupal/rest/Tests/CreateTest.php b/core/modules/rest/lib/Drupal/rest/Tests/CreateTest.php new file mode 100644 index 0000000..d715fe9 --- /dev/null +++ b/core/modules/rest/lib/Drupal/rest/Tests/CreateTest.php @@ -0,0 +1,71 @@ + 'Create resource', + 'description' => 'Tests the creation of resources.', + 'group' => 'REST', + ); + } + + /** + * Tests several valid and invalid create requests on all entity types. + */ + 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); + // Create a user account that has the required permissions to create + // resources via the web API. + $account = $this->drupalCreateUser(array('restful post entity:' . $entity_type)); + $this->drupalLogin($account); + $entity = $this->entityCreate($entity_type); + $serialized = $serializer->serialize($entity, 'drupal_jsonld'); + // Create the entity over the web API. + $response = $this->httpRequest('entity/' . $entity_type, 'POST', $serialized, 'application/vnd.drupal.ld+json'); + $this->assertResponse('201', 'HTTP response code is correct.'); + + // Get the new entity ID from the location header and try to read it from + // the database. + $location_url = $this->responseHeaders['location']; + $url_parts = explode('/', $location_url); + $id = end($url_parts); + $entity = entity_load($entity_type, $id); + $this->assertNotIdentical(FALSE, entity_load($entity_type, $id, TRUE), 'The new ' . $entity_type . ' was found in the database.'); + + // Try to create an entity without proper permissions. + $this->drupalLogout(); + $response = $this->httpRequest('entity/' . $entity_type, 'POST', $serialized, 'application/vnd.drupal.ld+json'); + $this->assertResponse(403); + + // Try to create a resource which is not web API enabled. + $this->enableService(FALSE); + $this->drupalLogin($account); + $this->httpRequest('entity/entity_test', 'POST', $serialized, 'application/vnd.drupal.ld+json'); + $this->assertResponse(404); + } +} diff --git a/core/modules/rest/lib/Drupal/rest/Tests/DBLogTest.php b/core/modules/rest/lib/Drupal/rest/Tests/DBLogTest.php index 5002397..9ea37c1 100644 --- a/core/modules/rest/lib/Drupal/rest/Tests/DBLogTest.php +++ b/core/modules/rest/lib/Drupal/rest/Tests/DBLogTest.php @@ -52,7 +52,7 @@ public function testWatchdog() { $response = $this->httpRequest("dblog/$id", 'GET', NULL, 'application/json'); $this->assertResponse(200); - $this->assertHeader('Content-Type', 'application/json'); + $this->assertHeader('content-type', 'application/json'); $log = drupal_json_decode($response); $this->assertEqual($log['wid'], $id, 'Log ID is correct.'); $this->assertEqual($log['type'], 'rest_test', 'Type of log message is correct.'); diff --git a/core/modules/rest/lib/Drupal/rest/Tests/RESTTestBase.php b/core/modules/rest/lib/Drupal/rest/Tests/RESTTestBase.php index acf4802..3640e48 100644 --- a/core/modules/rest/lib/Drupal/rest/Tests/RESTTestBase.php +++ b/core/modules/rest/lib/Drupal/rest/Tests/RESTTestBase.php @@ -85,7 +85,9 @@ protected function httpRequest($url, $method, $body = NULL, $format = 'applicati $header_lines = explode("\r\n", $header); foreach ($header_lines as $line) { $parts = explode(':', $line, 2); - $this->responseHeaders[$parts[0]] = isset($parts[1]) ? trim($parts[1]) : ''; + // Store the header keys lower cased to be more robust. Headers are case + // insensitive according to RFC 2616. + $this->responseHeaders[strtolower($parts[0])] = isset($parts[1]) ? trim($parts[1]) : ''; } $this->verbose($method . ' request to: ' . $url . @@ -99,25 +101,38 @@ protected function httpRequest($url, $method, $body = NULL, $format = 'applicati /** * Creates entity objects based on their types. * - * Required properties differ from entity type to entity type, so we keep a - * minimum mapping here. - * * @param string $entity_type - * The type of the entity that should be created.. + * The type of the entity that should be created. * * @return \Drupal\Core\Entity\EntityInterface * The new entity object. */ protected function entityCreate($entity_type) { + return entity_create($entity_type, $this->entityValues($entity_type)); + } + + /** + * Provides an array of suitable property values for an entity type. + * + * Required properties differ from entity type to entity type, so we keep a + * minimum mapping here. + * + * @param string $entity_type + * The type of the entity that should be created. + * + * @return array + * An array of values keyed by property name. + */ + protected function entityValues($entity_type) { switch ($entity_type) { case 'entity_test': - return entity_create('entity_test', array('name' => $this->randomName(), 'user_id' => 1)); + return array('name' => $this->randomName(), 'user_id' => 1); case 'node': - return entity_create('node', array('title' => $this->randomString())); + return array('title' => $this->randomString()); case 'user': - return entity_create('user', array('name' => $this->randomName())); + return array('name' => $this->randomName()); default: - return entity_create($entity_type, array()); + return array(); } } diff --git a/core/modules/rest/lib/Drupal/rest/Tests/ReadTest.php b/core/modules/rest/lib/Drupal/rest/Tests/ReadTest.php index 28ffcea..54bb8c2 100644 --- a/core/modules/rest/lib/Drupal/rest/Tests/ReadTest.php +++ b/core/modules/rest/lib/Drupal/rest/Tests/ReadTest.php @@ -53,7 +53,7 @@ public function testRead() { // Read it over the web API. $response = $this->httpRequest('entity/' . $entity_type . '/' . $entity->id(), 'GET', NULL, 'application/vnd.drupal.ld+json'); $this->assertResponse('200', 'HTTP response code is correct.'); - $this->assertHeader('Content-Type', 'application/vnd.drupal.ld+json'); + $this->assertHeader('content-type', 'application/vnd.drupal.ld+json'); $data = drupal_json_decode($response); // Only assert one example property here, other properties should be // checked in serialization tests. diff --git a/core/modules/rest/lib/Drupal/rest/Tests/UpdateTest.php b/core/modules/rest/lib/Drupal/rest/Tests/UpdateTest.php new file mode 100644 index 0000000..f438043 --- /dev/null +++ b/core/modules/rest/lib/Drupal/rest/Tests/UpdateTest.php @@ -0,0 +1,84 @@ + 'Update resource', + 'description' => 'Tests the update of resources.', + 'group' => 'REST', + ); + } + + /** + * Tests several valid and invalid update requests on test entities. + */ + 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); + // Create a user account that has the required permissions to create + // resources via the web API. + $account = $this->drupalCreateUser(array('restful put entity:' . $entity_type)); + $this->drupalLogin($account); + + // Create an entity and save it to the database. + $entity_values = $this->entityValues($entity_type); + $entity = entity_create($entity_type, $entity_values); + $entity->save(); + // Create a second entity that will overwrite the original. + $update_values = $this->entityValues($entity_type); + // @todo Remove the next line once deserialization is implemented. For now + // we set the update value to the same that is used in the RequestHandler. + $update_values['name'] = 'test'; + $update_entity = entity_create($entity_type, $entity_values); + + $serialized = $serializer->serialize($update_entity, 'drupal_jsonld'); + // Update the entity over the web API. + $response = $this->httpRequest('entity/' . $entity_type . '/' . $entity->id(), 'PUT', $serialized, 'application/vnd.drupal.ld+json'); + $this->assertResponse('200', 'HTTP response code is correct.'); + + // Re-load updated entity from the database. + $entity = entity_load($entity_type, $entity->id(), TRUE); + foreach ($update_values as $property => $value) { + $actual_value = $entity->get($property); + $this->assertEqual($value, $actual_value->value, 'Updated property ' . $property . ' expected: ' . $value . ', actual: ' . $actual_value->value); + } + + // Try to update an entity without proper permissions. + $this->drupalLogout(); + $response = $this->httpRequest('entity/' . $entity_type . '/' . $entity->id(), 'PUT', $serialized, 'application/vnd.drupal.ld+json'); + $this->assertResponse(403); + + // Try to update a resource which is not web API enabled. + $this->enableService(FALSE); + // Reset cURL here because it is confused from our previously used cURL + // options. + unset($this->curlHandle); + $this->drupalLogin($account); + $response = $this->httpRequest('entity/' . $entity_type . '/' . $entity->id(), 'PUT', $serialized, 'application/vnd.drupal.ld+json'); + $this->assertResponse(404); + } +}