Commit 5956f8c4 authored by Dries's avatar Dries

Issue #1839346 by klausi, moshe weitzman: Added REST module: POST/create.

parent 0631d191
......@@ -43,6 +43,7 @@ public function getDerivativeDefinitions(array $base_plugin_definition) {
$this->derivatives[$entity_type] = array(
'id' => 'entity:' . $entity_type,
'entity_type' => $entity_type,
'serialization_class' => $entity_info['class'],
'label' => $entity_info['label'],
);
$this->derivatives[$entity_type] += $base_plugin_definition;
......
......@@ -53,24 +53,36 @@ 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) {
$lower_method = strtolower($method);
// Only expose routes where the HTTP request method exists on the plugin.
if (method_exists($this, $lower_method)) {
$prefix = 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',
'_plugin' => $this->plugin_id,
'id' => NULL,
), array(
// The HTTP method is a requirement for this route.
'_method' => $method,
'_permission' => "restful $lower_method $this->plugin_id",
));
}
else {
$route = new Route("/$prefix/{id}", array(
'_controller' => 'Drupal\rest\RequestHandler::handle',
// Pass the resource plugin ID along as default property.
'_plugin' => $this->plugin_id,
), array(
// The HTTP method is a requirement for this route.
'_method' => $method,
'_permission' => "restful $lower_method $this->plugin_id",
));
$name = strtr($this->plugin_id, ':', '.');
}
$collection->add("$name.$method", $route);
}
}
......
......@@ -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;
}
}
......
......@@ -9,9 +9,11 @@
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;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
use Symfony\Component\HttpKernel\Exception\HttpException;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
......@@ -21,6 +23,7 @@
* @Plugin(
* id = "entity",
* label = @Translation("Entity"),
* serialization_class = "Drupal\Core\Entity\Entity",
* derivative = "Drupal\rest\Plugin\Derivative\EntityDerivative"
* )
*/
......@@ -46,6 +49,42 @@ public function get($id) {
throw new NotFoundHttpException(t('Entity with ID @id not found', array('@id' => $id)));
}
/**
* Responds to entity POST requests and saves the new entity.
*
* @param mixed $id
* Ignored. A new entity is created with a new 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 post($id, EntityInterface $entity) {
// Verify that the deserialized entity is of the type that we expect to
// prevent security issues.
$definition = $this->getDefinition();
if ($entity->entityType() != $definition['entity_type']) {
throw new BadRequestHttpException();
}
// POSTed entities must not have an ID set, because we always want to create
// new entities here.
if (!$entity->isNew()) {
throw new BadRequestHttpException();
}
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 DELETE requests.
*
......
......@@ -34,19 +34,30 @@ public function handle(Request $request, $id = NULL) {
$resource = $this->container
->get('plugin.manager.rest')
->getInstance(array('id' => $plugin));
// Deserialze incoming data if available.
$serializer = $this->container->get('serializer');
$received = $request->getContent();
// @todo De-serialization should happen here if the request is supposed
// to carry incoming data.
$unserialized = NULL;
if (!empty($received)) {
$definition = $resource->getDefinition();
$class = $definition['serialization_class'];
// @todo Replace the format here with something we get from the HTTP
// Content-type header. See http://drupal.org/node/1850704
$unserialized = $serializer->deserialize($received, $class, 'drupal_jsonld');
}
// Invoke the operation on the resource plugin.
try {
$response = $resource->{$method}($id, $received);
$response = $resource->{$method}($id, $unserialized, $request);
}
catch (HttpException $e) {
return new Response($e->getMessage(), $e->getStatusCode(), $e->getHeaders());
}
// Serialize the outgoing data for the response, if available.
$data = $response->getResponseData();
if ($data != NULL) {
// Serialize the response data.
$serializer = $this->container->get('serializer');
// @todo Replace the format here with something we get from the HTTP
// Accept headers. See http://drupal.org/node/1833440
$output = $serializer->serialize($data, 'drupal_jsonld');
......
......@@ -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 {
......
<?php
/**
* @file
* Definition of Drupal\rest\test\CreateTest.
*/
namespace Drupal\rest\Tests;
use Drupal\rest\Tests\RESTTestBase;
/**
* Tests resource creation on user, node and test entities.
*/
class CreateTest extends RESTTestBase {
/**
* Modules to enable.
*
* @var array
*/
public static $modules = array('rest', 'entity_test');
public static function getInfo() {
return array(
'name' => '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_values = $this->entityValues($entity_type);
$entity = entity_create($entity_type, $entity_values);
$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);
$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);
$this->assertEqual($value, $actual_value->value, 'Created property ' . $property . ' expected: ' . $value . ', actual: ' . $actual_value->value);
}
// 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);
// @todo Once EntityNG is implemented for other entity types add a security
// test. It should not be possible for example to create a test entity on a
// node resource route.
}
}
......@@ -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.');
......
......@@ -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();
}
}
......
......@@ -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.
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment