diff --git a/core/core.services.yml b/core/core.services.yml index 79f1fc1..4e2eb06 100644 --- a/core/core.services.yml +++ b/core/core.services.yml @@ -302,6 +302,7 @@ services: arguments: [16] mime_type_matcher: class: Drupal\Core\Routing\MimeTypeMatcher + arguments: ['@content_negotiation'] tags: - { name: route_filter } paramconverter_manager: diff --git a/core/lib/Drupal/Core/Routing/MimeTypeMatcher.php b/core/lib/Drupal/Core/Routing/MimeTypeMatcher.php index f77e254..36d313f 100644 --- a/core/lib/Drupal/Core/Routing/MimeTypeMatcher.php +++ b/core/lib/Drupal/Core/Routing/MimeTypeMatcher.php @@ -7,16 +7,33 @@ namespace Drupal\Core\Routing; +use Drupal\Core\ContentNegotiation; +use Symfony\Cmf\Component\Routing\NestedMatcher\RouteFilterInterface; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpKernel\Exception\NotAcceptableHttpException; use Symfony\Component\Routing\RouteCollection; -use Symfony\Cmf\Component\Routing\NestedMatcher\RouteFilterInterface; /** * This class filters routes based on the media type in HTTP Accept headers. */ class MimeTypeMatcher implements RouteFilterInterface { + /** + * The content negotiation library. + * + * @var \Drupal\Core\ContentNegotiation + */ + protected $contentNegotiation; + + /** + * Constructs a new MimeTypeMatcher. + * + * @param \Drupal\Core\ContentNegotiation $cotent_negotiation + * The content negotiation library. + */ + public function __construct(ContentNegotiation $content_negotiation) { + $this->contentNegotiation = $content_negotiation; + } /** * Implements \Symfony\Cmf\Component\Routing\NestedMatcher\RouteFilterInterface::filter() @@ -26,25 +43,43 @@ public function filter(RouteCollection $collection, Request $request) { // @todo replace by proper content negotiation library. $acceptable_mime_types = $request->getAcceptableContentTypes(); $acceptable_formats = array_map(array($request, 'getFormat'), $acceptable_mime_types); + $primary_format = $this->contentNegotiation->getContentType($request); - $filtered_collection = new RouteCollection(); + // Collect a list of routes that match the primary request content type. + $primary_matches = new RouteCollection(); + // List of routes that match any of multiple specified content types in the + // request. + $somehow_matches = new RouteCollection(); foreach ($collection as $name => $route) { // _format could be a |-delimited list of supported formats. $supported_formats = array_filter(explode('|', $route->getRequirement('_format'))); + + // HTML is the default format if the route does not specify it. + if (empty($supported_formats)) { + $supported_formats = array('html'); + } + + if (in_array($primary_format, $supported_formats)) { + $primary_matches->add($name, $route); + } // The route partially matches if it doesn't care about format, if it // explicitly allows any format, or if one of its allowed formats is // in the request's list of acceptable formats. - if (empty($supported_formats) || in_array('*/*', $acceptable_mime_types) || array_intersect($acceptable_formats, $supported_formats)) { - $filtered_collection->add($name, $route); + elseif (in_array('*/*', $acceptable_mime_types) || array_intersect($acceptable_formats, $supported_formats)) { + $somehow_matches->add($name, $route); } } - if (!count($filtered_collection)) { - throw new NotAcceptableHttpException(); + if (count($primary_matches)) { + return $primary_matches; + } + + if (count($somehow_matches)) { + return $somehow_matches; } - return $filtered_collection; + throw new NotAcceptableHttpException(); } } diff --git a/core/modules/rest/lib/Drupal/rest/Plugin/Derivative/EntityDerivative.php b/core/modules/rest/lib/Drupal/rest/Plugin/Derivative/EntityDerivative.php index 9bb2f99..2b9abd7 100644 --- a/core/modules/rest/lib/Drupal/rest/Plugin/Derivative/EntityDerivative.php +++ b/core/modules/rest/lib/Drupal/rest/Plugin/Derivative/EntityDerivative.php @@ -74,6 +74,18 @@ public function getDerivativeDefinitions(array $base_plugin_definition) { 'serialization_class' => $entity_info['class'], 'label' => $entity_info['label'], ); + // Use the entity links as REST URL patterns if available. + $this->derivatives[$entity_type]['links']['drupal:create'] = isset($entity_info['links']['drupal:create']) ? $entity_info['links']['drupal:create'] : "/entity/$entity_type"; + // Replace the default cannonical link pattern with a version that + // directly uses the entity type, because we don't want to hand 2 + // parameters to the entity resource plugin. + if ($entity_info['links']['canonical'] == '/entity/{entityType}/{id}') { + $this->derivatives[$entity_type]['links']['canonical'] = "/entity/$entity_type/{id}"; + } + else { + $this->derivatives[$entity_type]['links']['canonical'] = $entity_info['links']['canonical']; + } + $this->derivatives[$entity_type] += $base_plugin_definition; } } diff --git a/core/modules/rest/lib/Drupal/rest/Plugin/ResourceBase.php b/core/modules/rest/lib/Drupal/rest/Plugin/ResourceBase.php index 413cd8d..0af19f0 100644 --- a/core/modules/rest/lib/Drupal/rest/Plugin/ResourceBase.php +++ b/core/modules/rest/lib/Drupal/rest/Plugin/ResourceBase.php @@ -78,13 +78,17 @@ public function permissions() { */ public function routes() { $collection = new RouteCollection(); - $path_prefix = strtr($this->pluginId, ':', '/'); + + $definition = $this->getPluginDefinition(); + $canonical_path = isset($definition['links']['canonical']) ? $definition['links']['canonical'] : '/' . strtr($this->pluginId, ':', '/') . '/{id}'; + $create_path = isset($definition['links']['drupal:create']) ? $definition['links']['drupal:create'] : '/' . strtr($this->pluginId, ':', '/'); + $route_name = strtr($this->pluginId, ':', '.'); $methods = $this->availableMethods(); foreach ($methods as $method) { $lower_method = strtolower($method); - $route = new Route("/$path_prefix/{id}", array( + $route = new Route($canonical_path, array( '_controller' => 'Drupal\rest\RequestHandler::handle', // Pass the resource plugin ID along as default property. '_plugin' => $this->pluginId, @@ -97,7 +101,7 @@ public function routes() { switch ($method) { case 'POST': // POST routes do not require an ID in the URL path. - $route->setPattern("/$path_prefix"); + $route->setPattern($create_path); $route->addDefaults(array('id' => NULL)); $collection->add("$route_name.$method", $route); break; 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 2d2f353..a482b7d 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 @@ -25,7 +25,11 @@ * id = "entity", * label = @Translation("Entity"), * serialization_class = "Drupal\Core\Entity\Entity", - * derivative = "Drupal\rest\Plugin\Derivative\EntityDerivative" + * derivative = "Drupal\rest\Plugin\Derivative\EntityDerivative", + * links = { + * "canonical" = "/entity/{entity_type}/{entity}", + * "drupal:create" = "/entity/{entity_type}" + * } * ) */ class EntityResource extends ResourceBase { @@ -33,17 +37,20 @@ class EntityResource extends ResourceBase { /** * Responds to entity GET requests. * - * @param mixed $id - * The entity ID. + * @param mixed $entity + * The entity ID or the already upcasted entity object. * * @return \Drupal\rest\ResourceResponse * The response containing the loaded entity. * * @throws \Symfony\Component\HttpKernel\Exception\HttpException */ - public function get($id) { - $definition = $this->getPluginDefinition(); - $entity = entity_load($definition['entity_type'], $id); + public function get($entity) { + $id = $entity; + if (is_scalar($entity)) { + $definition = $this->getPluginDefinition(); + $entity = entity_load($definition['entity_type'], $entity); + } if ($entity) { if (!$entity->access('view')) { throw new AccessDeniedHttpException(); diff --git a/core/modules/rest/lib/Drupal/rest/RequestHandler.php b/core/modules/rest/lib/Drupal/rest/RequestHandler.php index 521d09e..23a1022 100644 --- a/core/modules/rest/lib/Drupal/rest/RequestHandler.php +++ b/core/modules/rest/lib/Drupal/rest/RequestHandler.php @@ -25,13 +25,12 @@ class RequestHandler extends ContainerAware { * * @param Symfony\Component\HttpFoundation\Request $request * The HTTP request object. - * @param mixed $id - * The resource ID. * * @return \Symfony\Component\HttpFoundation\Response * The response object. */ - public function handle(Request $request, $id = NULL) { + public function handle(Request $request) { + $plugin = $request->attributes->get(RouteObjectInterface::ROUTE_OBJECT)->getDefault('_plugin'); $method = strtolower($request->getMethod()); @@ -69,13 +68,24 @@ public function handle(Request $request, $id = NULL) { } } + // Determine the request parameters that should be passed to the resource + // plugin. + $route_parameters = $request->attributes->get('_route_params'); + $parameters = array(); + // Filter out all internal parameters starting with "_". + foreach ($route_parameters as $key => $parameter) { + if (strpos($key, '_') !== 0) { + $parameters[] = $parameter; + } + } + // Invoke the operation on the resource plugin. // 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 HAL. $format = $request->attributes->get(RouteObjectInterface::ROUTE_OBJECT)->getRequirement('_format') ?: 'hal_json'; try { - $response = $resource->{$method}($id, $unserialized, $request); + $response = call_user_func_array(array($resource, $method), array_merge($parameters, array($unserialized, $request))); } catch (HttpException $e) { $error['error'] = $e->getMessage(); diff --git a/core/modules/rest/lib/Drupal/rest/Tests/RESTTestBase.php b/core/modules/rest/lib/Drupal/rest/Tests/RESTTestBase.php index d6fc9f9..8d88ab1 100644 --- a/core/modules/rest/lib/Drupal/rest/Tests/RESTTestBase.php +++ b/core/modules/rest/lib/Drupal/rest/Tests/RESTTestBase.php @@ -295,4 +295,19 @@ protected function entityPermissions($entity_type, $operation) { } } } + + /** + * Returns the base URI path of an entity type. + * + * @param type $entity_type + * @return string + */ + protected function entityBasePath($entity_type) { + switch ($entity_type) { + case 'entity_test': + return 'entity/entity_test'; + case 'node': + return 'node'; + } + } } diff --git a/core/modules/rest/lib/Drupal/rest/Tests/ReadTest.php b/core/modules/rest/lib/Drupal/rest/Tests/ReadTest.php index 7c84cfd..1052c2c 100644 --- a/core/modules/rest/lib/Drupal/rest/Tests/ReadTest.php +++ b/core/modules/rest/lib/Drupal/rest/Tests/ReadTest.php @@ -51,7 +51,7 @@ public function testRead() { $entity = $this->entityCreate($entity_type); $entity->save(); // Read it over the REST API. - $response = $this->httpRequest('entity/' . $entity_type . '/' . $entity->id(), 'GET', NULL, $this->defaultMimeType); + $response = $this->httpRequest($this->entityBasePath($entity_type) . '/' . $entity->id(), 'GET', NULL, $this->defaultMimeType); $this->assertResponse('200', 'HTTP response code is correct.'); $this->assertHeader('content-type', $this->defaultMimeType); $data = drupal_json_decode($response); @@ -60,11 +60,11 @@ public function testRead() { $this->assertEqual($data['uuid'][0]['value'], $entity->uuid(), 'Entity UUID is correct'); // Try to read the entity with an unsupported mime format. - $response = $this->httpRequest('entity/' . $entity_type . '/' . $entity->id(), 'GET', NULL, 'application/wrongformat'); + $response = $this->httpRequest($this->entityBasePath($entity_type) . '/' . $entity->id(), 'GET', NULL, 'application/wrongformat'); $this->assertResponse(406); // Try to read an entity that does not exist. - $response = $this->httpRequest('entity/' . $entity_type . '/9999', 'GET', NULL, $this->defaultMimeType); + $response = $this->httpRequest($this->entityBasePath($entity_type) . '/9999', 'GET', NULL, $this->defaultMimeType); $this->assertResponse(404); $decoded = drupal_json_decode($response); $this->assertEqual($decoded['error'], 'Entity with ID 9999 not found', 'Response message is correct.'); @@ -75,7 +75,7 @@ public function testRead() { if ($entity_type == 'entity_test') { $entity->field_test_text->value = 'no access value'; $entity->save(); - $response = $this->httpRequest('entity/' . $entity_type . '/' . $entity->id(), 'GET', NULL, $this->defaultMimeType); + $response = $this->httpRequest($this->entityBasePath($entity_type) . '/' . $entity->id(), 'GET', NULL, $this->defaultMimeType); $this->assertResponse(200); $this->assertHeader('content-type', $this->defaultMimeType); $data = drupal_json_decode($response); @@ -84,7 +84,7 @@ public function testRead() { // Try to read an entity without proper permissions. $this->drupalLogout(); - $response = $this->httpRequest('entity/' . $entity_type . '/' . $entity->id(), 'GET', NULL, $this->defaultMimeType); + $response = $this->httpRequest($this->entityBasePath($entity_type) . '/' . $entity->id(), 'GET', NULL, $this->defaultMimeType); $this->assertResponse(403); $this->assertNull(drupal_json_decode($response), 'No valid JSON found.'); }