diff --git a/core/core.services.yml b/core/core.services.yml index 24ef51e..25534d1 100644 --- a/core/core.services.yml +++ b/core/core.services.yml @@ -300,8 +300,13 @@ services: password: class: Drupal\Core\Password\PhpassHashedPassword arguments: [16] - mime_type_matcher: - class: Drupal\Core\Routing\MimeTypeMatcher + accept_header_matcher: + class: Drupal\Core\Routing\AcceptHeaderMatcher + arguments: ['@content_negotiation'] + tags: + - { name: route_filter } + content_type_header_matcher: + class: Drupal\Core\Routing\ContentTypeHeaderMatcher tags: - { name: route_filter } paramconverter_manager: @@ -365,6 +370,10 @@ services: class: Drupal\Core\EventSubscriber\SpecialAttributesRouteSubscriber tags: - { name: event_subscriber } + route_http_method_subscriber: + class: Drupal\Core\EventSubscriber\RouteMethodSubscriber + tags: + - { name: event_subscriber } controller.page: class: Drupal\Core\Controller\HtmlPageController arguments: ['@http_kernel', '@controller_resolver', '@string_translation', '@title_resolver'] diff --git a/core/lib/Drupal/Core/Controller/ExceptionController.php b/core/lib/Drupal/Core/Controller/ExceptionController.php index 66a6215..61a84e9 100644 --- a/core/lib/Drupal/Core/Controller/ExceptionController.php +++ b/core/lib/Drupal/Core/Controller/ExceptionController.php @@ -57,7 +57,7 @@ public function execute(FlattenException $exception, Request $request) { return $this->$method($exception, $request); } - return new Response('A fatal error occurred: ' . $exception->getMessage(), $exception->getStatusCode(), $exception->getHeaders()); + return new Response('An error occurred: ' . $exception->getMessage(), $exception->getStatusCode(), $exception->getHeaders()); } /** diff --git a/core/lib/Drupal/Core/EventSubscriber/RouteMethodSubscriber.php b/core/lib/Drupal/Core/EventSubscriber/RouteMethodSubscriber.php new file mode 100644 index 0000000..19b5488 --- /dev/null +++ b/core/lib/Drupal/Core/EventSubscriber/RouteMethodSubscriber.php @@ -0,0 +1,48 @@ +getRouteCollection() as $route) { + $methods = $route->getMethods(); + if (empty($methods)) { + $route->setMethods(array('GET', 'POST')); + } + } + } + + /** + * {@inheritdoc} + */ + static function getSubscribedEvents() { + $events[RoutingEvents::ALTER][] = 'onRouteBuilding'; + return $events; + } + +} diff --git a/core/lib/Drupal/Core/ParamConverter/ParamConverterManager.php b/core/lib/Drupal/Core/ParamConverter/ParamConverterManager.php index cad587b..068162b 100644 --- a/core/lib/Drupal/Core/ParamConverter/ParamConverterManager.php +++ b/core/lib/Drupal/Core/ParamConverter/ParamConverterManager.php @@ -186,7 +186,10 @@ public function enhance(array $defaults, Request $request) { // converted in which case we throw a 404. $defaults[$name] = $this->getConverter($definition['converter'])->convert($defaults[$name], $definition, $name, $defaults, $request); if (!isset($defaults[$name])) { - throw new NotFoundHttpException(); + throw new NotFoundHttpException(format_string('Item "@name" with ID @id not found', array( + '@name' => $name, + '@id' => $defaults['_raw_variables']->get($name), + ))); } } diff --git a/core/lib/Drupal/Core/Routing/AcceptHeaderMatcher.php b/core/lib/Drupal/Core/Routing/AcceptHeaderMatcher.php new file mode 100644 index 0000000..7ebc4d3 --- /dev/null +++ b/core/lib/Drupal/Core/Routing/AcceptHeaderMatcher.php @@ -0,0 +1,91 @@ +contentNegotiation = $content_negotiation; + } + + /** + * {@inheritdoc} + */ + public function filter(RouteCollection $collection, Request $request) { + // Generates a list of Symfony formats matching the acceptable MIME types. + // @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); + + // 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'))); + + if (in_array($primary_format, $supported_formats)) { + $primary_matches->add($name, $route); + continue; + } + + // HTML is the default format if the route does not specify it. We also + // add the other Drupal AJAX and JSON formats here to cover general use + // cases. + if (empty($supported_formats)) { + $supported_formats = array('html', 'drupal_ajax', 'drupal_modal', 'drupal_dialog', 'json'); + } + // 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 (in_array('*/*', $acceptable_mime_types) || array_intersect($acceptable_formats, $supported_formats)) { + $somehow_matches->add($name, $route); + } + } + + if (count($primary_matches)) { + return $primary_matches; + } + + if (count($somehow_matches)) { + return $somehow_matches; + } + + // We do not throw a + // \Symfony\Component\Routing\Exception\ResourceNotFoundException here + // because we don't want to return a 404 status code, but rather a 406. + throw new NotAcceptableHttpException(format_string('No route found for the specified formats @formats.', array('@formats' => implode(' ', $acceptable_mime_types)))); + } + +} diff --git a/core/lib/Drupal/Core/Routing/ContentTypeHeaderMatcher.php b/core/lib/Drupal/Core/Routing/ContentTypeHeaderMatcher.php new file mode 100644 index 0000000..04bd40b --- /dev/null +++ b/core/lib/Drupal/Core/Routing/ContentTypeHeaderMatcher.php @@ -0,0 +1,58 @@ +getMethod() == 'GET') { + return $collection; + } + + $format = $request->getContentType(); + if ($format === NULL) { + // Even if the request has no Content-type header we initialize it here + // with a default so that we can filter out routes that require a + // different one later. + $format = 'html'; + } + foreach ($collection as $name => $route) { + $supported_formats = array_filter(explode('|', $route->getRequirement('_content_type_format'))); + if (empty($supported_formats)) { + // The route has not specified any Content-Type restrictions, so we + // assume default restrictions. + $supported_formats = array('html', 'drupal_ajax', 'drupal_modal', 'drupal_dialog'); + } + if (!in_array($format, $supported_formats)) { + $collection->remove($name); + } + } + if (count($collection)) { + return $collection; + } + // We do not throw a + // \Symfony\Component\Routing\Exception\ResourceNotFoundException here + // because we don't want to return a 404 status code, but rather a 400. + throw new BadRequestHttpException('No route found that matches the Content-Type header.'); + } + +} diff --git a/core/lib/Drupal/Core/Routing/MimeTypeMatcher.php b/core/lib/Drupal/Core/Routing/MimeTypeMatcher.php deleted file mode 100644 index f77e254..0000000 --- a/core/lib/Drupal/Core/Routing/MimeTypeMatcher.php +++ /dev/null @@ -1,50 +0,0 @@ -getAcceptableContentTypes(); - $acceptable_formats = array_map(array($request, 'getFormat'), $acceptable_mime_types); - - $filtered_collection = new RouteCollection(); - - foreach ($collection as $name => $route) { - // _format could be a |-delimited list of supported formats. - $supported_formats = array_filter(explode('|', $route->getRequirement('_format'))); - // 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); - } - } - - if (!count($filtered_collection)) { - throw new NotAcceptableHttpException(); - } - - return $filtered_collection; - } - -} diff --git a/core/modules/hal/lib/Drupal/hal/HalSubscriber.php b/core/modules/hal/lib/Drupal/hal/HalSubscriber.php index 93a70bb..b770a74 100644 --- a/core/modules/hal/lib/Drupal/hal/HalSubscriber.php +++ b/core/modules/hal/lib/Drupal/hal/HalSubscriber.php @@ -34,7 +34,8 @@ public function onKernelRequest(GetResponseEvent $event) { * An array of event listener definitions. */ static function getSubscribedEvents() { - $events[KernelEvents::REQUEST][] = array('onKernelRequest', 40); + // The format must be available before routing, so we make the priority big. + $events[KernelEvents::REQUEST][] = array('onKernelRequest', 4000); return $events; } 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..6f2ac07 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, @@ -96,9 +100,14 @@ public function routes() { switch ($method) { case 'POST': - // POST routes do not require an ID in the URL path. - $route->setPattern("/$path_prefix"); - $route->addDefaults(array('id' => NULL)); + $route->setPattern($create_path); + // Do not break here, fall through to PATCH additions which also apply + // to POST. + + case 'PATCH': + // Restrict the incoming HTTP Content-type header to the known + // serialization formats. + $route->addRequirements(array('_content_type_format' => implode('|', $this->serializerFormats))); $collection->add("$route_name.$method", $route); break; @@ -108,7 +117,6 @@ public function routes() { // HTTP Accept headers. foreach ($this->serializerFormats as $format_name) { // Expose one route per available format. - //$format_route = new Route($route->getPath(), $route->getDefaults(), $route->getRequirements()); $format_route = clone $route; $format_route->addRequirements(array('_format' => $format_name)); $collection->add("$route_name.$method.$format_name", $format_route); 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..e3f9f36 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(); @@ -61,8 +68,6 @@ public function get($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. * @@ -71,7 +76,7 @@ public function get($id) { * * @throws \Symfony\Component\HttpKernel\Exception\HttpException */ - public function post($id, EntityInterface $entity = NULL) { + public function post(EntityInterface $entity = NULL) { if ($entity == NULL) { throw new BadRequestHttpException(t('No entity content received.')); } @@ -114,8 +119,8 @@ public function post($id, EntityInterface $entity = NULL) { /** * Responds to entity PATCH requests. * - * @param mixed $id - * The entity ID. + * @param mixed $original_entity + * The entity ID or the already upcasted original entity object. * @param \Drupal\Core\Entity\EntityInterface $entity * The entity. * @@ -124,19 +129,21 @@ public function post($id, EntityInterface $entity = NULL) { * * @throws \Symfony\Component\HttpKernel\Exception\HttpException */ - public function patch($id, EntityInterface $entity = NULL) { + public function patch($original_entity, EntityInterface $entity = NULL) { if ($entity == NULL) { throw new BadRequestHttpException(t('No entity content received.')); } - if (empty($id)) { + if (empty($original_entity)) { throw new NotFoundHttpException(); } $definition = $this->getPluginDefinition(); if ($entity->entityType() != $definition['entity_type']) { throw new BadRequestHttpException(t('Invalid entity type')); } - $original_entity = entity_load($definition['entity_type'], $id); + if (is_scalar($original_entity)) { + $original_entity = entity_load($definition['entity_type'], $original_entity); + } // We don't support creating entities with PATCH, so we throw an error if // there is no existing entity. if ($original_entity == FALSE) { @@ -176,17 +183,20 @@ public function patch($id, EntityInterface $entity = NULL) { /** * Responds to entity DELETE requests. * - * @param mixed $id - * The entity ID. + * @param mixed $entity + * The entity ID or the already upcasted entity object. * * @return \Drupal\rest\ResourceResponse * The HTTP response object. * * @throws \Symfony\Component\HttpKernel\Exception\HttpException */ - public function delete($id) { - $definition = $this->getPluginDefinition(); - $entity = entity_load($definition['entity_type'], $id); + public function delete($entity) { + $id = $entity; + if (is_scalar($entity)) { + $definition = $this->getPluginDefinition(); + $entity = entity_load($definition['entity_type'], $id); + } if ($entity) { if (!$entity->access('delete')) { 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/AuthTest.php b/core/modules/rest/lib/Drupal/rest/Tests/AuthTest.php index 7c40096..7cb1b9a 100644 --- a/core/modules/rest/lib/Drupal/rest/Tests/AuthTest.php +++ b/core/modules/rest/lib/Drupal/rest/Tests/AuthTest.php @@ -48,7 +48,7 @@ public function testRead() { // Try to read the resource as an anonymous user, which should not work. $response = $this->httpRequest('entity/' . $entity_type . '/' . $entity->id(), 'GET', NULL, $this->defaultMimeType); $this->assertResponse('401', 'HTTP response code is 401 when the request is not authenticated and the user is anonymous.'); - $this->assertText('A fatal error occurred: No authentication credentials provided.'); + $this->assertText('An error occurred: No authentication credentials provided.'); // Create a user account that has the required permissions to read // resources via the REST API, but the request is authenticated diff --git a/core/modules/rest/lib/Drupal/rest/Tests/DeleteTest.php b/core/modules/rest/lib/Drupal/rest/Tests/DeleteTest.php index 10695c4..62341f9 100644 --- a/core/modules/rest/lib/Drupal/rest/Tests/DeleteTest.php +++ b/core/modules/rest/lib/Drupal/rest/Tests/DeleteTest.php @@ -50,7 +50,7 @@ public function testDelete() { $entity = $this->entityCreate($entity_type); $entity->save(); // Delete it over the REST API. - $response = $this->httpRequest('entity/' . $entity_type . '/' . $entity->id(), 'DELETE'); + $response = $this->httpRequest($this->entityBasePath($entity_type) . '/' . $entity->id(), 'DELETE'); // Clear the static cache with entity_load(), otherwise we won't see the // update. $entity = entity_load($entity_type, $entity->id(), TRUE); @@ -59,17 +59,22 @@ public function testDelete() { $this->assertEqual($response, '', 'Response body is empty.'); // Try to delete an entity that does not exist. - $response = $this->httpRequest('entity/' . $entity_type . '/9999', 'DELETE'); + $response = $this->httpRequest($this->entityBasePath($entity_type) . '/9999', 'DELETE'); $this->assertResponse(404); - $decoded = drupal_json_decode($response); - $this->assertEqual($decoded['error'], 'Entity with ID 9999 not found', 'Response message is correct.'); + if ($entity_type == 'entity_test') { + $decoded = drupal_json_decode($response); + $this->assertEqual($decoded['error'], 'Entity with ID 9999 not found', 'Response message is correct.'); + } + else { + $this->assertText('The requested page "/node/9999" could not be found.'); + } // Try to delete an entity without proper permissions. $this->drupalLogout(); // Re-save entity to the database. $entity = $this->entityCreate($entity_type); $entity->save(); - $this->httpRequest('entity/' . $entity_type . '/' . $entity->id(), 'DELETE'); + $this->httpRequest($this->entityBasePath($entity_type) . '/' . $entity->id(), 'DELETE'); $this->assertResponse(403); $this->assertNotIdentical(FALSE, entity_load($entity_type, $entity->id(), TRUE), 'The ' . $entity_type . ' entity is still in the database.'); } diff --git a/core/modules/rest/lib/Drupal/rest/Tests/NodeTest.php b/core/modules/rest/lib/Drupal/rest/Tests/NodeTest.php index 76773df..56b7188 100644 --- a/core/modules/rest/lib/Drupal/rest/Tests/NodeTest.php +++ b/core/modules/rest/lib/Drupal/rest/Tests/NodeTest.php @@ -55,7 +55,7 @@ public function testNodes() { $node = $this->entityCreate('node'); $node->save(); - $this->httpRequest('entity/node/' . $node->id(), 'GET', NULL, $this->defaultMimeType); + $this->httpRequest('node/' . $node->id(), 'GET', NULL, $this->defaultMimeType); $this->assertResponse(200); // Check that a simple PATCH update to the node title works as expected. @@ -76,7 +76,7 @@ public function testNodes() { ), ); $serialized = $this->container->get('serializer')->serialize($data, $this->defaultFormat); - $this->httpRequest('entity/node/' . $node->id(), 'PATCH', $serialized, $this->defaultMimeType); + $this->httpRequest('node/' . $node->id(), 'PATCH', $serialized, $this->defaultMimeType); $this->assertResponse(204); // Reload the node from the DB and check if the title was correctly updated. diff --git a/core/modules/rest/lib/Drupal/rest/Tests/RESTTestBase.php b/core/modules/rest/lib/Drupal/rest/Tests/RESTTestBase.php index d6fc9f9..bfc2736 100644 --- a/core/modules/rest/lib/Drupal/rest/Tests/RESTTestBase.php +++ b/core/modules/rest/lib/Drupal/rest/Tests/RESTTestBase.php @@ -248,16 +248,22 @@ protected function assertHeader($header, $value, $message = '', $group = 'Browse } /** - * Overrides WebTestBase::drupalLogin(). + * {@inheritdoc} + * + * This method is overridden to deal with a cURL quirk: the usage of + * CURLOPT_CUSTOMREQUEST cannot be unset on the cURL handle, so we need to + * override it every time it is omitted. */ - protected function drupalLogin(AccountInterface $user) { - if (isset($this->curlHandle)) { - // cURL quirk: when setting CURLOPT_CUSTOMREQUEST to anything other than - // POST in httpRequest() it has to be restored to POST here. Otherwise the - // POST request to login a user will not work. - curl_setopt($this->curlHandle, CURLOPT_CUSTOMREQUEST, 'POST'); + protected function curlExec($curl_options, $redirect = FALSE) { + if (!isset($curl_options[CURLOPT_CUSTOMREQUEST])) { + if (!empty($curl_options[CURLOPT_HTTPGET])) { + $curl_options[CURLOPT_CUSTOMREQUEST] = 'GET'; + } + if (!empty($curl_options[CURLOPT_POST])) { + $curl_options[CURLOPT_CUSTOMREQUEST] = 'POST'; + } } - parent::drupalLogin($user); + return parent::curlExec($curl_options, $redirect); } /** @@ -295,4 +301,22 @@ protected function entityPermissions($entity_type, $operation) { } } } + + /** + * Returns the base URI path of an entity type. + * + * @param string $entity_type + * The entity type. + * + * @return string + * The URI path. + */ + 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 a22174e..3be1b33 100644 --- a/core/modules/rest/lib/Drupal/rest/Tests/ReadTest.php +++ b/core/modules/rest/lib/Drupal/rest/Tests/ReadTest.php @@ -50,7 +50,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); @@ -59,14 +59,22 @@ 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); + $this->assertEqual($response, 'An error occurred: No route found for the specified formats application/wrongformat.'); // 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.'); + if ($entity_type == 'entity_test') { + $decoded = drupal_json_decode($response); + $this->assertEqual($decoded['error'], 'Entity with ID 9999 not found'); + } + if ($entity_type == 'node') { + // Nodes are different because they get upcasted by a route enhancer, + // which throws an exception if the node does not exist. + $this->assertEqual($response, 'An error occurred: Item "node" with ID 9999 not found'); + } // Make sure that field level access works and that the according field is // not available in the response. Only applies to entity_test. @@ -74,7 +82,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); @@ -83,7 +91,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.'); } @@ -113,7 +121,7 @@ public function testResourceStructure() { $entity->save(); // Read it over the REST API. - $response = $this->httpRequest('entity/node/' . $entity->id(), 'GET', NULL, 'application/json'); + $response = $this->httpRequest('node/' . $entity->id(), 'GET', NULL, 'application/json'); $this->assertResponse('200', 'HTTP response code is correct.'); } diff --git a/core/modules/system/lib/Drupal/system/Tests/Routing/MimeTypeMatcherTest.php b/core/modules/system/lib/Drupal/system/Tests/Routing/AcceptHeaderMatcherTest.php similarity index 88% rename from core/modules/system/lib/Drupal/system/Tests/Routing/MimeTypeMatcherTest.php rename to core/modules/system/lib/Drupal/system/Tests/Routing/AcceptHeaderMatcherTest.php index 827c733..99e7901 100644 --- a/core/modules/system/lib/Drupal/system/Tests/Routing/MimeTypeMatcherTest.php +++ b/core/modules/system/lib/Drupal/system/Tests/Routing/AcceptHeaderMatcherTest.php @@ -2,21 +2,21 @@ /** * @file - * Contains Drupal\system\Tests\Routing\MimeTypeMatcherTest. + * Contains Drupal\system\Tests\Routing\AcceptHeaderMatcherTest. */ namespace Drupal\system\Tests\Routing; -use Drupal\Core\Routing\MimeTypeMatcher; +use Drupal\Core\Routing\AcceptHeaderMatcher; use Drupal\simpletest\UnitTestBase; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpKernel\Exception\NotAcceptableHttpException; /** - * Basic tests for the MimeTypeMatcher class. + * Basic tests for the AcceptHeaderMatcher class. */ -class MimeTypeMatcherTest extends UnitTestBase { +class AcceptHeaderMatcherTest extends UnitTestBase { /** * A collection of shared fixture data for tests. @@ -44,7 +44,7 @@ function __construct($test_id = NULL) { */ public function testFilterRoutes() { - $matcher = new MimeTypeMatcher(); + $matcher = new AcceptHeaderMatcher(); $collection = $this->fixtures->sampleRouteCollection(); // Tests basic JSON request. @@ -73,10 +73,10 @@ public function testFilterRoutes() { } /** - * Confirms that the MimeTypeMatcher matcher throws an exception for no-route. + * Confirms that the AcceptHeaderMatcher matcher throws an exception for no-route. */ public function testNoRouteFound() { - $matcher = new MimeTypeMatcher(); + $matcher = new AcceptHeaderMatcher(); // Remove the sample routes that would match any method. $routes = $this->fixtures->sampleRouteCollection();