Commit 79b823ac authored by Dries's avatar Dries

Issue #1834288 by klausi, ygerasimov: RESTfully request an entity with JSON-LD serialized response.

parent 49b37cd4
......@@ -7,17 +7,19 @@
namespace Drupal\rest\Plugin\rest\resource;
use Drupal\rest\Plugin\ResourceBase;
use Drupal\Core\Annotation\Plugin;
use Symfony\Component\HttpFoundation\Response;
use Drupal\Core\Annotation\Translation;
use Drupal\rest\Plugin\ResourceBase;
use Drupal\rest\ResourceResponse;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
/**
* Provides a resource for database watchdog log entries.
*
* @Plugin(
* id = "dblog",
* label = "Watchdog database log"
* id = "dblog",
* label = @Translation("Watchdog database log")
* )
*/
class DBLogResource extends ResourceBase {
......@@ -38,23 +40,24 @@ public function routes() {
*
* Returns a watchdog log entry for the specified ID.
*
* @return \Symfony\Component\HttpFoundation\Response
* The response object.
* @return \Drupal\rest\ResourceResponse
* The response containing the log entry.
*
* @throws \Symfony\Component\HttpKernel\Exception\HttpException
*/
public function get($id = NULL) {
if ($id) {
$result = db_select('watchdog', 'w')
->condition('wid', $id)
->fields('w')
->execute()
->fetchAll();
if (empty($result)) {
throw new NotFoundHttpException('Not Found');
$result = db_query("SELECT * FROM {watchdog} WHERE wid = :wid", array(':wid' => $id))
->fetchObject();
if (!empty($result)) {
// 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));
return $response;
}
// @todo remove hard coded format here.
return new Response(drupal_json_encode($result[0]), 200, array('Content-Type' => 'application/json'));
}
throw new NotFoundHttpException('Not Found');
}
}
......@@ -8,9 +8,10 @@
namespace Drupal\rest\Plugin\rest\resource;
use Drupal\Core\Annotation\Plugin;
use Drupal\Core\Annotation\Translation;
use Drupal\Core\Entity\EntityStorageException;
use Drupal\rest\Plugin\ResourceBase;
use Symfony\Component\HttpFoundation\Response;
use Drupal\rest\ResourceResponse;
use Symfony\Component\HttpKernel\Exception\HttpException;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
......@@ -18,21 +19,41 @@
* Represents entities as resources.
*
* @Plugin(
* id = "entity",
* label = "Entity",
* derivative = "Drupal\rest\Plugin\Derivative\EntityDerivative"
* id = "entity",
* label = @Translation("Entity"),
* derivative = "Drupal\rest\Plugin\Derivative\EntityDerivative"
* )
*/
class EntityResource extends ResourceBase {
/**
* Responds to entity GET requests.
*
* @param mixed $id
* The entity ID.
*
* @return \Drupal\rest\ResourceResponse
* The response containing the loaded entity.
*
* @throws \Symfony\Component\HttpKernel\Exception\HttpException
*/
public function get($id) {
$definition = $this->getDefinition();
$entity = entity_load($definition['entity_type'], $id);
if ($entity) {
return new ResourceResponse($entity);
}
throw new NotFoundHttpException(t('Entity with ID @id not found', array('@id' => $id)));
}
/**
* Responds to entity DELETE requests.
*
* @param mixed $id
* The entity ID.
*
* @return \Symfony\Component\HttpFoundation\Response
* The response object.
* @return \Drupal\rest\ResourceResponse
* The HTTP response object.
*
* @throws \Symfony\Component\HttpKernel\Exception\HttpException
*/
......@@ -43,7 +64,7 @@ public function delete($id) {
try {
$entity->delete();
// Delete responses have an empty body.
return new Response('', 204);
return new ResourceResponse(NULL, 204);
}
catch (EntityStorageException $e) {
throw new HttpException(500, 'Internal Server Error', $e);
......
......@@ -38,12 +38,26 @@ public function handle($plugin, Request $request, $id = NULL) {
$resource = $this->container
->get('plugin.manager.rest')
->getInstance(array('id' => $plugin));
$received = $request->getContent();
// @todo De-serialization should happen here if the request is supposed
// to carry incoming data.
try {
return $resource->{$method}($id);
$response = $resource->{$method}($id, $received);
}
catch (HttpException $e) {
return new Response($e->getMessage(), $e->getStatusCode(), $e->getHeaders());
}
$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');
$response->setContent($output);
$response->headers->set('Content-Type', 'application/vnd.drupal.ld+json');
}
return $response;
}
return new Response('Access Denied', 403);
}
......
<?php
/**
* @file
* Definition of Drupal\rest\ResourceResponse.
*/
namespace Drupal\rest;
use Symfony\Component\HttpFoundation\Response;
/**
* Contains data for serialization before sending the response.
*/
class ResourceResponse extends Response {
/**
* Response data that should be serialized.
*
* @var mixed
*/
protected $responseData;
/**
* Constructor for ResourceResponse objects.
*
* @param mixed $data
* Response data that should be serialized.
* @param int $status
* The response status code.
* @param array $headers
* An array of response headers.
*/
public function __construct($data = NULL, $status = 200, $headers = array()) {
$this->responseData = $data;
parent::__construct('', $status, $headers);
}
/**
* Returns response data that should be serialized.
*
* @return mixed
* Response data that should be serialized.
*/
public function getResponseData() {
return $this->responseData;
}
}
......@@ -19,7 +19,7 @@ class DBLogTest extends RESTTestBase {
*
* @var array
*/
public static $modules = array('rest', 'dblog');
public static $modules = array('jsonld', 'rest', 'dblog');
public static function getInfo() {
return array(
......@@ -32,17 +32,7 @@ public static function getInfo() {
public function setUp() {
parent::setUp();
// Enable web API for the watchdog resource.
$config = config('rest');
$config->set('resources', array(
'dblog' => 'dblog',
));
$config->save();
// Rebuild routing cache, so that the web API paths are available.
drupal_container()->get('router.builder')->rebuild();
// Reset the Simpletest permission cache, so that the new resource
// permissions get picked up.
drupal_static_reset('checkPermissions');
$this->enableService('dblog');
}
/**
......@@ -60,15 +50,16 @@ public function testWatchdog() {
$account = $this->drupalCreateUser(array('restful get dblog'));
$this->drupalLogin($account);
$response = $this->httpRequest("dblog/$id", 'GET');
$response = $this->httpRequest("dblog/$id", 'GET', NULL, 'application/json');
$this->assertResponse(200);
$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.');
$this->assertEqual($log['message'], 'Test message', 'Log message text is correct.');
// Request an unknown log entry.
$response = $this->httpRequest("dblog/9999", 'GET');
$response = $this->httpRequest("dblog/9999", 'GET', NULL, 'application/json');
$this->assertResponse(404);
$this->assertEqual($response, 'Not Found', 'Response message is correct.');
}
......
......@@ -34,18 +34,7 @@ public static function getInfo() {
*/
public function testDelete() {
foreach (entity_get_info() as $entity_type => $info) {
// Enable web API for this entity type.
$config = config('rest');
$config->set('resources', array(
'entity:' . $entity_type => 'entity:' . $entity_type,
));
$config->save();
// Rebuild routing cache, so that the web API paths are available.
drupal_container()->get('router.builder')->rebuild();
// Reset the Simpletest permission cache, so that the new resource
// permissions get picked up.
drupal_static_reset('checkPermissions');
$this->enableService('entity:' . $entity_type);
// Create a user account that has the required permissions to delete
// resources via the web API.
$account = $this->drupalCreateUser(array('restful delete entity:' . $entity_type));
......@@ -81,6 +70,7 @@ public function testDelete() {
$this->assertNotIdentical(FALSE, entity_load($entity_type, $entity->id(), TRUE), 'The ' . $entity_type . ' entity is still in the database.');
}
// Try to delete a resource which is not web API enabled.
$this->enableService(FALSE);
$account = $this->drupalCreateUser();
// Reset cURL here because it is confused from our previously used cURL
// options.
......@@ -88,32 +78,7 @@ public function testDelete() {
$this->drupalLogin($account);
$this->httpRequest('entity/user/' . $account->id(), 'DELETE');
$user = entity_load('user', $account->id(), TRUE);
$this->assertEqual($account->id(), $user->id());
$this->assertEqual($account->id(), $user->id(), 'User still exists in the database.');
$this->assertResponse(404);
}
/**
* 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..
*
* @return \Drupal\Core\Entity\EntityInterface
* The new entity object.
*/
protected function entityCreate($entity_type) {
switch ($entity_type) {
case 'entity_test':
return entity_create('entity_test', array('name' => 'test', 'user_id' => 1));
case 'node':
return entity_create('node', array('title' => $this->randomString()));
case 'user':
return entity_create('user', array('name' => $this->randomName()));
default:
return entity_create($entity_type, array());
}
}
}
......@@ -14,6 +14,13 @@
*/
abstract class RESTTestBase extends WebTestBase {
/**
* Stores HTTP response headers from the last HTTP request.
*
* @var array
*/
protected $responseHeaders;
/**
* Helper function to issue a HTTP request with simpletest's cURL.
*
......@@ -31,36 +38,136 @@ protected function httpRequest($url, $method, $body = NULL, $format = 'applicati
case 'GET':
// Set query if there are additional GET parameters.
$options = isset($body) ? array('absolute' => TRUE, 'query' => $body) : array('absolute' => TRUE);
return $this->curlExec(array(
$curl_options = array(
CURLOPT_HTTPGET => TRUE,
CURLOPT_URL => url($url, $options),
CURLOPT_NOBODY => FALSE)
CURLOPT_NOBODY => FALSE
);
break;
case 'POST':
return $this->curlExec(array(
$curl_options = array(
CURLOPT_HTTPGET => FALSE,
CURLOPT_POST => TRUE,
CURLOPT_POSTFIELDS => $body,
CURLOPT_URL => url($url, array('absolute' => TRUE)),
CURLOPT_NOBODY => FALSE,
CURLOPT_HTTPHEADER => array('Content-Type: ' . $format),
));
);
break;
case 'PUT':
return $this->curlExec(array(
$curl_options = array(
CURLOPT_HTTPGET => FALSE,
CURLOPT_CUSTOMREQUEST => 'PUT',
CURLOPT_POSTFIELDS => $body,
CURLOPT_URL => url($url, array('absolute' => TRUE)),
CURLOPT_NOBODY => FALSE,
CURLOPT_HTTPHEADER => array('Content-Type: ' . $format),
));
);
break;
case 'DELETE':
return $this->curlExec(array(
$curl_options = array(
CURLOPT_HTTPGET => FALSE,
CURLOPT_CUSTOMREQUEST => 'DELETE',
CURLOPT_URL => url($url, array('absolute' => TRUE)),
CURLOPT_NOBODY => FALSE,
));
);
break;
}
// Include all HTTP headers in the response.
$curl_options[CURLOPT_HEADER] = TRUE;
$response = $this->curlExec($curl_options);
list($header, $body) = explode("\r\n\r\n", $response, 2);
$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]) : '';
}
$this->verbose($method . ' request to: ' . $url .
'<hr />Code: ' . curl_getinfo($this->curlHandle, CURLINFO_HTTP_CODE) .
'<hr />Response headers: ' . $header .
'<hr />Response body: ' . $body);
return $body;
}
/**
* 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..
*
* @return \Drupal\Core\Entity\EntityInterface
* The new entity object.
*/
protected function entityCreate($entity_type) {
switch ($entity_type) {
case 'entity_test':
return entity_create('entity_test', array('name' => $this->randomName(), 'user_id' => 1));
case 'node':
return entity_create('node', array('title' => $this->randomString()));
case 'user':
return entity_create('user', array('name' => $this->randomName()));
default:
return entity_create($entity_type, array());
}
}
/**
* Enables the web service interface for a specific entity type.
*
* @param string|FALSE $resource_type
* The resource type that should get web API enabled or FALSE to disable all
* resource types.
*/
protected function enableService($resource_type) {
// Enable web API for this entity type.
$config = config('rest');
if ($resource_type) {
$config->set('resources', array(
$resource_type => $resource_type,
));
}
else {
$config->set('resources', array());
}
$config->save();
// Rebuild routing cache, so that the web API paths are available.
drupal_container()->get('router.builder')->rebuild();
// Reset the Simpletest permission cache, so that the new resource
// permissions get picked up.
drupal_static_reset('checkPermissions');
}
/**
* Check if a HTTP response header exists and has the expected value.
*
* @param string $header
* The header key, example: Content-Type
* @param string $value
* The header value.
* @param string $message
* (optional) A message to display with the assertion.
* @param string $group
* (optional) The group this message is in, which is displayed in a column
* in test output. Use 'Debug' to indicate this is debugging output. Do not
* translate this string. Defaults to 'Other'; most tests do not override
* this default.
*
* @return bool
* TRUE if the assertion succeeded, FALSE otherwise.
*/
protected function assertHeader($header, $value, $message = '', $group = 'Browser') {
$match = isset($this->responseHeaders[$header]) && $this->responseHeaders[$header] == $value;
return $this->assertTrue($match, $message ? $message : 'HTTP response header ' . $header . ' with value ' . $value . ' found.', $group);
}
}
<?php
/**
* @file
* Definition of Drupal\rest\test\ReadTest.
*/
namespace Drupal\rest\Tests;
use Drupal\rest\Tests\RESTTestBase;
/**
* Tests resource read operations on test entities, nodes and users.
*/
class ReadTest extends RESTTestBase {
/**
* Modules to enable.
*
* @var array
*/
public static $modules = array('jsonld', 'rest', 'entity_test');
public static function getInfo() {
return array(
'name' => 'Read resource',
'description' => 'Tests the retrieval of resources.',
'group' => 'REST',
);
}
/**
* Tests several valid and invalid read requests on all entity types.
*/
public function testRead() {
// @todo once EntityNG is implemented for other entity types use the full
// entity_get_info() for all entity types here.
$entity_test_info = entity_get_info('entity_test');
$entity_info = array('entity_test' => $entity_test_info);
foreach ($entity_info as $entity_type => $info) {
$this->enableService('entity:' . $entity_type);
// Create a user account that has the required permissions to delete
// resources via the web API.
$account = $this->drupalCreateUser(array('restful get entity:' . $entity_type));
// Reset cURL here because it is confused from our previously used cURL
// options.
unset($this->curlHandle);
$this->drupalLogin($account);
// Create an entity programmatically.
$entity = $this->entityCreate($entity_type);
$entity->save();
// 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');
$data = drupal_json_decode($response);
// Only assert one example property here, other properties should be
// checked in serialization tests.
$this->assertEqual($data['uuid'][LANGUAGE_DEFAULT][0]['value'], $entity->uuid(), 'Entity UUID is correct');
// Try to read an entity that does not exist.
$response = $this->httpRequest('entity/' . $entity_type . '/9999', 'GET', NULL, 'application/ld+json');
$this->assertResponse(404);
$this->assertEqual($response, 'Entity with ID 9999 not found', 'Response message is correct.');
// Try to read an entity without proper permissions.
$this->drupalLogout();
$response = $this->httpRequest('entity/' . $entity_type . '/' . $entity->id(), 'GET', NULL, 'application/vnd.drupal.ld+json');
$this->assertResponse(403);
$this->assertNull(drupal_json_decode($response), 'No valid JSON found.');
}
// Try to read a resource which is not web API enabled.
$account = $this->drupalCreateUser();
// Reset cURL here because it is confused from our previously used cURL
// options.
unset($this->curlHandle);
$this->drupalLogin($account);
$response = $this->httpRequest('entity/user/' . $account->id(), 'GET', NULL, 'application/vnd.drupal.ld+json');
$this->assertResponse(404);
$this->assertNull(drupal_json_decode($response), 'No valid JSON found.');
}
}
......@@ -3,4 +3,6 @@ description = Exposes entities and other resources as RESTful web API
package = Core
version = VERSION
core = 8.x
; @todo Remove this dependency once hard coding to JSON-LD is gone.
dependencies[] = jsonld
configure = admin/config/services/rest
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