Skip to content
Snippets Groups Projects
Commit 79b823ac authored by Dries Buytaert's avatar Dries Buytaert
Browse files

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

parent 49b37cd4
No related branches found
No related tags found
2 merge requests!7452Issue #1797438. HTML5 validation is preventing form submit and not fully...,!789Issue #3210310: Adjust Database API to remove deprecated Drupal 9 code in Drupal 10
...@@ -7,17 +7,19 @@ ...@@ -7,17 +7,19 @@
namespace Drupal\rest\Plugin\rest\resource; namespace Drupal\rest\Plugin\rest\resource;
use Drupal\rest\Plugin\ResourceBase;
use Drupal\Core\Annotation\Plugin; 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; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
/** /**
* Provides a resource for database watchdog log entries. * Provides a resource for database watchdog log entries.
* *
* @Plugin( * @Plugin(
* id = "dblog", * id = "dblog",
* label = "Watchdog database log" * label = @Translation("Watchdog database log")
* ) * )
*/ */
class DBLogResource extends ResourceBase { class DBLogResource extends ResourceBase {
...@@ -38,23 +40,24 @@ public function routes() { ...@@ -38,23 +40,24 @@ public function routes() {
* *
* Returns a watchdog log entry for the specified ID. * Returns a watchdog log entry for the specified ID.
* *
* @return \Symfony\Component\HttpFoundation\Response * @return \Drupal\rest\ResourceResponse
* The response object. * The response containing the log entry.
* *
* @throws \Symfony\Component\HttpKernel\Exception\HttpException * @throws \Symfony\Component\HttpKernel\Exception\HttpException
*/ */
public function get($id = NULL) { public function get($id = NULL) {
if ($id) { if ($id) {
$result = db_select('watchdog', 'w') $result = db_query("SELECT * FROM {watchdog} WHERE wid = :wid", array(':wid' => $id))
->condition('wid', $id) ->fetchObject();
->fields('w') if (!empty($result)) {
->execute() // Serialization is done here, so we indicate with NULL that there is no
->fetchAll(); // subsequent serialization necessary.
if (empty($result)) { $response = new ResourceResponse(NULL, 200, array('Content-Type' => 'application/json'));
throw new NotFoundHttpException('Not Found'); // @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 @@ ...@@ -8,9 +8,10 @@
namespace Drupal\rest\Plugin\rest\resource; namespace Drupal\rest\Plugin\rest\resource;
use Drupal\Core\Annotation\Plugin; use Drupal\Core\Annotation\Plugin;
use Drupal\Core\Annotation\Translation;
use Drupal\Core\Entity\EntityStorageException; use Drupal\Core\Entity\EntityStorageException;
use Drupal\rest\Plugin\ResourceBase; 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\HttpException;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
...@@ -18,21 +19,41 @@ ...@@ -18,21 +19,41 @@
* Represents entities as resources. * Represents entities as resources.
* *
* @Plugin( * @Plugin(
* id = "entity", * id = "entity",
* label = "Entity", * label = @Translation("Entity"),
* derivative = "Drupal\rest\Plugin\Derivative\EntityDerivative" * derivative = "Drupal\rest\Plugin\Derivative\EntityDerivative"
* ) * )
*/ */
class EntityResource extends ResourceBase { 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. * Responds to entity DELETE requests.
* *
* @param mixed $id * @param mixed $id
* The entity ID. * The entity ID.
* *
* @return \Symfony\Component\HttpFoundation\Response * @return \Drupal\rest\ResourceResponse
* The response object. * The HTTP response object.
* *
* @throws \Symfony\Component\HttpKernel\Exception\HttpException * @throws \Symfony\Component\HttpKernel\Exception\HttpException
*/ */
...@@ -43,7 +64,7 @@ public function delete($id) { ...@@ -43,7 +64,7 @@ public function delete($id) {
try { try {
$entity->delete(); $entity->delete();
// Delete responses have an empty body. // Delete responses have an empty body.
return new Response('', 204); return new ResourceResponse(NULL, 204);
} }
catch (EntityStorageException $e) { catch (EntityStorageException $e) {
throw new HttpException(500, 'Internal Server Error', $e); throw new HttpException(500, 'Internal Server Error', $e);
......
...@@ -38,12 +38,26 @@ public function handle($plugin, Request $request, $id = NULL) { ...@@ -38,12 +38,26 @@ public function handle($plugin, Request $request, $id = NULL) {
$resource = $this->container $resource = $this->container
->get('plugin.manager.rest') ->get('plugin.manager.rest')
->getInstance(array('id' => $plugin)); ->getInstance(array('id' => $plugin));
$received = $request->getContent();
// @todo De-serialization should happen here if the request is supposed
// to carry incoming data.
try { try {
return $resource->{$method}($id); $response = $resource->{$method}($id, $received);
} }
catch (HttpException $e) { catch (HttpException $e) {
return new Response($e->getMessage(), $e->getStatusCode(), $e->getHeaders()); 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); 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 { ...@@ -19,7 +19,7 @@ class DBLogTest extends RESTTestBase {
* *
* @var array * @var array
*/ */
public static $modules = array('rest', 'dblog'); public static $modules = array('jsonld', 'rest', 'dblog');
public static function getInfo() { public static function getInfo() {
return array( return array(
...@@ -32,17 +32,7 @@ public static function getInfo() { ...@@ -32,17 +32,7 @@ public static function getInfo() {
public function setUp() { public function setUp() {
parent::setUp(); parent::setUp();
// Enable web API for the watchdog resource. // Enable web API for the watchdog resource.
$config = config('rest'); $this->enableService('dblog');
$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');
} }
/** /**
...@@ -60,15 +50,16 @@ public function testWatchdog() { ...@@ -60,15 +50,16 @@ public function testWatchdog() {
$account = $this->drupalCreateUser(array('restful get dblog')); $account = $this->drupalCreateUser(array('restful get dblog'));
$this->drupalLogin($account); $this->drupalLogin($account);
$response = $this->httpRequest("dblog/$id", 'GET'); $response = $this->httpRequest("dblog/$id", 'GET', NULL, 'application/json');
$this->assertResponse(200); $this->assertResponse(200);
$this->assertHeader('Content-Type', 'application/json');
$log = drupal_json_decode($response); $log = drupal_json_decode($response);
$this->assertEqual($log['wid'], $id, 'Log ID is correct.'); $this->assertEqual($log['wid'], $id, 'Log ID is correct.');
$this->assertEqual($log['type'], 'rest_test', 'Type of log message 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.'); $this->assertEqual($log['message'], 'Test message', 'Log message text is correct.');
// Request an unknown log entry. // Request an unknown log entry.
$response = $this->httpRequest("dblog/9999", 'GET'); $response = $this->httpRequest("dblog/9999", 'GET', NULL, 'application/json');
$this->assertResponse(404); $this->assertResponse(404);
$this->assertEqual($response, 'Not Found', 'Response message is correct.'); $this->assertEqual($response, 'Not Found', 'Response message is correct.');
} }
......
...@@ -34,18 +34,7 @@ public static function getInfo() { ...@@ -34,18 +34,7 @@ public static function getInfo() {
*/ */
public function testDelete() { public function testDelete() {
foreach (entity_get_info() as $entity_type => $info) { foreach (entity_get_info() as $entity_type => $info) {
// Enable web API for this entity type. $this->enableService('entity:' . $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');
// Create a user account that has the required permissions to delete // Create a user account that has the required permissions to delete
// resources via the web API. // resources via the web API.
$account = $this->drupalCreateUser(array('restful delete entity:' . $entity_type)); $account = $this->drupalCreateUser(array('restful delete entity:' . $entity_type));
...@@ -81,6 +70,7 @@ public function testDelete() { ...@@ -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.'); $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. // Try to delete a resource which is not web API enabled.
$this->enableService(FALSE);
$account = $this->drupalCreateUser(); $account = $this->drupalCreateUser();
// Reset cURL here because it is confused from our previously used cURL // Reset cURL here because it is confused from our previously used cURL
// options. // options.
...@@ -88,32 +78,7 @@ public function testDelete() { ...@@ -88,32 +78,7 @@ public function testDelete() {
$this->drupalLogin($account); $this->drupalLogin($account);
$this->httpRequest('entity/user/' . $account->id(), 'DELETE'); $this->httpRequest('entity/user/' . $account->id(), 'DELETE');
$user = entity_load('user', $account->id(), TRUE); $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); $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 @@ ...@@ -14,6 +14,13 @@
*/ */
abstract class RESTTestBase extends WebTestBase { 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. * Helper function to issue a HTTP request with simpletest's cURL.
* *
...@@ -31,36 +38,136 @@ protected function httpRequest($url, $method, $body = NULL, $format = 'applicati ...@@ -31,36 +38,136 @@ protected function httpRequest($url, $method, $body = NULL, $format = 'applicati
case 'GET': case 'GET':
// Set query if there are additional GET parameters. // Set query if there are additional GET parameters.
$options = isset($body) ? array('absolute' => TRUE, 'query' => $body) : array('absolute' => TRUE); $options = isset($body) ? array('absolute' => TRUE, 'query' => $body) : array('absolute' => TRUE);
return $this->curlExec(array( $curl_options = array(
CURLOPT_HTTPGET => TRUE, CURLOPT_HTTPGET => TRUE,
CURLOPT_URL => url($url, $options), CURLOPT_URL => url($url, $options),
CURLOPT_NOBODY => FALSE) CURLOPT_NOBODY => FALSE
); );
break;
case 'POST': case 'POST':
return $this->curlExec(array( $curl_options = array(
CURLOPT_HTTPGET => FALSE, CURLOPT_HTTPGET => FALSE,
CURLOPT_POST => TRUE, CURLOPT_POST => TRUE,
CURLOPT_POSTFIELDS => $body, CURLOPT_POSTFIELDS => $body,
CURLOPT_URL => url($url, array('absolute' => TRUE)), CURLOPT_URL => url($url, array('absolute' => TRUE)),
CURLOPT_NOBODY => FALSE, CURLOPT_NOBODY => FALSE,
CURLOPT_HTTPHEADER => array('Content-Type: ' . $format), CURLOPT_HTTPHEADER => array('Content-Type: ' . $format),
)); );
break;
case 'PUT': case 'PUT':
return $this->curlExec(array( $curl_options = array(
CURLOPT_HTTPGET => FALSE, CURLOPT_HTTPGET => FALSE,
CURLOPT_CUSTOMREQUEST => 'PUT', CURLOPT_CUSTOMREQUEST => 'PUT',
CURLOPT_POSTFIELDS => $body, CURLOPT_POSTFIELDS => $body,
CURLOPT_URL => url($url, array('absolute' => TRUE)), CURLOPT_URL => url($url, array('absolute' => TRUE)),
CURLOPT_NOBODY => FALSE, CURLOPT_NOBODY => FALSE,
CURLOPT_HTTPHEADER => array('Content-Type: ' . $format), CURLOPT_HTTPHEADER => array('Content-Type: ' . $format),
)); );
break;
case 'DELETE': case 'DELETE':
return $this->curlExec(array( $curl_options = array(
CURLOPT_HTTPGET => FALSE, CURLOPT_HTTPGET => FALSE,
CURLOPT_CUSTOMREQUEST => 'DELETE', CURLOPT_CUSTOMREQUEST => 'DELETE',
CURLOPT_URL => url($url, array('absolute' => TRUE)), CURLOPT_URL => url($url, array('absolute' => TRUE)),
CURLOPT_NOBODY => FALSE, 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 ...@@ -3,4 +3,6 @@ description = Exposes entities and other resources as RESTful web API
package = Core package = Core
version = VERSION version = VERSION
core = 8.x core = 8.x
; @todo Remove this dependency once hard coding to JSON-LD is gone.
dependencies[] = jsonld
configure = admin/config/services/rest configure = admin/config/services/rest
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment