Issue #1850734 by klausi: Make serialization formats configurable.
parent
00dfa02242
commit
73f3973425
|
@ -54,15 +54,30 @@ class RouteSubscriber implements EventSubscriberInterface {
|
|||
public function dynamicRoutes(RouteBuildEvent $event) {
|
||||
|
||||
$collection = $event->getRouteCollection();
|
||||
$enabled_resources = $this->config->get('rest.settings')->load()->get('resources');
|
||||
|
||||
$resources = $this->config->get('rest.settings')->load()->get('resources');
|
||||
if ($resources && $enabled = array_intersect_key($this->manager->getDefinitions(), $resources)) {
|
||||
foreach ($enabled as $key => $resource) {
|
||||
$plugin = $this->manager->getInstance(array('id' => $key));
|
||||
// Iterate over all enabled resource plugins.
|
||||
foreach ($enabled_resources as $id => $enabled_methods) {
|
||||
$plugin = $this->manager->getInstance(array('id' => $id));
|
||||
|
||||
foreach ($plugin->routes() as $name => $route) {
|
||||
foreach ($plugin->routes() as $name => $route) {
|
||||
$method = $route->getRequirement('_method');
|
||||
// Only expose routes where the method is enabled in the configuration.
|
||||
if ($method && isset($enabled_methods[$method])) {
|
||||
$route->setRequirement('_access_rest_csrf', 'TRUE');
|
||||
$collection->add("rest.$name", $route);
|
||||
|
||||
// If the array of configured format restrictions is empty for a
|
||||
// method always add the route.
|
||||
if (empty($enabled_methods[$method])) {
|
||||
$collection->add("rest.$name", $route);
|
||||
continue;
|
||||
}
|
||||
// If there is no format requirement or if it matches the
|
||||
// configuration also add the route.
|
||||
$format_requirement = $route->getRequirement('_format');
|
||||
if (!$format_requirement || isset($enabled_methods[$method][$format_requirement])) {
|
||||
$collection->add("rest.$name", $route);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -26,15 +26,11 @@ abstract class ResourceBase extends PluginBase implements ResourceInterface {
|
|||
public function permissions() {
|
||||
$permissions = array();
|
||||
$definition = $this->getDefinition();
|
||||
foreach ($this->requestMethods() as $method) {
|
||||
foreach ($this->availableMethods() as $method) {
|
||||
$lowered_method = strtolower($method);
|
||||
// Only expose permissions where the HTTP request method exists on the
|
||||
// plugin.
|
||||
if (method_exists($this, $lowered_method)) {
|
||||
$permissions["restful $lowered_method $this->plugin_id"] = array(
|
||||
'title' => t('Access @method on %label resource', array('@method' => $method, '%label' => $definition['label'])),
|
||||
);
|
||||
}
|
||||
$permissions["restful $lowered_method $this->plugin_id"] = array(
|
||||
'title' => t('Access @method on %label resource', array('@method' => $method, '%label' => $definition['label'])),
|
||||
);
|
||||
}
|
||||
return $permissions;
|
||||
}
|
||||
|
@ -47,38 +43,44 @@ abstract class ResourceBase extends PluginBase implements ResourceInterface {
|
|||
$path_prefix = strtr($this->plugin_id, ':', '/');
|
||||
$route_name = strtr($this->plugin_id, ':', '.');
|
||||
|
||||
$methods = $this->requestMethods();
|
||||
$methods = $this->availableMethods();
|
||||
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)) {
|
||||
$route = new Route("/$path_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",
|
||||
));
|
||||
$route = new Route("/$path_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",
|
||||
));
|
||||
|
||||
switch ($method) {
|
||||
case 'POST':
|
||||
// POST routes do not require an ID in the URL path.
|
||||
$route->setPattern("/$path_prefix");
|
||||
$route->addDefaults(array('id' => NULL));
|
||||
break;
|
||||
switch ($method) {
|
||||
case 'POST':
|
||||
// POST routes do not require an ID in the URL path.
|
||||
$route->setPattern("/$path_prefix");
|
||||
$route->addDefaults(array('id' => NULL));
|
||||
$collection->add("$route_name.$method", $route);
|
||||
break;
|
||||
|
||||
case 'GET':
|
||||
case 'HEAD':
|
||||
// Restrict GET and HEAD requests to the media type specified in the
|
||||
// HTTP Accept headers.
|
||||
// @todo Replace hard coded format here with available formats.
|
||||
$route->addRequirements(array('_format' => 'drupal_jsonld'));
|
||||
break;
|
||||
}
|
||||
case 'GET':
|
||||
case 'HEAD':
|
||||
// Restrict GET and HEAD requests to the media type specified in the
|
||||
// HTTP Accept headers.
|
||||
$formats = drupal_container()->getParameter('serializer.formats');
|
||||
foreach ($formats as $format_name => $label) {
|
||||
// Expose one route per available format.
|
||||
//$format_route = new Route($route->getPattern(), $route->getDefaults(), $route->getRequirements());
|
||||
$format_route = clone $route;
|
||||
$format_route->addRequirements(array('_format' => $format_name));
|
||||
$collection->add("$route_name.$method.$format_name", $format_route);
|
||||
}
|
||||
break;
|
||||
|
||||
$collection->add("$route_name.$method", $route);
|
||||
default:
|
||||
$collection->add("$route_name.$method", $route);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -95,7 +97,7 @@ abstract class ResourceBase extends PluginBase implements ResourceInterface {
|
|||
* The list of allowed HTTP request method strings.
|
||||
*/
|
||||
protected function requestMethods() {
|
||||
return drupal_map_assoc(array(
|
||||
return array(
|
||||
'HEAD',
|
||||
'GET',
|
||||
'POST',
|
||||
|
@ -105,6 +107,21 @@ abstract class ResourceBase extends PluginBase implements ResourceInterface {
|
|||
'OPTIONS',
|
||||
'CONNECT',
|
||||
'PATCH',
|
||||
));
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements ResourceInterface::availableMethods().
|
||||
*/
|
||||
public function availableMethods() {
|
||||
$methods = $this->requestMethods();
|
||||
$available = array();
|
||||
foreach ($methods as $method) {
|
||||
// Only expose methods where the HTTP request method exists on the plugin.
|
||||
if (method_exists($this, strtolower($method))) {
|
||||
$available[] = $method;
|
||||
}
|
||||
}
|
||||
return $available;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -36,4 +36,12 @@ interface ResourceInterface extends PluginInspectionInterface {
|
|||
*/
|
||||
public function permissions();
|
||||
|
||||
/**
|
||||
* Returns the available HTTP request methods on this plugin.
|
||||
*
|
||||
* @return array
|
||||
* The list of supported methods. Example: array('GET', 'POST', 'PATCH').
|
||||
*/
|
||||
public function availableMethods();
|
||||
|
||||
}
|
||||
|
|
|
@ -50,12 +50,7 @@ class DBLogResource extends ResourceBase {
|
|||
$record = db_query("SELECT * FROM {watchdog} WHERE wid = :wid", array(':wid' => $id))
|
||||
->fetchObject();
|
||||
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/vnd.drupal.ld+json'));
|
||||
// @todo remove hard coded format here.
|
||||
$response->setContent(drupal_json_encode($record));
|
||||
return $response;
|
||||
return new ResourceResponse((array) $record);
|
||||
}
|
||||
}
|
||||
throw new NotFoundHttpException(t('Log entry with ID @id was not found', array('@id' => $id)));
|
||||
|
|
|
@ -12,6 +12,7 @@ use Symfony\Component\DependencyInjection\ContainerAware;
|
|||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\HttpKernel\Exception\HttpException;
|
||||
use Symfony\Component\HttpKernel\Exception\UnsupportedMediaTypeHttpException;
|
||||
use Symfony\Component\Serializer\Exception\UnexpectedValueException;
|
||||
|
||||
/**
|
||||
|
@ -43,17 +44,28 @@ class RequestHandler extends ContainerAware {
|
|||
$received = $request->getContent();
|
||||
$unserialized = NULL;
|
||||
if (!empty($received)) {
|
||||
$definition = $resource->getDefinition();
|
||||
$class = $definition['serialization_class'];
|
||||
try {
|
||||
// @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');
|
||||
$format = $request->getContentType();
|
||||
|
||||
// Only allow serialization formats that are explicitly configured. If no
|
||||
// formats are configured allow all and hope that the serializer knows the
|
||||
// format. If the serializer cannot handle it an exception will be thrown
|
||||
// that bubbles up to the client.
|
||||
$config = $this->container->get('config.factory')->get('rest.settings')->get('resources');
|
||||
$enabled_formats = $config[$plugin][$request->getMethod()];
|
||||
if (empty($enabled_formats) || isset($enabled_formats[$format])) {
|
||||
$definition = $resource->getDefinition();
|
||||
$class = $definition['serialization_class'];
|
||||
try {
|
||||
$unserialized = $serializer->deserialize($received, $class, $format);
|
||||
}
|
||||
catch (UnexpectedValueException $e) {
|
||||
$error['error'] = $e->getMessage();
|
||||
$content = $serializer->serialize($error, $format);
|
||||
return new Response($content, 400, array('Content-Type' => $request->getMimeType($format)));
|
||||
}
|
||||
}
|
||||
catch (UnexpectedValueException $e) {
|
||||
$error['error'] = $e->getMessage();
|
||||
$content = $serializer->serialize($error, 'drupal_jsonld');
|
||||
return new Response($content, 400, array('Content-Type' => 'application/vnd.drupal.ld+json'));
|
||||
else {
|
||||
throw new UnsupportedMediaTypeHttpException();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -63,21 +75,26 @@ class RequestHandler extends ContainerAware {
|
|||
}
|
||||
catch (HttpException $e) {
|
||||
$error['error'] = $e->getMessage();
|
||||
$content = $serializer->serialize($error, 'drupal_jsonld');
|
||||
$format = $request->attributes->get(RouteObjectInterface::ROUTE_OBJECT)->getRequirement('_format') ?: 'drupal_jsonld';
|
||||
$content = $serializer->serialize($error, $format);
|
||||
// Add the default content type, but only if the headers from the
|
||||
// exception have not specified it already.
|
||||
$headers = $e->getHeaders() + array('Content-Type' => 'application/vnd.drupal.ld+json');
|
||||
$headers = $e->getHeaders() + array('Content-Type' => $request->getMimeType($format));
|
||||
return new Response($content, $e->getStatusCode(), $headers);
|
||||
}
|
||||
|
||||
// Serialize the outgoing data for the response, if available.
|
||||
$data = $response->getResponseData();
|
||||
if ($data != NULL) {
|
||||
// @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');
|
||||
// All REST routes are restricted to exactly one format, so instead of
|
||||
// parsing it out of the Accept headers again we can simply retrieve the
|
||||
// format requirement. If there is no format associated just pick Drupal
|
||||
// JSON-LD.
|
||||
$format = $request->attributes->get(RouteObjectInterface::ROUTE_OBJECT)->getRequirement('_format') ?: 'drupal_jsonld';
|
||||
|
||||
$output = $serializer->serialize($data, $format);
|
||||
$response->setContent($output);
|
||||
$response->headers->set('Content-Type', 'application/vnd.drupal.ld+json');
|
||||
$response->headers->set('Content-Type', $request->getMimeType($format));
|
||||
}
|
||||
return $response;
|
||||
}
|
||||
|
|
|
@ -38,7 +38,7 @@ class CreateTest extends RESTTestBase {
|
|||
// entity types here as well.
|
||||
$entity_type = 'entity_test';
|
||||
|
||||
$this->enableService('entity:' . $entity_type);
|
||||
$this->enableService('entity:' . $entity_type, 'POST');
|
||||
// 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));
|
||||
|
|
|
@ -36,7 +36,7 @@ class DeleteTest extends RESTTestBase {
|
|||
// Define the entity types we want to test.
|
||||
$entity_types = array('entity_test', 'node', 'user');
|
||||
foreach ($entity_types as $entity_type) {
|
||||
$this->enableService('entity:' . $entity_type);
|
||||
$this->enableService('entity:' . $entity_type, 'DELETE');
|
||||
// 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));
|
||||
|
|
|
@ -156,18 +156,24 @@ abstract class RESTTestBase extends WebTestBase {
|
|||
* @param string|FALSE $resource_type
|
||||
* The resource type that should get web API enabled or FALSE to disable all
|
||||
* resource types.
|
||||
* @param string $method
|
||||
* The HTTP method to enable, e.g. GET, POST etc.
|
||||
* @param string $format
|
||||
* (Optional) The serialization format, e.g. jsonld.
|
||||
*/
|
||||
protected function enableService($resource_type) {
|
||||
protected function enableService($resource_type, $method = 'GET', $format = NULL) {
|
||||
// Enable web API for this entity type.
|
||||
$config = config('rest.settings');
|
||||
$settings = array();
|
||||
if ($resource_type) {
|
||||
$config->set('resources', array(
|
||||
$resource_type => $resource_type,
|
||||
));
|
||||
}
|
||||
else {
|
||||
$config->set('resources', array());
|
||||
if ($format) {
|
||||
$settings[$resource_type][$method][$format] = 'TRUE';
|
||||
}
|
||||
else {
|
||||
$settings[$resource_type][$method] = array();
|
||||
}
|
||||
}
|
||||
$config->set('resources', $settings);
|
||||
$config->save();
|
||||
|
||||
// Rebuild routing cache, so that the web API paths are available.
|
||||
|
|
|
@ -38,7 +38,7 @@ class ReadTest extends RESTTestBase {
|
|||
// Define the entity types we want to test.
|
||||
$entity_types = array('entity_test');
|
||||
foreach ($entity_types as $entity_type) {
|
||||
$this->enableService('entity:' . $entity_type);
|
||||
$this->enableService('entity:' . $entity_type, 'GET');
|
||||
// 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));
|
||||
|
@ -57,12 +57,8 @@ class ReadTest extends RESTTestBase {
|
|||
$this->assertEqual($data['uuid'][LANGUAGE_DEFAULT][0]['value'], $entity->uuid(), 'Entity UUID is correct');
|
||||
|
||||
// Try to read the entity with an unsupported mime format.
|
||||
// Because the matcher checks mime type first, then method, this will hit
|
||||
// zero viable routes on the method. If the mime matcher wasn't working,
|
||||
// we would still find an existing GET route with the wrong format. That
|
||||
// means this is a valid functional test for mime-matching.
|
||||
$response = $this->httpRequest('entity/' . $entity_type . '/' . $entity->id(), 'GET', NULL, 'application/wrongformat');
|
||||
$this->assertResponse(405);
|
||||
$this->assertResponse(415);
|
||||
|
||||
// Try to read an entity that does not exist.
|
||||
$response = $this->httpRequest('entity/' . $entity_type . '/9999', 'GET', NULL, 'application/vnd.drupal.ld+json');
|
||||
|
|
|
@ -38,7 +38,7 @@ class UpdateTest extends RESTTestBase {
|
|||
// entity types here as well.
|
||||
$entity_type = 'entity_test';
|
||||
|
||||
$this->enableService('entity:' . $entity_type);
|
||||
$this->enableService('entity:' . $entity_type, 'PATCH');
|
||||
// Create a user account that has the required permissions to create
|
||||
// resources via the web API.
|
||||
$account = $this->drupalCreateUser(array('restful patch entity:' . $entity_type));
|
||||
|
@ -103,7 +103,7 @@ class UpdateTest extends RESTTestBase {
|
|||
// entity types here as well.
|
||||
$entity_type = 'entity_test';
|
||||
|
||||
$this->enableService('entity:' . $entity_type);
|
||||
$this->enableService('entity:' . $entity_type, 'PUT');
|
||||
// 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));
|
||||
|
|
|
@ -34,7 +34,10 @@ function rest_admin_form($form, &$form_state) {
|
|||
}
|
||||
asort($entity_resources);
|
||||
asort($other_resources);
|
||||
$enabled_resources = config('rest.settings')->get('resources') ?: array();
|
||||
$config = config('rest.settings')->get('resources') ?: array();
|
||||
// Strip out the nested method configuration, we are only interested in the
|
||||
// plugin IDs of the resources.
|
||||
$enabled_resources = drupal_map_assoc(array_keys($config));
|
||||
|
||||
// Render the output using table_select().
|
||||
$header = array(
|
||||
|
@ -79,9 +82,19 @@ function rest_admin_form($form, &$form_state) {
|
|||
* Form submission handler for rest_admin_form().
|
||||
*/
|
||||
function rest_admin_form_submit($form, &$form_state) {
|
||||
$resources = array_filter($form_state['values']['entity_resources']);
|
||||
$enabled_resources = array_filter($form_state['values']['entity_resources']);
|
||||
if (!empty($form_state['values']['other_resources'])) {
|
||||
$resources += array_filter($form_state['values']['other_resources']);
|
||||
$enabled_resources += array_filter($form_state['values']['other_resources']);
|
||||
}
|
||||
$resources = array();
|
||||
$plugin_manager = drupal_container()->get('plugin.manager.rest');
|
||||
|
||||
// Enable all methods and all formats for each selected resource.
|
||||
foreach ($enabled_resources as $resource) {
|
||||
$plugin = $plugin_manager->getInstance(array('id' => $resource));
|
||||
$methods = $plugin->availableMethods();
|
||||
// An empty array means all formats are allowed for a method.
|
||||
$resources[$resource] = array_fill_keys($methods, array());
|
||||
}
|
||||
|
||||
$config = config('rest.settings');
|
||||
|
|
Loading…
Reference in New Issue