From 30ca0d1de63fc67213afe4e5aa2aaaa6f400eff1 Mon Sep 17 00:00:00 2001 From: Nathaniel Catchpole <catch@35733.no-reply.drupal.org> Date: Mon, 8 Jun 2015 11:11:09 +0100 Subject: [PATCH] Issue #2481453 by dawehner, pwolanin, rteijeiro, neclimdul, znerol: Implement query parameter based content negotiation as alternative to extensions --- core/core.services.yml | 8 +- core/lib/Drupal/Core/ContentNegotiation.php | 22 +-- .../DefaultExceptionSubscriber.php | 8 +- .../HttpExceptionSubscriberBase.php | 5 +- .../Drupal/Core/Routing/LazyRouteFilter.php | 5 +- .../Core/Routing/RequestFormatRouteFilter.php | 57 +++++++ .../StackMiddleware/NegotiationMiddleware.php | 4 +- core/misc/progress.js | 11 +- .../src/Tests/Views/DisplayBlockTest.php | 2 +- .../src/Tests/Views/CommentRestExportTest.php | 2 +- .../views.view.test_comment_rest.yml | 1 + .../src/Tests/ConfigTranslationUiTest.php | 2 +- .../Tests/ContextualDynamicContextTest.php | 2 +- .../src/Tests/Rest/DbLogResourceTest.php | 5 +- .../editor/src/Tests/EditorSecurityTest.php | 2 +- .../Tests/QuickEditIntegrationLoadingTest.php | 3 +- core/modules/hal/src/Encoder/JsonEncoder.php | 2 +- core/modules/hal/src/HalServiceProvider.php | 2 +- .../Normalizer/ContentEntityNormalizer.php | 14 +- core/modules/hal/src/Tests/NormalizeTest.php | 8 +- .../page_cache/src/Tests/PageCacheTest.php | 23 +-- .../Tests/QuickEditAutocompleteTermTest.php | 2 +- .../src/Tests/QuickEditLoadingTest.php | 23 +-- .../src/Plugin/views/display/RestExport.php | 11 +- .../src/Plugin/views/style/Serializer.php | 6 +- core/modules/rest/src/Tests/AuthTest.php | 6 +- core/modules/rest/src/Tests/NodeTest.php | 4 +- core/modules/rest/src/Tests/PageCacheTest.php | 6 +- core/modules/rest/src/Tests/ReadTest.php | 28 ++-- core/modules/rest/src/Tests/ResourceTest.php | 31 ++-- .../src/Tests/Views/StyleSerializerTest.php | 37 +++-- core/modules/simpletest/src/WebTestBase.php | 54 +++++- .../Routing/ContentNegotiationRoutingTest.php | 157 ++++++++++++++++++ .../Tests/Routing/ExceptionHandlingTest.php | 11 +- .../Tests/System/ResponseGeneratorTest.php | 2 +- .../accept_header_routing_test.info.yml | 3 + .../accept_header_routing_test.services.yml | 5 + .../src/AcceptHeaderMiddleware.php | 47 ++++++ ...AcceptHeaderRoutingTestServiceProvider.php | 28 ++++ .../src}/Routing/AcceptHeaderMatcher.php | 5 +- .../tests/Unit}/AcceptHeaderMatcherTest.php | 10 +- .../modules/conneg_test/conneg_test.info.yml | 6 + .../conneg_test/conneg_test.routing.yml | 32 ++++ .../src/Controller/TestController.php | 76 +++++++++ .../PageCacheAcceptHeaderController.php | 2 +- .../views_ui/src/Tests/DisplayTest.php | 2 +- .../Tests/Core/ContentNegotiationTest.php | 31 +--- .../Routing/RequestFormatRouteFilterTest.php | 85 ++++++++++ 48 files changed, 720 insertions(+), 178 deletions(-) create mode 100644 core/lib/Drupal/Core/Routing/RequestFormatRouteFilter.php create mode 100644 core/modules/system/src/Tests/Routing/ContentNegotiationRoutingTest.php create mode 100644 core/modules/system/tests/modules/accept_header_routing_test/accept_header_routing_test.info.yml create mode 100644 core/modules/system/tests/modules/accept_header_routing_test/accept_header_routing_test.services.yml create mode 100644 core/modules/system/tests/modules/accept_header_routing_test/src/AcceptHeaderMiddleware.php create mode 100644 core/modules/system/tests/modules/accept_header_routing_test/src/AcceptHeaderRoutingTestServiceProvider.php rename core/{lib/Drupal/Core => modules/system/tests/modules/accept_header_routing_test/src}/Routing/AcceptHeaderMatcher.php (93%) rename core/{tests/Drupal/Tests/Core/Routing => modules/system/tests/modules/accept_header_routing_test/tests/Unit}/AcceptHeaderMatcherTest.php (91%) create mode 100644 core/modules/system/tests/modules/conneg_test/conneg_test.info.yml create mode 100644 core/modules/system/tests/modules/conneg_test/conneg_test.routing.yml create mode 100644 core/modules/system/tests/modules/conneg_test/src/Controller/TestController.php create mode 100644 core/tests/Drupal/Tests/Core/Routing/RequestFormatRouteFilterTest.php diff --git a/core/core.services.yml b/core/core.services.yml index 7b41a1d2c64a..86c3dad4eb54 100644 --- a/core/core.services.yml +++ b/core/core.services.yml @@ -560,10 +560,6 @@ services: http_middleware.negotiation: class: Drupal\Core\StackMiddleware\NegotiationMiddleware arguments: ['@http_negotiation.format_negotiator'] - calls: - - [registerFormat, ['drupal_ajax', ['application/vnd.drupal-ajax']]] - - [registerFormat, ['drupal_dialog', ['application/vnd.drupal-dialog']]] - - [registerFormat, ['drupal_modal', ['application/vnd.drupal-modal']]] tags: - { name: http_middleware, priority: 400 } http_middleware.reverse_proxy: @@ -758,8 +754,8 @@ services: password: class: Drupal\Core\Password\PhpassHashedPassword arguments: [16] - accept_header_matcher: - class: Drupal\Core\Routing\AcceptHeaderMatcher + request_format_route_filter: + class: Drupal\Core\Routing\RequestFormatRouteFilter tags: - { name: route_filter } content_type_header_matcher: diff --git a/core/lib/Drupal/Core/ContentNegotiation.php b/core/lib/Drupal/Core/ContentNegotiation.php index e45c92dccdd5..1074b816e03c 100644 --- a/core/lib/Drupal/Core/ContentNegotiation.php +++ b/core/lib/Drupal/Core/ContentNegotiation.php @@ -10,10 +10,7 @@ use Symfony\Component\HttpFoundation\Request; /** - * This class is a central library for content type negotiation. - * - * @todo Replace this class with a real content negotiation library based on - * mod_negotiation. Development of that is a work in progress. + * Provides content negotation based upon query parameters. */ class ContentNegotiation { @@ -36,21 +33,8 @@ public function getContentType(Request $request) { return 'iframeupload'; } - // Check all formats, if priority format is found return it. - $first_found_format = FALSE; - foreach ($request->getAcceptableContentTypes() as $mime_type) { - $format = $request->getFormat($mime_type); - if ($format === 'html') { - return $format; - } - if (!is_null($format) && !$first_found_format) { - $first_found_format = $format; - } - } - - // No HTML found, return first found. - if ($first_found_format) { - return $first_found_format; + if ($request->query->has('_format')) { + return $request->query->get('_format'); } if ($request->isXmlHttpRequest()) { diff --git a/core/lib/Drupal/Core/EventSubscriber/DefaultExceptionSubscriber.php b/core/lib/Drupal/Core/EventSubscriber/DefaultExceptionSubscriber.php index 883927ce23fd..46d6c4605c55 100644 --- a/core/lib/Drupal/Core/EventSubscriber/DefaultExceptionSubscriber.php +++ b/core/lib/Drupal/Core/EventSubscriber/DefaultExceptionSubscriber.php @@ -198,13 +198,7 @@ public function onException(GetResponseForExceptionEvent $event) { * The format as which to treat the exception. */ protected function getFormat(Request $request) { - // @todo We are trying to switch to a more robust content negotiation - // library in https://www.drupal.org/node/1505080 that will make - // $request->getRequestFormat() reliable as a better alternative - // to this code. We therefore use this style for now on the expectation - // that it will get replaced with better code later. This approach makes - // that change easier when we get to it. - $format = \Drupal::service('http_negotiation.format_negotiator')->getContentType($request); + $format = $request->query->get(MainContentViewSubscriber::WRAPPER_FORMAT, $request->getRequestFormat()); // These are all JSON errors for our purposes. Any special handling for // them can/should happen in earlier listeners if desired. diff --git a/core/lib/Drupal/Core/EventSubscriber/HttpExceptionSubscriberBase.php b/core/lib/Drupal/Core/EventSubscriber/HttpExceptionSubscriberBase.php index a14d327b16c4..6fcacc9443ca 100644 --- a/core/lib/Drupal/Core/EventSubscriber/HttpExceptionSubscriberBase.php +++ b/core/lib/Drupal/Core/EventSubscriber/HttpExceptionSubscriberBase.php @@ -86,11 +86,12 @@ public function onException(GetResponseForExceptionEvent $event) { $exception = $event->getException(); // Make the exception available for example when rendering a block. - $event->getRequest()->attributes->set('exception', $exception); + $request = $event->getRequest(); + $request->attributes->set('exception', $exception); $handled_formats = $this->getHandledFormats(); - $format = $event->getRequest()->getRequestFormat(); + $format = $request->query->get(MainContentViewSubscriber::WRAPPER_FORMAT, $request->getRequestFormat()); if ($exception instanceof HttpExceptionInterface && (empty($handled_formats) || in_array($format, $handled_formats))) { $method = 'on' . $exception->getStatusCode(); diff --git a/core/lib/Drupal/Core/Routing/LazyRouteFilter.php b/core/lib/Drupal/Core/Routing/LazyRouteFilter.php index 288e09b694a7..5cb85a72d81f 100644 --- a/core/lib/Drupal/Core/Routing/LazyRouteFilter.php +++ b/core/lib/Drupal/Core/Routing/LazyRouteFilter.php @@ -10,6 +10,7 @@ use Symfony\Cmf\Component\Routing\NestedMatcher\RouteFilterInterface as BaseRouteFilterInterface; use Symfony\Component\DependencyInjection\ContainerAwareInterface; use Symfony\Component\DependencyInjection\ContainerAwareTrait; +use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\Routing\RouteCollection; @@ -95,7 +96,9 @@ public function filter(RouteCollection $collection, Request $request) { if (isset($filter_ids)) { foreach ($filter_ids as $filter_id) { - $collection = $this->container->get($filter_id)->filter($collection, $request); + if ($filter = $this->container->get($filter_id, ContainerInterface::NULL_ON_INVALID_REFERENCE)) { + $collection = $filter->filter($collection, $request); + } } } return $collection; diff --git a/core/lib/Drupal/Core/Routing/RequestFormatRouteFilter.php b/core/lib/Drupal/Core/Routing/RequestFormatRouteFilter.php new file mode 100644 index 000000000000..4d5189a2d303 --- /dev/null +++ b/core/lib/Drupal/Core/Routing/RequestFormatRouteFilter.php @@ -0,0 +1,57 @@ +<?php + +/** + * @file + * Contains \Drupal\Core\Routing\RequestFormatRouteFilter. + */ + +namespace Drupal\Core\Routing; + +use Drupal\Component\Utility\SafeMarkup; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpKernel\Exception\NotAcceptableHttpException; +use Symfony\Component\Routing\Route; +use Symfony\Component\Routing\RouteCollection; + +/** + * Provides a route filter, which filters by the request format. + */ +class RequestFormatRouteFilter implements RouteFilterInterface { + + /** + * {@inheritdoc} + */ + public function applies(Route $route) { + return $route->hasRequirement('_format'); + } + + /** + * {@inheritdoc} + */ + public function filter(RouteCollection $collection, Request $request) { + $format = $request->getRequestFormat('html'); + /** @var \Symfony\Component\Routing\Route $route */ + foreach ($collection as $name => $route) { + // If the route has no _format specification, we move it to the end. If it + // does, then no match means the route is removed entirely. + if ($supported_formats = array_filter(explode('|', $route->getRequirement('_format')))) { + if (!in_array($format, $supported_formats)) { + $collection->remove($name); + } + } + else { + $collection->add($name, $route); + } + } + + 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 406. + throw new NotAcceptableHttpException(SafeMarkup::format('No route found for the specified format @format.', ['@format' => $format])); + } + +} diff --git a/core/lib/Drupal/Core/StackMiddleware/NegotiationMiddleware.php b/core/lib/Drupal/Core/StackMiddleware/NegotiationMiddleware.php index 295857d626c5..3b0bf7627052 100644 --- a/core/lib/Drupal/Core/StackMiddleware/NegotiationMiddleware.php +++ b/core/lib/Drupal/Core/StackMiddleware/NegotiationMiddleware.php @@ -38,7 +38,7 @@ class NegotiationMiddleware implements HttpKernelInterface { * * @var array */ - protected $formats; + protected $formats = []; /** * Constructs a new NegotiationMiddleware. @@ -57,10 +57,12 @@ public function __construct(HttpKernelInterface $app, ContentNegotiation $negoti * {@inheritdoc} */ public function handle(Request $request, $type = self::MASTER_REQUEST, $catch = true) { + // Register available mime types. foreach ($this->formats as $format => $mime_type) { $request->setFormat($format, $mime_type); } + // Determine the request format using the negotiator. $request->setRequestFormat($this->negotiator->getContentType($request)); return $this->app->handle($request, $type, $catch); } diff --git a/core/misc/progress.js b/core/misc/progress.js index 7305d24a2033..e3d1223d0b18 100644 --- a/core/misc/progress.js +++ b/core/misc/progress.js @@ -83,11 +83,18 @@ var pb = this; // When doing a post request, you need non-null data. Otherwise a // HTTP 411 or HTTP 406 (with Apache mod_security) error may result. + var uri = this.uri; + if (uri.indexOf('?') === -1) { + uri += '?'; + } + else { + uri += '&'; + } + uri += '_format=json'; $.ajax({ type: this.method, - url: this.uri, + url: uri, data: '', - dataType: 'json', success: function (progress) { // Display errors. if (progress.status === 0) { diff --git a/core/modules/block/src/Tests/Views/DisplayBlockTest.php b/core/modules/block/src/Tests/Views/DisplayBlockTest.php index 56e7354791ed..3f96ab10f82b 100644 --- a/core/modules/block/src/Tests/Views/DisplayBlockTest.php +++ b/core/modules/block/src/Tests/Views/DisplayBlockTest.php @@ -285,7 +285,7 @@ public function testBlockContextualLinks() { // Get server-rendered contextual links. // @see \Drupal\contextual\Tests\ContextualDynamicContextTest:renderContextualLinks() $post = array('ids[0]' => $id, 'ids[1]' => $cached_id); - $response = $this->drupalPost('contextual/render', 'application/json', $post, array('query' => array('destination' => 'test-page'))); + $response = $this->drupalPostWithFormat('contextual/render', 'json', $post, array('query' => array('destination' => 'test-page'))); $this->assertResponse(200); $json = Json::decode($response); $this->assertIdentical($json[$id], '<ul class="contextual-links"><li class="block-configure"><a href="' . base_path() . 'admin/structure/block/manage/' . $block->id() . '">Configure block</a></li><li class="entityviewedit-form"><a href="' . base_path() . 'admin/structure/views/view/test_view_block/edit/block_1">Edit view</a></li></ul>'); diff --git a/core/modules/comment/src/Tests/Views/CommentRestExportTest.php b/core/modules/comment/src/Tests/Views/CommentRestExportTest.php index ecf1a546b2e6..9737b4fbd021 100644 --- a/core/modules/comment/src/Tests/Views/CommentRestExportTest.php +++ b/core/modules/comment/src/Tests/Views/CommentRestExportTest.php @@ -55,7 +55,7 @@ protected function setUp() { * Test comment row. */ public function testCommentRestExport() { - $this->drupalGet(sprintf('node/%d/comments', $this->nodeUserCommented->id()), [], ['Accept' => 'application/hal+json']); + $this->drupalGetWithFormat(sprintf('node/%d/comments', $this->nodeUserCommented->id()), 'hal_json'); $this->assertResponse(200); $contents = Json::decode($this->getRawContent()); $this->assertEqual($contents[0]['subject'], 'How much wood would a woodchuck chuck'); diff --git a/core/modules/comment/tests/modules/comment_test_views/test_views/views.view.test_comment_rest.yml b/core/modules/comment/tests/modules/comment_test_views/test_views/views.view.test_comment_rest.yml index 2de8cdb6a9e4..6350455000d4 100644 --- a/core/modules/comment/tests/modules/comment_test_views/test_views/views.view.test_comment_rest.yml +++ b/core/modules/comment/tests/modules/comment_test_views/test_views/views.view.test_comment_rest.yml @@ -386,6 +386,7 @@ display: uses_fields: false formats: json: json + hal_json: hal_json row: type: data_field options: diff --git a/core/modules/config_translation/src/Tests/ConfigTranslationUiTest.php b/core/modules/config_translation/src/Tests/ConfigTranslationUiTest.php index 00b36c526df3..c7218dffcf9a 100644 --- a/core/modules/config_translation/src/Tests/ConfigTranslationUiTest.php +++ b/core/modules/config_translation/src/Tests/ConfigTranslationUiTest.php @@ -904,7 +904,7 @@ protected function renderContextualLinks($ids, $current_path) { for ($i = 0; $i < count($ids); $i++) { $post['ids[' . $i . ']'] = $ids[$i]; } - return $this->drupalPost('contextual/render', 'application/json', $post, array('query' => array('destination' => $current_path))); + return $this->drupalPostWithFormat('contextual/render', 'json', $post, array('query' => array('destination' => $current_path))); } /** diff --git a/core/modules/contextual/src/Tests/ContextualDynamicContextTest.php b/core/modules/contextual/src/Tests/ContextualDynamicContextTest.php index 01eb4f57d0f2..8846ab935888 100644 --- a/core/modules/contextual/src/Tests/ContextualDynamicContextTest.php +++ b/core/modules/contextual/src/Tests/ContextualDynamicContextTest.php @@ -181,6 +181,6 @@ protected function renderContextualLinks($ids, $current_path) { for ($i = 0; $i < count($ids); $i++) { $post['ids[' . $i . ']'] = $ids[$i]; } - return $this->drupalPost('contextual/render', 'application/json', $post, array('query' => array('destination' => $current_path))); + return $this->drupalPostWithFormat('contextual/render', 'json', $post, array('query' => array('destination' => $current_path))); } } diff --git a/core/modules/dblog/src/Tests/Rest/DbLogResourceTest.php b/core/modules/dblog/src/Tests/Rest/DbLogResourceTest.php index 25c7a901bde4..91097757369e 100644 --- a/core/modules/dblog/src/Tests/Rest/DbLogResourceTest.php +++ b/core/modules/dblog/src/Tests/Rest/DbLogResourceTest.php @@ -8,6 +8,7 @@ namespace Drupal\dblog\Tests\Rest; use Drupal\Component\Serialization\Json; +use Drupal\Core\Url; use Drupal\rest\Tests\RESTTestBase; /** @@ -45,7 +46,7 @@ public function testWatchdog() { $account = $this->drupalCreateUser(array('restful get dblog')); $this->drupalLogin($account); - $response = $this->httpRequest("dblog/$id", 'GET', NULL, $this->defaultMimeType); + $response = $this->httpRequest(Url::fromRoute('rest.dblog.GET.' . $this->defaultFormat, ['id' => $id, '_format' => $this->defaultFormat]), 'GET'); $this->assertResponse(200); $this->assertHeader('content-type', $this->defaultMimeType); $log = Json::decode($response); @@ -54,7 +55,7 @@ public function testWatchdog() { $this->assertEqual($log['message'], 'Test message', 'Log message text is correct.'); // Request an unknown log entry. - $response = $this->httpRequest("dblog/9999", 'GET', NULL, $this->defaultMimeType); + $response = $this->httpRequest(Url::fromRoute('rest.dblog.GET.' . $this->defaultFormat, ['id' => 9999, '_format' => $this->defaultFormat]), 'GET'); $this->assertResponse(404); $decoded = Json::decode($response); $this->assertEqual($decoded['error'], 'Log entry with ID 9999 was not found', 'Response message is correct.'); diff --git a/core/modules/editor/src/Tests/EditorSecurityTest.php b/core/modules/editor/src/Tests/EditorSecurityTest.php index 305692dca22c..703c65bdbbd4 100644 --- a/core/modules/editor/src/Tests/EditorSecurityTest.php +++ b/core/modules/editor/src/Tests/EditorSecurityTest.php @@ -409,7 +409,7 @@ function testSwitchingSecurity() { 'value' => self::$sampleContent, 'original_format_id' => $case['format'], ); - $response = $this->drupalPost('editor/filter_xss/' . $format, 'application/json', $post); + $response = $this->drupalPostWithFormat('editor/filter_xss/' . $format, 'json', $post); $this->assertResponse(200); $json = Json::decode($response); $this->assertIdentical($json, $expected_filtered_value, 'The value was correctly filtered for XSS attack vectors.'); diff --git a/core/modules/editor/src/Tests/QuickEditIntegrationLoadingTest.php b/core/modules/editor/src/Tests/QuickEditIntegrationLoadingTest.php index c4b81eadcdf4..9204f2876e55 100644 --- a/core/modules/editor/src/Tests/QuickEditIntegrationLoadingTest.php +++ b/core/modules/editor/src/Tests/QuickEditIntegrationLoadingTest.php @@ -8,6 +8,7 @@ namespace Drupal\editor\Tests; use Drupal\Component\Serialization\Json; +use Drupal\Core\EventSubscriber\MainContentViewSubscriber; use Drupal\simpletest\WebTestBase; /** @@ -88,7 +89,7 @@ public function testUsersWithoutPermission() { $this->assertRaw('<p>Do you also love Drupal?</p><figure class="caption caption-img"><img src="druplicon.png" /><figcaption>Druplicon</figcaption></figure>'); // Retrieving the untransformed text should result in an empty 403 response. - $response = $this->drupalPost('editor/' . 'node/1/body/en/full', 'application/vnd.drupal-ajax', array()); + $response = $this->drupalPost('editor/' . 'node/1/body/en/full', '', array(), array('query' => array(MainContentViewSubscriber::WRAPPER_FORMAT => 'drupal_ajax'))); $this->assertResponse(403); $this->assertIdentical('{}', $response); } diff --git a/core/modules/hal/src/Encoder/JsonEncoder.php b/core/modules/hal/src/Encoder/JsonEncoder.php index 4d932ff8a0bd..897bc4496a2d 100644 --- a/core/modules/hal/src/Encoder/JsonEncoder.php +++ b/core/modules/hal/src/Encoder/JsonEncoder.php @@ -12,7 +12,7 @@ /** * Encodes HAL data in JSON. * - * Simply respond to application/hal+json requests using the JSON encoder. + * Simply respond to hal_json format requests using the JSON encoder. */ class JsonEncoder extends SymfonyJsonEncoder { diff --git a/core/modules/hal/src/HalServiceProvider.php b/core/modules/hal/src/HalServiceProvider.php index d8ac47f0f08d..afe8d2d20fee 100644 --- a/core/modules/hal/src/HalServiceProvider.php +++ b/core/modules/hal/src/HalServiceProvider.php @@ -19,7 +19,7 @@ class HalServiceProvider implements ServiceModifierInterface { * {@inheritdoc} */ public function alter(ContainerBuilder $container) { - if ($container->has('http_middleware.negotiation')) { + if ($container->has('http_middleware.negotiation') && is_a($container->getDefinition('http_middleware.negotiation')->getClass(), '\Drupal\Core\StackMiddleware\NegotiationMiddleware', TRUE)) { $container->getDefinition('http_middleware.negotiation')->addMethodCall('registerFormat', ['hal_json', ['application/hal+json']]); } } diff --git a/core/modules/hal/src/Normalizer/ContentEntityNormalizer.php b/core/modules/hal/src/Normalizer/ContentEntityNormalizer.php index 14fdb0b44066..a4761b356087 100644 --- a/core/modules/hal/src/Normalizer/ContentEntityNormalizer.php +++ b/core/modules/hal/src/Normalizer/ContentEntityNormalizer.php @@ -8,6 +8,7 @@ namespace Drupal\hal\Normalizer; use Drupal\Component\Utility\NestedArray; +use Drupal\Core\Entity\EntityInterface; use Drupal\Core\Entity\EntityManagerInterface; use Drupal\Core\Extension\ModuleHandlerInterface; use Drupal\rest\LinkManager\LinkManagerInterface; @@ -195,14 +196,19 @@ public function denormalize($data, $class, $format = NULL, array $context = arra /** * Constructs the entity URI. * - * @param $entity + * @param \Drupal\Core\Entity\EntityInterface * The entity. - * * @return string * The entity URI. */ - protected function getEntityUri($entity) { - return $entity->url('canonical', array('absolute' => TRUE)); + protected function getEntityUri(EntityInterface $entity) { + // Some entity types don't provide a canonical link template, at least call + // out to ->url(). + if ($entity->isNew() || !$entity->hasLinkTemplate('canonical')) { + return $entity->url('canonical', []); + } + $url = $entity->urlInfo('canonical', ['absolute' => TRUE]); + return $url->setRouteParameter('_format', 'hal_json')->toString(); } /** diff --git a/core/modules/hal/src/Tests/NormalizeTest.php b/core/modules/hal/src/Tests/NormalizeTest.php index 9525f056ca13..44d7f242b1d2 100644 --- a/core/modules/hal/src/Tests/NormalizeTest.php +++ b/core/modules/hal/src/Tests/NormalizeTest.php @@ -7,6 +7,7 @@ namespace Drupal\hal\Tests; +use Drupal\Core\Entity\EntityInterface; use Drupal\Core\Url; /** @@ -169,14 +170,15 @@ public function testNormalize() { /** * Constructs the entity URI. * - * @param $entity + * @param \Drupal\Core\Entity\EntityInterface $entity * The entity. * * @return string * The entity URI. */ - protected function getEntityUri($entity) { - return $entity->url('canonical', array('absolute' => TRUE)); + protected function getEntityUri(EntityInterface $entity) { + $url = $entity->urlInfo('canonical', ['absolute' => TRUE]); + return $url->setRouteParameter('_format', 'hal_json')->toString(); } } diff --git a/core/modules/page_cache/src/Tests/PageCacheTest.php b/core/modules/page_cache/src/Tests/PageCacheTest.php index 7faa45e3b4e2..37fd06b7f7a1 100644 --- a/core/modules/page_cache/src/Tests/PageCacheTest.php +++ b/core/modules/page_cache/src/Tests/PageCacheTest.php @@ -79,15 +79,16 @@ function testPageCacheTags() { } /** - * Tests support for different cache items with different Accept headers. + * Tests support for different cache items with different request formats + * specified via a query parameter. */ - function testAcceptHeaderRequests() { + function testQueryParameterFormatRequests() { $config = $this->config('system.performance'); $config->set('cache.page.max_age', 300); $config->save(); $accept_header_cache_url = Url::fromRoute('system_test.page_cache_accept_header'); - $json_accept_header = array('Accept: application/json'); + $accept_header_cache_url_with_json = Url::fromRoute('system_test.page_cache_accept_header', ['_format' => 'json']); $this->drupalGet($accept_header_cache_url); $this->assertEqual($this->drupalGetHeader('X-Drupal-Cache'), 'MISS', 'HTML page was not yet cached.'); @@ -95,9 +96,9 @@ function testAcceptHeaderRequests() { $this->assertEqual($this->drupalGetHeader('X-Drupal-Cache'), 'HIT', 'HTML page was cached.'); $this->assertRaw('<p>oh hai this is html.</p>', 'The correct HTML response was returned.'); - $this->drupalGet($accept_header_cache_url, array(), $json_accept_header); + $this->drupalGet($accept_header_cache_url_with_json); $this->assertEqual($this->drupalGetHeader('X-Drupal-Cache'), 'MISS', 'Json response was not yet cached.'); - $this->drupalGet($accept_header_cache_url, array(), $json_accept_header); + $this->drupalGet($accept_header_cache_url_with_json); $this->assertEqual($this->drupalGetHeader('X-Drupal-Cache'), 'HIT', 'Json response was cached.'); $this->assertRaw('{"content":"oh hai this is json"}', 'The correct Json response was returned.'); @@ -105,8 +106,8 @@ function testAcceptHeaderRequests() { \Drupal::service('module_installer')->install(['node', 'rest', 'hal']); $this->drupalCreateContentType(['type' => 'article']); $node = $this->drupalCreateNode(['type' => 'article']); - $node_uri = 'node/' . $node->id(); - $hal_json_accept_header = ['Accept: application/hal+json']; + $node_uri = $node->urlInfo(); + $node_url_with_hal_json_format = $node->urlInfo('canonical')->setRouteParameter('_format', 'hal_json'); /** @var \Drupal\user\RoleInterface $role */ $role = Role::load('anonymous'); $role->grantPermission('restful get entity:node'); @@ -121,20 +122,20 @@ function testAcceptHeaderRequests() { // Now request a HAL page, we expect that the first request is a cache miss // and it serves HTML. - $this->drupalGet($node_uri, [], $hal_json_accept_header); + $this->drupalGet($node_url_with_hal_json_format); $this->assertEqual($this->drupalGetHeader('X-Drupal-Cache'), 'MISS'); $this->assertEqual($this->drupalGetHeader('Content-Type'), 'application/hal+json'); - $this->drupalGet($node_uri, [], $hal_json_accept_header); + $this->drupalGet($node_url_with_hal_json_format); $this->assertEqual($this->drupalGetHeader('X-Drupal-Cache'), 'HIT'); $this->assertEqual($this->drupalGetHeader('Content-Type'), 'application/hal+json'); // Clear the page cache. After that request a HAL request, followed by an // ordinary HTML one. \Drupal::cache('render')->deleteAll(); - $this->drupalGet($node_uri, [], $hal_json_accept_header); + $this->drupalGet($node_url_with_hal_json_format); $this->assertEqual($this->drupalGetHeader('X-Drupal-Cache'), 'MISS'); $this->assertEqual($this->drupalGetHeader('Content-Type'), 'application/hal+json'); - $this->drupalGet($node_uri, [], $hal_json_accept_header); + $this->drupalGet($node_url_with_hal_json_format); $this->assertEqual($this->drupalGetHeader('X-Drupal-Cache'), 'HIT'); $this->assertEqual($this->drupalGetHeader('Content-Type'), 'application/hal+json'); diff --git a/core/modules/quickedit/src/Tests/QuickEditAutocompleteTermTest.php b/core/modules/quickedit/src/Tests/QuickEditAutocompleteTermTest.php index 467a66c8616e..81eaa973de5a 100644 --- a/core/modules/quickedit/src/Tests/QuickEditAutocompleteTermTest.php +++ b/core/modules/quickedit/src/Tests/QuickEditAutocompleteTermTest.php @@ -181,7 +181,7 @@ public function testAutocompleteQuickEdit() { // Save the entity. $post = array('nocssjs' => 'true'); - $response = $this->drupalPost('quickedit/entity/node/' . $this->node->id(), 'application/json', $post); + $response = $this->drupalPostWithFormat('quickedit/entity/node/' . $this->node->id(), 'json', $post); $this->assertResponse(200); // The full node display should now link to all entities, with the new diff --git a/core/modules/quickedit/src/Tests/QuickEditLoadingTest.php b/core/modules/quickedit/src/Tests/QuickEditLoadingTest.php index 6cb092409b1f..1747b1937d9f 100644 --- a/core/modules/quickedit/src/Tests/QuickEditLoadingTest.php +++ b/core/modules/quickedit/src/Tests/QuickEditLoadingTest.php @@ -12,7 +12,8 @@ use Drupal\block_content\Entity\BlockContent; use Drupal\field\Entity\FieldConfig; use Drupal\field\Entity\FieldStorageConfig; -use Drupal\file\Entity\File; +use Drupal\Core\EventSubscriber\MainContentViewSubscriber; +use Drupal\Core\Url; use Drupal\node\Entity\Node; use Drupal\node\Entity\NodeType; use Drupal\simpletest\WebTestBase; @@ -106,7 +107,7 @@ public function testUserWithoutPermission() { // Retrieving the metadata should result in an empty 403 response. $post = array('fields[0]' => 'node/1/body/en/full'); - $response = $this->drupalPost('quickedit/metadata', 'application/json', $post); + $response = $this->drupalPostWithFormat(Url::fromRoute('quickedit.metadata'), 'json', $post); $this->assertIdentical('{"message":""}', $response); $this->assertResponse(403); @@ -114,11 +115,11 @@ public function testUserWithoutPermission() { // was empty as above, but we need to make sure that malicious users aren't // able to use any of the other endpoints either. $post = array('editors[0]' => 'form') + $this->getAjaxPageStatePostData(); - $response = $this->drupalPost('quickedit/attachments', 'application/vnd.drupal-ajax', $post); + $response = $this->drupalPost('quickedit/attachments', '', $post, ['query' => [MainContentViewSubscriber::WRAPPER_FORMAT => 'drupal_ajax']]); $this->assertIdentical('{}', $response); $this->assertResponse(403); $post = array('nocssjs' => 'true') + $this->getAjaxPageStatePostData(); - $response = $this->drupalPost('quickedit/form/' . 'node/1/body/en/full', 'application/vnd.drupal-ajax', $post); + $response = $this->drupalPost('quickedit/form/' . 'node/1/body/en/full', '', $post, ['query' => [MainContentViewSubscriber::WRAPPER_FORMAT => 'drupal_ajax']]); $this->assertIdentical('{}', $response); $this->assertResponse(403); $edit = array(); @@ -129,11 +130,11 @@ public function testUserWithoutPermission() { $edit['body[0][value]'] = '<p>Malicious content.</p>'; $edit['body[0][format]'] = 'filtered_html'; $edit['op'] = t('Save'); - $response = $this->drupalPost('quickedit/form/' . 'node/1/body/en/full', 'application/vnd.drupal-ajax', $edit); + $response = $this->drupalPost('quickedit/form/' . 'node/1/body/en/full', '', $edit, ['query' => [MainContentViewSubscriber::WRAPPER_FORMAT => 'drupal_ajax']]); $this->assertIdentical('{}', $response); $this->assertResponse(403); $post = array('nocssjs' => 'true'); - $response = $this->drupalPost('quickedit/entity/' . 'node/1', 'application/json', $post); + $response = $this->drupalPostWithFormat('quickedit/entity/' . 'node/1', 'json', $post); $this->assertIdentical('{"message":""}', $response); $this->assertResponse(403); } @@ -166,7 +167,7 @@ public function testUserWithPermission() { // Retrieving the metadata should result in a 200 JSON response. $htmlPageDrupalSettings = $this->drupalSettings; $post = array('fields[0]' => 'node/1/body/en/full'); - $response = $this->drupalPost('quickedit/metadata', 'application/json', $post); + $response = $this->drupalPostWithFormat('quickedit/metadata', 'json', $post); $this->assertResponse(200); $expected = array( 'node/1/body/en/full' => array( @@ -239,7 +240,7 @@ public function testUserWithPermission() { // Save the entity by moving the PrivateTempStore values to entity storage. $post = array('nocssjs' => 'true'); - $response = $this->drupalPost('quickedit/entity/' . 'node/1', 'application/json', $post); + $response = $this->drupalPostWithFormat('quickedit/entity/' . 'node/1', 'json', $post); $this->assertResponse(200); $ajax_commands = Json::decode($response); $this->assertIdentical(1, count($ajax_commands), 'The entity submission HTTP request results in one AJAX command.'); @@ -293,7 +294,7 @@ public function testUserWithPermission() { // Save the entity. $post = array('nocssjs' => 'true'); - $response = $this->drupalPost('quickedit/entity/' . 'node/1', 'application/json', $post); + $response = $this->drupalPostWithFormat('quickedit/entity/' . 'node/1', 'json', $post); $this->assertResponse(200); $ajax_commands = Json::decode($response); $this->assertIdentical(1, count($ajax_commands)); @@ -325,7 +326,7 @@ public function testTitleBaseField() { // Retrieving the metadata should result in a 200 JSON response. $htmlPageDrupalSettings = $this->drupalSettings; $post = array('fields[0]' => 'node/1/title/en/full'); - $response = $this->drupalPost('quickedit/metadata', 'application/json', $post); + $response = $this->drupalPostWithFormat('quickedit/metadata', 'json', $post); $this->assertResponse(200); $expected = array( 'node/1/title/en/full' => array( @@ -383,7 +384,7 @@ public function testTitleBaseField() { // Save the entity by moving the PrivateTempStore values to entity storage. $post = array('nocssjs' => 'true'); - $response = $this->drupalPost('quickedit/entity/' . 'node/1', 'application/json', $post); + $response = $this->drupalPostWithFormat('quickedit/entity/' . 'node/1', 'json', $post); $this->assertResponse(200); $ajax_commands = Json::decode($response); $this->assertIdentical(1, count($ajax_commands), 'The entity submission HTTP request results in one AJAX command.'); diff --git a/core/modules/rest/src/Plugin/views/display/RestExport.php b/core/modules/rest/src/Plugin/views/display/RestExport.php index 0bce1becb768..2886079cd9cb 100644 --- a/core/modules/rest/src/Plugin/views/display/RestExport.php +++ b/core/modules/rest/src/Plugin/views/display/RestExport.php @@ -266,10 +266,13 @@ public function collectRoutes(RouteCollection $collection) { $route->setMethods(['GET']); // Format as a string using pipes as a delimiter. - $requirements['_format'] = implode('|', $style_plugin->getFormats()); - - // Add the new requirements to the route. - $route->addRequirements($requirements); + if ($formats = $style_plugin->getFormats()) { + // Allow a REST Export View to be returned with an HTML-only accept + // format. That allows browsers or other non-compliant systems to access + // the view, as it is unlikely to have a conflicting HTML representation + // anyway. + $route->setRequirement('_format', implode('|', $formats + ['html'])); + } } } diff --git a/core/modules/rest/src/Plugin/views/style/Serializer.php b/core/modules/rest/src/Plugin/views/style/Serializer.php index ba3ca6f07359..ceeb39f6636f 100644 --- a/core/modules/rest/src/Plugin/views/style/Serializer.php +++ b/core/modules/rest/src/Plugin/views/style/Serializer.php @@ -146,11 +146,7 @@ public function render() { * An array of formats. */ public function getFormats() { - if (!empty($this->options['formats'])) { - return $this->options['formats']; - } - - return $this->formats; + return $this->options['formats']; } } diff --git a/core/modules/rest/src/Tests/AuthTest.php b/core/modules/rest/src/Tests/AuthTest.php index c222b26e354b..4552fc681a12 100644 --- a/core/modules/rest/src/Tests/AuthTest.php +++ b/core/modules/rest/src/Tests/AuthTest.php @@ -38,7 +38,7 @@ public function testRead() { $entity->save(); // Try to read the resource as an anonymous user, which should not work. - $this->httpRequest($entity->urlInfo(), 'GET', NULL, $this->defaultMimeType); + $this->httpRequest($entity->urlInfo()->setRouteParameter('_format', $this->defaultFormat), 'GET'); $this->assertResponse('401', 'HTTP response code is 401 when the request is not authenticated and the user is anonymous.'); $this->assertRaw(json_encode(['message' => 'A fatal error occurred: No authentication credentials provided.'])); @@ -55,7 +55,7 @@ public function testRead() { // Try to read the resource with session cookie authentication, which is // not enabled and should not work. - $this->httpRequest($entity->urlInfo(), 'GET', NULL, $this->defaultMimeType); + $this->httpRequest($entity->urlInfo()->setRouteParameter('_format', $this->defaultFormat), 'GET'); $this->assertResponse('403', 'HTTP response code is 403 when the request was authenticated by the wrong authentication provider.'); // Ensure that cURL settings/headers aren't carried over to next request. @@ -63,7 +63,7 @@ public function testRead() { // Now read it with the Basic authentication which is enabled and should // work. - $this->basicAuthGet($entity->urlInfo(), $account->getUsername(), $account->pass_raw, $this->defaultMimeType); + $this->basicAuthGet($entity->urlInfo()->setRouteParameter('_format', $this->defaultFormat), $account->getUsername(), $account->pass_raw); $this->assertResponse('200', 'HTTP response code is 200 for successfully authenticated requests.'); $this->curlClose(); } diff --git a/core/modules/rest/src/Tests/NodeTest.php b/core/modules/rest/src/Tests/NodeTest.php index 7169102eebde..2c8de1e47417 100644 --- a/core/modules/rest/src/Tests/NodeTest.php +++ b/core/modules/rest/src/Tests/NodeTest.php @@ -51,14 +51,14 @@ public function testNodes() { $node = $this->entityCreate('node'); $node->save(); - $this->httpRequest($node->urlInfo(), 'GET', NULL, $this->defaultMimeType); + $this->httpRequest($node->urlInfo()->setRouteParameter('_format', $this->defaultFormat), 'GET'); $this->assertResponse(200); $this->assertHeader('Content-type', $this->defaultMimeType); // Also check that JSON works and the routing system selects the correct // REST route. $this->enableService('entity:node', 'GET', 'json'); - $this->httpRequest($node->urlInfo(), 'GET', NULL, 'application/json'); + $this->httpRequest($node->urlInfo()->setRouteParameter('_format', 'json'), 'GET'); $this->assertResponse(200); $this->assertHeader('Content-type', 'application/json'); diff --git a/core/modules/rest/src/Tests/PageCacheTest.php b/core/modules/rest/src/Tests/PageCacheTest.php index 9cf2859ec37b..573238f810ac 100644 --- a/core/modules/rest/src/Tests/PageCacheTest.php +++ b/core/modules/rest/src/Tests/PageCacheTest.php @@ -35,14 +35,14 @@ public function testConfigChangePageCache() { $entity = $this->entityCreate('entity_test'); $entity->save(); // Read it over the REST API. - $this->httpRequest($entity->urlInfo(), 'GET', NULL, $this->defaultMimeType); + $this->httpRequest($entity->urlInfo()->setRouteParameter('_format', $this->defaultFormat), 'GET', NULL, $this->defaultMimeType); $this->assertResponse(200, 'HTTP response code is correct.'); $this->assertHeader('x-drupal-cache', 'MISS'); $this->assertCacheTag('config:rest.settings'); $this->assertCacheTag('entity_test:1'); // Read it again, should be page-cached now. - $this->httpRequest($entity->urlInfo(), 'GET', NULL, $this->defaultMimeType); + $this->httpRequest($entity->urlInfo()->setRouteParameter('_format', $this->defaultFormat), 'GET', NULL, $this->defaultMimeType); $this->assertResponse(200, 'HTTP response code is correct.'); $this->assertHeader('x-drupal-cache', 'HIT'); $this->assertCacheTag('config:rest.settings'); @@ -51,7 +51,7 @@ public function testConfigChangePageCache() { // Trigger a config save which should clear the page cache, so we should get // a cache miss now for the same request. $this->config('rest.settings')->save(); - $this->httpRequest($entity->urlInfo(), 'GET', NULL, $this->defaultMimeType); + $this->httpRequest($entity->urlInfo()->setRouteParameter('_format', $this->defaultFormat), 'GET', NULL, $this->defaultMimeType); $this->assertResponse(200, 'HTTP response code is correct.'); $this->assertHeader('x-drupal-cache', 'MISS'); $this->assertCacheTag('config:rest.settings'); diff --git a/core/modules/rest/src/Tests/ReadTest.php b/core/modules/rest/src/Tests/ReadTest.php index 3be342ee865c..983b54742419 100644 --- a/core/modules/rest/src/Tests/ReadTest.php +++ b/core/modules/rest/src/Tests/ReadTest.php @@ -8,6 +8,7 @@ namespace Drupal\rest\Tests; use Drupal\Component\Serialization\Json; +use Drupal\Core\Url; use Drupal\rest\Tests\RESTTestBase; /** @@ -44,7 +45,7 @@ public function testRead() { $entity = $this->entityCreate($entity_type); $entity->save(); // Read it over the REST API. - $response = $this->httpRequest($entity->urlInfo(), 'GET', NULL, $this->defaultMimeType); + $response = $this->httpRequest($entity->urlInfo()->setRouteParameter('_format', $this->defaultFormat), 'GET'); $this->assertResponse('200', 'HTTP response code is correct.'); $this->assertHeader('content-type', $this->defaultMimeType); $data = Json::decode($response); @@ -53,12 +54,12 @@ 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->urlInfo(), 'GET', NULL, 'application/wrongformat'); - $this->assertResponse(200); - $this->assertHeader('Content-type', 'text/html; charset=UTF-8'); + $response = $this->httpRequest($entity->urlInfo()->setRouteParameter('_format', 'wrongformat'), 'GET'); + $this->assertResponse(406); + $this->assertHeader('Content-type', 'application/json'); // Try to read an entity that does not exist. - $response = $this->httpRequest($entity_type . '/9999', 'GET', NULL, $this->defaultMimeType); + $response = $this->httpRequest(Url::fromUri('base://' . $entity_type . '/9999', ['query' => ['_format' => $this->defaultFormat]]), 'GET'); $this->assertResponse(404); $path = $entity_type == 'node' ? '/node/{node}' : '/entity_test/{entity_test}'; $expected_message = Json::encode(['message' => 'The "' . $entity_type . '" parameter was not converted for the path "' . $path . '" (route name: "rest.entity.' . $entity_type . '.GET.hal_json")']); @@ -70,7 +71,7 @@ public function testRead() { if ($entity_type == 'entity_test') { $entity->field_test_text->value = 'no access value'; $entity->save(); - $response = $this->httpRequest($entity->urlInfo(), 'GET', NULL, $this->defaultMimeType); + $response = $this->httpRequest($entity->urlInfo()->setRouteParameter('_format', $this->defaultFormat), 'GET'); $this->assertResponse(200); $this->assertHeader('content-type', $this->defaultMimeType); $data = Json::decode($response); @@ -79,18 +80,19 @@ public function testRead() { // Try to read an entity without proper permissions. $this->drupalLogout(); - $response = $this->httpRequest($entity->urlInfo(), 'GET', NULL, $this->defaultMimeType); + $response = $this->httpRequest($entity->urlInfo()->setRouteParameter('_format', $this->defaultFormat), 'GET'); $this->assertResponse(403); $this->assertIdentical('{"message":""}', $response); } // Try to read a resource which is not REST API enabled. $account = $this->drupalCreateUser(); $this->drupalLogin($account); - $response = $this->httpRequest($account->urlInfo(), 'GET', NULL, $this->defaultMimeType); - // AcceptHeaderMatcher considers the canonical, non-REST route a match, but - // a lower quality one: no format restrictions means there's always a match, - // and hence when there is no matching REST route, the non-REST route is - // used, but it can't render into application/hal+json, so it returns a 406. + $response = $this->httpRequest($account->urlInfo()->setRouteParameter('_format', $this->defaultFormat), 'GET'); + // \Drupal\Core\Routing\RequestFormatRouteFilter considers the canonical, + // non-REST route a match, but a lower quality one: no format restrictions + // means there's always a match and hence when there is no matching REST + // route, the non-REST route is used, but can't render into + // application/hal+json, so it returns a 406. $this->assertResponse('406', 'HTTP response code is 406 when the resource does not define formats, because it falls back to the canonical, non-REST route.'); $this->assertEqual($response, Json::encode([ 'message' => 'Not acceptable', @@ -115,7 +117,7 @@ public function testResourceStructure() { $entity->save(); // Read it over the REST API. - $response = $this->httpRequest($entity->urlInfo(), 'GET', NULL, 'application/json'); + $response = $this->httpRequest($entity->urlInfo()->setRouteParameter('_format', 'json'), 'GET'); $this->assertResponse('200', 'HTTP response code is correct.'); } diff --git a/core/modules/rest/src/Tests/ResourceTest.php b/core/modules/rest/src/Tests/ResourceTest.php index 5144ae325a06..af2208575cce 100644 --- a/core/modules/rest/src/Tests/ResourceTest.php +++ b/core/modules/rest/src/Tests/ResourceTest.php @@ -7,8 +7,6 @@ namespace Drupal\rest\Tests; -use Drupal\rest\Tests\RESTTestBase; - /** * Tests the structure of a REST resource. * @@ -23,6 +21,13 @@ class ResourceTest extends RESTTestBase { */ public static $modules = array('hal', 'rest', 'entity_test'); + /** + * The entity. + * + * @var \Drupal\Core\Entity\EntityInterface + */ + protected $entity; + /** * {@inheritdoc} */ @@ -55,11 +60,12 @@ public function testFormats() { $this->rebuildCache(); // Verify that accessing the resource returns 406. - $response = $this->httpRequest($this->entity->urlInfo(), 'GET', NULL, $this->defaultMimeType); - // AcceptHeaderMatcher considers the canonical, non-REST route a match, but - // a lower quality one: no format restrictions means there's always a match, - // and hence when there is no matching REST route, the non-REST route is - // used, but it can't render into application/hal+json, so it returns a 406. + $response = $this->httpRequest($this->entity->urlInfo()->setRouteParameter('_format', $this->defaultFormat), 'GET'); + // \Drupal\Core\Routing\RequestFormatRouteFilter considers the canonical, + // non-REST route a match, but a lower quality one: no format restrictions + // means there's always a match and hence when there is no matching REST + // route, the non-REST route is used, but can't render into + // application/hal+json, so it returns a 406. $this->assertResponse('406', 'HTTP response code is 406 when the resource does not define formats, because it falls back to the canonical, non-REST route.'); $this->curlClose(); } @@ -84,11 +90,12 @@ public function testAuthentication() { $this->rebuildCache(); // Verify that accessing the resource returns 401. - $response = $this->httpRequest($this->entity->urlInfo(), 'GET', NULL, $this->defaultMimeType); - // AcceptHeaderMatcher considers the canonical, non-REST route a match, but - // a lower quality one: no format restrictions means there's always a match, - // and hence when there is no matching REST route, the non-REST route is - // used, but it can't render into application/hal+json, so it returns a 406. + $response = $this->httpRequest($this->entity->urlInfo()->setRouteParameter('_format', $this->defaultFormat), 'GET'); + // \Drupal\Core\Routing\RequestFormatRouteFilter considers the canonical, + // non-REST route a match, but a lower quality one: no format restrictions + // means there's always a match and hence when there is no matching REST + // route, the non-REST route is used, but can't render into + // application/hal+json, so it returns a 406. $this->assertResponse('406', 'HTTP response code is 406 when the resource does not define formats, because it falls back to the canonical, non-REST route.'); $this->curlClose(); } diff --git a/core/modules/rest/src/Tests/Views/StyleSerializerTest.php b/core/modules/rest/src/Tests/Views/StyleSerializerTest.php index 0a14c3ccfbc8..9ca4c5c98c3d 100644 --- a/core/modules/rest/src/Tests/Views/StyleSerializerTest.php +++ b/core/modules/rest/src/Tests/Views/StyleSerializerTest.php @@ -76,7 +76,7 @@ public function testSerializerResponses() { $view->initDisplay(); $this->executeView($view); - $actual_json = $this->drupalGet('test/serialize/field', array(), array('Accept: application/json')); + $actual_json = $this->drupalGetWithFormat('test/serialize/field', 'json'); $this->assertResponse(200); $this->assertCacheTags($view->getCacheTags()); // @todo Due to https://www.drupal.org/node/2352009 we can't yet test the @@ -125,7 +125,7 @@ public function testSerializerResponses() { $expected = $serializer->serialize($entities, 'json'); - $actual_json = $this->drupalGet('test/serialize/entity', array(), array('Accept: application/json')); + $actual_json = $this->drupalGetWithFormat('test/serialize/entity', 'json'); $this->assertResponse(200); $this->assertIdentical($actual_json, $expected, 'The expected JSON output was found.'); $expected_cache_tags = $view->getCacheTags(); @@ -137,7 +137,7 @@ public function testSerializerResponses() { $this->assertCacheTags($expected_cache_tags); $expected = $serializer->serialize($entities, 'hal_json'); - $actual_json = $this->drupalGet('test/serialize/entity', array(), array('Accept: application/hal+json')); + $actual_json = $this->drupalGetWithFormat('test/serialize/entity', 'hal_json'); $this->assertIdentical($actual_json, $expected, 'The expected HAL output was found.'); $this->assertCacheTags($expected_cache_tags); @@ -171,10 +171,10 @@ public function testSerializerResponses() { )); $view->save(); $expected = $serializer->serialize($entities, 'json'); - $actual_json = $this->drupalGet('test/serialize/entity', array(), array('Accept: application/json')); + $actual_json = $this->drupalGetWithFormat('test/serialize/entity', 'json'); $this->assertIdentical($actual_json, $expected, 'The expected JSON output was found.'); $expected = $serializer->serialize($entities, 'xml'); - $actual_xml = $this->drupalGet('test/serialize/entity', array(), array('Accept: application/xml')); + $actual_xml = $this->drupalGetWithFormat('test/serialize/entity', 'xml'); $this->assertIdentical($actual_xml, $expected, 'The expected XML output was found.'); } @@ -191,10 +191,10 @@ public function testResponseFormatConfiguration() { $this->drupalPostForm(NULL, array(), t('Save')); // Should return a 406. - $this->drupalGet('test/serialize/field', array(), array('Accept: application/json')); + $this->drupalGetWithFormat('test/serialize/field', 'json'); $this->assertResponse(406, 'A 406 response was returned when JSON was requested.'); // Should return a 200. - $this->drupalGet('test/serialize/field', array(), array('Accept: application/xml')); + $this->drupalGetWithFormat('test/serialize/field', 'xml'); $this->assertResponse(200, 'A 200 response was returned when XML was requested.'); // Add 'json' as an accepted format, so we have multiple. @@ -203,7 +203,7 @@ public function testResponseFormatConfiguration() { // Should return a 200. // @todo This should be fixed when we have better content negotiation. - $this->drupalGet('test/serialize/field', array(), array('Accept: */*')); + $this->drupalGet('test/serialize/field'); $this->assertResponse(200, 'A 200 response was returned when any format was requested.'); // Should return a 200. Emulates a sample Firefox header. @@ -211,14 +211,27 @@ public function testResponseFormatConfiguration() { $this->assertResponse(200, 'A 200 response was returned when a browser accept header was requested.'); // Should return a 200. - $this->drupalGet('test/serialize/field', array(), array('Accept: application/json')); + $this->drupalGetWithFormat('test/serialize/field', 'json'); $this->assertResponse(200, 'A 200 response was returned when JSON was requested.'); // Should return a 200. - $this->drupalGet('test/serialize/field', array(), array('Accept: application/xml')); + $this->drupalGetWithFormat('test/serialize/field', 'xml'); $this->assertResponse(200, 'A 200 response was returned when XML was requested'); // Should return a 406. - $this->drupalGet('test/serialize/field', array(), array('Accept: application/html')); - $this->assertResponse(406, 'A 406 response was returned when HTML was requested.'); + $this->drupalGetWithFormat('test/serialize/field', 'html'); + $this->assertResponse(200, 'A 200 response was returned when HTML was requested.'); + + // Now configure now format, so all of them should be allowed. + $this->drupalPostForm($style_options, array('style_options[formats][json]' => '0', 'style_options[formats][xml]' => '0'), t('Apply')); + + // Should return a 200. + $this->drupalGetWithFormat('test/serialize/field', 'json'); + $this->assertResponse(200, 'A 200 response was returned when JSON was requested.'); + // Should return a 200. + $this->drupalGetWithFormat('test/serialize/field', 'xml'); + $this->assertResponse(200, 'A 200 response was returned when XML was requested'); + // Should return a 200. + $this->drupalGetWithFormat('test/serialize/field', 'html'); + $this->assertResponse(200, 'A 200 response was returned when HTML was requested.'); } /** diff --git a/core/modules/simpletest/src/WebTestBase.php b/core/modules/simpletest/src/WebTestBase.php index 2cf859af2b53..77a872368709 100644 --- a/core/modules/simpletest/src/WebTestBase.php +++ b/core/modules/simpletest/src/WebTestBase.php @@ -1395,8 +1395,27 @@ protected function drupalGet($path, array $options = array(), array $headers = a * Requests a Drupal path in JSON format, and JSON decodes the response. */ protected function drupalGetJSON($path, array $options = array(), array $headers = array()) { - $headers[] = 'Accept: application/json'; - return Json::decode($this->drupalGet($path, $options, $headers)); + return Json::decode($this->drupalGetWithFormat($path, 'json', $options, $headers)); + } + + /** + * Retrieves a Drupal path or an absolute path for a given format. + * + * @param string $path + * Path to request AJAX from. + * @param string $format + * The wanted request format. + * @param array $options + * Array of URL options. + * @param array $headers + * Array of headers. + * + * @return mixed + * The result of the request. + */ + protected function drupalGetWithFormat($path, $format, array $options = [], array $headers = []) { + $options += ['query' => ['_format' => $format]]; + return $this->drupalGet($path, $options, $headers); } /** @@ -1890,6 +1909,34 @@ protected function drupalPost($path, $accept, array $post, $options = array()) { )); } + /** + * Performs a POST HTTP request with a specific format. + * + * @param string $path + * Drupal path where the request should be POSTed to. Will be transformed + * into an absolute path automatically. + * @param string $format + * The request format. + * @param array $post + * The POST data. When making a 'application/vnd.drupal-ajax' request, the + * Ajax page state data should be included. Use getAjaxPageStatePostData() + * for that. + * @param array $options + * (optional) Options to be forwarded to the url generator. The 'absolute' + * option will automatically be enabled. + * + * @return string + * The content returned from the call to curl_exec(). + * + * @see WebTestBase::drupalPost + * @see WebTestBase::getAjaxPageStatePostData() + * @see WebTestBase::curlExec() + */ + protected function drupalPostWithFormat($path, $format, array $post, $options = []) { + $options['query']['_format'] = $format; + return $this->drupalPost($path, '', $post, $options); + } + /** * Get the Ajax page state from drupalSettings and prepare it for POSTing. * @@ -2616,6 +2663,9 @@ protected function prepareRequestForGenerator($clean_urls = TRUE, $override_serv */ protected function buildUrl($path, array $options = array()) { if ($path instanceof Url) { + $url_options = $path->getOptions(); + $options = $url_options + $options; + $path->setOptions($options); return $path->setAbsolute()->toString(); } // The URL generator service is not necessarily available yet; e.g., in diff --git a/core/modules/system/src/Tests/Routing/ContentNegotiationRoutingTest.php b/core/modules/system/src/Tests/Routing/ContentNegotiationRoutingTest.php new file mode 100644 index 000000000000..e9430b0861f2 --- /dev/null +++ b/core/modules/system/src/Tests/Routing/ContentNegotiationRoutingTest.php @@ -0,0 +1,157 @@ +<?php + +/** + * @file + * Contains \Drupal\conneg_test\Tests\ContentNegotiationRoutingTest. + */ + +namespace Drupal\system\Tests\Routing; + +use Drupal\Core\DependencyInjection\ContainerBuilder; +use Drupal\simpletest\KernelTestBase; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; + +/** + * Tests content negotiation routing variations. + * + * @group ContentNegotiation + */ +class ContentNegotiationRoutingTest extends KernelTestBase { + + /** + * {@inheritdoc} + */ + public static $modules = ['system', 'conneg_test']; + + /** + * {@inheritdoc} + */ + protected function setUp() { + \Drupal::unsetContainer(); + parent::setUp(); + + $this->installSchema('system', ['router', 'url_alias']); + \Drupal::service('router.builder')->rebuild(); + } + + /** + * {@inheritdoc} + */ + public function containerBuild(ContainerBuilder $container) { + parent::containerBuild($container); + + // \Drupal\simpletest\KernelTestBase::containerBuild() removes the alias path + // processor. + if ($container->hasDefinition('path_processor_alias')) { + $definition = $container->getDefinition('path_processor_alias'); + $definition->addTag('path_processor_inbound', ['priority' => 100])->addTag('path_processor_outbound', ['priority' => 300]); + } + } + + /** + * Tests the content negotiation aspect of routing. + */ + function testContentRouting() { + /** @var \Drupal\Core\Path\AliasStorageInterface $path_alias_storage */ + $path_alias_storage = $this->container->get('path.alias_storage'); + // Alias with extension pointing to no extension/constant content-type. + $path_alias_storage->save('conneg/html', 'alias.html'); + + // Alias with extension pointing to dynamic extension/linked content-type. + $path_alias_storage->save('conneg/html?_format=json', 'alias.json'); + + $tests = [ + // ['path', 'accept', 'content-type'], + + // Extension is part of the route path. Constant Content-type. + ['conneg/simple.json', '', 'application/json'], + ['conneg/simple.json', 'application/xml', 'application/json'], + ['conneg/simple.json', 'application/json', 'application/json'], + // No extension. Constant Content-type. + ['conneg/html', '', 'text/html'], + ['conneg/html', '*/*', 'text/html'], + ['conneg/html', 'application/xml', 'text/html'], + ['conneg/html', 'text/xml', 'text/html'], + ['conneg/html', 'text/html', 'text/html'], + // Dynamic extension. Linked Content-type. + ['conneg/html?_format=json', '', 'application/json'], + ['conneg/html?_format=json', '*/*', 'application/json'], + ['conneg/html?_format=json', 'application/xml', 'application/json'], + ['conneg/html?_format=json', 'application/json', 'application/json'], + ['conneg/html?_format=xml', '', 'application/xml'], + ['conneg/html?_format=xml', '*/*', 'application/xml'], + ['conneg/html?_format=xml', 'application/json', 'application/xml'], + ['conneg/html?_format=xml', 'application/xml', 'application/xml'], + + // Path with a variable. Variable contains a period. + ['conneg/plugin/plugin.id', '', 'text/html'], + ['conneg/plugin/plugin.id', '*/*', 'text/html'], + ['conneg/plugin/plugin.id', 'text/xml', 'text/html'], + ['conneg/plugin/plugin.id', 'text/html', 'text/html'], + + // Alias with extension pointing to no extension/constant content-type. + ['alias.html', '', 'text/html'], + ['alias.html', '*/*', 'text/html'], + ['alias.html', 'text/xml', 'text/html'], + ['alias.html', 'text/html', 'text/html'], + ]; + + foreach ($tests as $test) { + $path = $test[0]; + $accept_header = $test[1]; + $content_type = $test[2]; + $message = "Testing path:$path Accept:$accept_header Content-type:$content_type"; + $request = Request::create($path); + if ($accept_header) { + $request->headers->set('Accept', $accept_header); + } + + /** @var \Symfony\Component\HttpKernel\HttpKernelInterface $kernel */ + $kernel = \Drupal::getContainer()->get('http_kernel'); + $response = $kernel->handle($request); + // Verbose message since simpletest doesn't let us provide a message and + // see the error. + $this->assertTrue(TRUE, $message); + $this->assertEqual($response->getStatusCode(), Response::HTTP_OK); + $this->assertTrue(strpos($response->headers->get('Content-type'), $content_type) !== FALSE); + } + } + + /** + * Full negotiation by header only. + */ + public function testFullNegotiation() { + $this->enableModules(['accept_header_routing_test']); + \Drupal::service('router.builder')->rebuild(); + $tests = [ + // ['path', 'accept', 'content-type'], + + ['conneg/negotiate', '', 'text/html'], // 406? + ['conneg/negotiate', '', 'text/html'], // 406? + // ['conneg/negotiate', '*/*', '??'], + ['conneg/negotiate', 'application/json', 'application/json'], + ['conneg/negotiate', 'application/xml', 'application/xml'], + ['conneg/negotiate', 'application/json', 'application/json'], + ['conneg/negotiate', 'application/xml', 'application/xml'], + ]; + + foreach ($tests as $test) { + $path = $test[0]; + $accept_header = $test[1]; + $content_type = $test[2]; + $message = "Testing path:$path Accept:$accept_header Content-type:$content_type"; + $request = Request::create($path); + $request->headers->set('Accept', $accept_header); + + /** @var \Symfony\Component\HttpKernel\HttpKernelInterface $kernel */ + $kernel = \Drupal::getContainer()->get('http_kernel'); + $response = $kernel->handle($request); + // Verbose message since simpletest doesn't let us provide a message and + // see the error. + $this->pass($message); + $this->assertEqual($response->getStatusCode(), Response::HTTP_OK); + } + } + +} diff --git a/core/modules/system/src/Tests/Routing/ExceptionHandlingTest.php b/core/modules/system/src/Tests/Routing/ExceptionHandlingTest.php index 401420ca73b6..ee3c7630865f 100644 --- a/core/modules/system/src/Tests/Routing/ExceptionHandlingTest.php +++ b/core/modules/system/src/Tests/Routing/ExceptionHandlingTest.php @@ -39,8 +39,8 @@ protected function setUp() { */ public function testJson403() { $request = Request::create('/router_test/test15'); - $request->headers->set('Accept', 'application/json'); - $request->setFormat('json', ['application/json']); + $request->query->set('_format', 'json'); + $request->setRequestFormat('json'); /** @var \Symfony\Component\HttpKernel\HttpKernelInterface $kernel */ $kernel = \Drupal::getContainer()->get('http_kernel'); @@ -56,8 +56,8 @@ public function testJson403() { */ public function testJson404() { $request = Request::create('/not-found'); - $request->headers->set('Accept', 'application/json'); - $request->setFormat('json', ['application/json']); + $request->query->set('_format', 'json'); + $request->setRequestFormat('json'); /** @var \Symfony\Component\HttpKernel\HttpKernelInterface $kernel */ $kernel = \Drupal::getContainer()->get('http_kernel'); @@ -73,7 +73,6 @@ public function testJson404() { */ public function testHtml403() { $request = Request::create('/router_test/test15'); - $request->headers->set('Accept', 'text/html'); $request->setFormat('html', ['text/html']); /** @var \Symfony\Component\HttpKernel\HttpKernelInterface $kernel */ @@ -89,7 +88,6 @@ public function testHtml403() { */ public function testHtml404() { $request = Request::create('/not-found'); - $request->headers->set('Accept', 'text/html'); $request->setFormat('html', ['text/html']); /** @var \Symfony\Component\HttpKernel\HttpKernelInterface $kernel */ @@ -108,7 +106,6 @@ public function testBacktraceEscaping() { $this->config('system.logging')->set('error_level', ERROR_REPORTING_DISPLAY_VERBOSE)->save(); $request = Request::create('/router_test/test17'); - $request->headers->set('Accept', 'text/html'); $request->setFormat('html', ['text/html']); /** @var \Symfony\Component\HttpKernel\HttpKernelInterface $kernel */ diff --git a/core/modules/system/src/Tests/System/ResponseGeneratorTest.php b/core/modules/system/src/Tests/System/ResponseGeneratorTest.php index 061f7f9bfeda..8684624b3e9b 100644 --- a/core/modules/system/src/Tests/System/ResponseGeneratorTest.php +++ b/core/modules/system/src/Tests/System/ResponseGeneratorTest.php @@ -62,7 +62,7 @@ function testGeneratorHeaderAdded() { $this->enableService('entity:node', 'GET', 'json'); // Tests to see if this also works for a non-html request - $this->httpRequest($node->urlInfo(), 'GET', NULL, 'application/json'); + $this->httpRequest($node->urlInfo()->setOption('query', ['_format' => 'json']), 'GET'); $this->assertResponse(200); $this->assertEqual('application/json', $this->drupalGetHeader('Content-Type')); $this->assertEqual($expectedGeneratorHeader, $this->drupalGetHeader('X-Generator')); diff --git a/core/modules/system/tests/modules/accept_header_routing_test/accept_header_routing_test.info.yml b/core/modules/system/tests/modules/accept_header_routing_test/accept_header_routing_test.info.yml new file mode 100644 index 000000000000..d93e67b2e32f --- /dev/null +++ b/core/modules/system/tests/modules/accept_header_routing_test/accept_header_routing_test.info.yml @@ -0,0 +1,3 @@ +name: Accept header based routing test +core: 8.x +type: module diff --git a/core/modules/system/tests/modules/accept_header_routing_test/accept_header_routing_test.services.yml b/core/modules/system/tests/modules/accept_header_routing_test/accept_header_routing_test.services.yml new file mode 100644 index 000000000000..dc0ce1087ebd --- /dev/null +++ b/core/modules/system/tests/modules/accept_header_routing_test/accept_header_routing_test.services.yml @@ -0,0 +1,5 @@ +services: + accept_header_matcher: + class: Drupal\accept_header_routing_test\Routing\AcceptHeaderMatcher + tags: + - { name: route_filter } diff --git a/core/modules/system/tests/modules/accept_header_routing_test/src/AcceptHeaderMiddleware.php b/core/modules/system/tests/modules/accept_header_routing_test/src/AcceptHeaderMiddleware.php new file mode 100644 index 000000000000..51daedb7e79e --- /dev/null +++ b/core/modules/system/tests/modules/accept_header_routing_test/src/AcceptHeaderMiddleware.php @@ -0,0 +1,47 @@ +<?php + +/** + * @file + * Contains \Drupal\accept_header_routing_test\AcceptHeaderMiddleware. + */ + +namespace Drupal\accept_header_routing_test; + +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpKernel\HttpKernelInterface; + +/** + * Example implementation of accept header based content negotation. + */ +class AcceptHeaderMiddleware implements HttpKernelInterface { + + /** + * Constructs a new AcceptHeaderMiddleware instance. + * + * @param \Symfony\Component\HttpKernel\HttpKernelInterface $app + * The app. + */ + public function __construct(HttpKernelInterface $app) { + $this->app = $app; + } + + /** + * {@inheritdoc} + */ + public function handle(Request $request, $type = self::MASTER_REQUEST, $catch = TRUE) { + $mapping = [ + 'application/json' => 'json', + 'application/hal+json' => 'hal_json', + 'application/xml' => 'xml', + 'text/html' => 'html', + ]; + + $accept = $request->headers->get('Accept') ?: ['text/html']; + if (isset($mapping[$accept[0]])) { + $request->setRequestFormat($mapping[$accept[0]]); + } + + return $this->app->handle($request, $type, $catch); + } + +} diff --git a/core/modules/system/tests/modules/accept_header_routing_test/src/AcceptHeaderRoutingTestServiceProvider.php b/core/modules/system/tests/modules/accept_header_routing_test/src/AcceptHeaderRoutingTestServiceProvider.php new file mode 100644 index 000000000000..e1cab64743ad --- /dev/null +++ b/core/modules/system/tests/modules/accept_header_routing_test/src/AcceptHeaderRoutingTestServiceProvider.php @@ -0,0 +1,28 @@ +<?php + +/** + * @file + * Contains \Drupal\accept_header_routing_test\AcceptHeaderRoutingTestServiceProvider. + */ + +namespace Drupal\accept_header_routing_test; + +use Drupal\Core\DependencyInjection\ContainerBuilder; +use Drupal\Core\DependencyInjection\ServiceModifierInterface; + +/** + * Service provider for the accept_header_routing_test module. + */ +class AcceptHeaderRoutingTestServiceProvider implements ServiceModifierInterface { + + /** + * {@inheritdoc} + */ + public function alter(ContainerBuilder $container) { + // Remove the basic content negotation middleware and replace it with a + // basic header based one. + $container->register('http_middleware.negotiation', 'Drupal\accept_header_routing_test\AcceptHeaderMiddleware') + ->addTag('http_middleware', ['priority' => 400]); + } + +} diff --git a/core/lib/Drupal/Core/Routing/AcceptHeaderMatcher.php b/core/modules/system/tests/modules/accept_header_routing_test/src/Routing/AcceptHeaderMatcher.php similarity index 93% rename from core/lib/Drupal/Core/Routing/AcceptHeaderMatcher.php rename to core/modules/system/tests/modules/accept_header_routing_test/src/Routing/AcceptHeaderMatcher.php index 779480063f88..0f52fba23e44 100644 --- a/core/lib/Drupal/Core/Routing/AcceptHeaderMatcher.php +++ b/core/modules/system/tests/modules/accept_header_routing_test/src/Routing/AcceptHeaderMatcher.php @@ -2,12 +2,13 @@ /** * @file - * Contains Drupal\Core\Routing\AcceptHeaderMatcher. + * Contains \Drupal\accept_header_routing_test\Routing\AcceptHeaderMatcher. */ -namespace Drupal\Core\Routing; +namespace Drupal\accept_header_routing_test\Routing; use Drupal\Component\Utility\SafeMarkup; +use Drupal\Core\Routing\RouteFilterInterface; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpKernel\Exception\NotAcceptableHttpException; use Symfony\Component\Routing\Route; diff --git a/core/tests/Drupal/Tests/Core/Routing/AcceptHeaderMatcherTest.php b/core/modules/system/tests/modules/accept_header_routing_test/tests/Unit/AcceptHeaderMatcherTest.php similarity index 91% rename from core/tests/Drupal/Tests/Core/Routing/AcceptHeaderMatcherTest.php rename to core/modules/system/tests/modules/accept_header_routing_test/tests/Unit/AcceptHeaderMatcherTest.php index d0c811cafc78..4dc9ddf5fbd6 100644 --- a/core/tests/Drupal/Tests/Core/Routing/AcceptHeaderMatcherTest.php +++ b/core/modules/system/tests/modules/accept_header_routing_test/tests/Unit/AcceptHeaderMatcherTest.php @@ -2,12 +2,12 @@ /** * @file - * Contains Drupal\Tests\Core\Routing\AcceptHeaderMatcherTest. + * Contains \Drupal\Tests\accept_header_routing_test\Unit\Routing\AcceptHeaderMatcherTest. */ -namespace Drupal\Tests\Core\Routing; +namespace Drupal\Tests\accept_header_routing_teste\Unit\Routing; -use Drupal\Core\Routing\AcceptHeaderMatcher; +use Drupal\accept_header_routing_test\Routing\AcceptHeaderMatcher; use Drupal\Tests\Core\Routing\RoutingFixtures; use Drupal\Tests\UnitTestCase; use Symfony\Component\HttpFoundation\Request; @@ -22,14 +22,14 @@ class AcceptHeaderMatcherTest extends UnitTestCase { /** * A collection of shared fixture data for tests. * - * @var RoutingFixtures + * @var \Drupal\Tests\Core\Routing\RoutingFixtures */ protected $fixtures; /** * The matcher object that is going to be tested. * - * @var \Drupal\Core\Routing\AcceptHeaderMatcher + * @var \Drupal\accept_header_routing_test\Routing\AcceptHeaderMatcher */ protected $matcher; diff --git a/core/modules/system/tests/modules/conneg_test/conneg_test.info.yml b/core/modules/system/tests/modules/conneg_test/conneg_test.info.yml new file mode 100644 index 000000000000..07f4600b5c0d --- /dev/null +++ b/core/modules/system/tests/modules/conneg_test/conneg_test.info.yml @@ -0,0 +1,6 @@ +name: Content negotiation test +type: module +description: 'Support testing content negotiation variations.' +package: Core +version: VERSION +core: 8.x diff --git a/core/modules/system/tests/modules/conneg_test/conneg_test.routing.yml b/core/modules/system/tests/modules/conneg_test/conneg_test.routing.yml new file mode 100644 index 000000000000..bedd52efbb8f --- /dev/null +++ b/core/modules/system/tests/modules/conneg_test/conneg_test.routing.yml @@ -0,0 +1,32 @@ +# Tests +conneg.simpletest: + path: conneg/simple.json + defaults: + _controller: '\Drupal\conneg_test\Controller\TestController::simple' + requirements: + _access: 'TRUE' +conneg.html: + path: conneg/html + defaults: + _controller: '\Drupal\conneg_test\Controller\TestController::html' + requirements: + _access: 'TRUE' +conneg.simple_conneg: + path: conneg/html + defaults: + _controller: '\Drupal\conneg_test\Controller\TestController::format' + requirements: + _access: 'TRUE' + _format: 'json|xml' +conneg.variable_with_period: + path: conneg/plugin/{plugin_id} + defaults: + _controller: '\Drupal\conneg_test\Controller\TestController::variable' + requirements: + _access: 'TRUE' +conneg.full_content_negotiation: + path: conneg/negotiate + defaults: + _controller: '\Drupal\conneg_test\Controller\TestController::format' + requirements: + _access: 'TRUE' diff --git a/core/modules/system/tests/modules/conneg_test/src/Controller/TestController.php b/core/modules/system/tests/modules/conneg_test/src/Controller/TestController.php new file mode 100644 index 000000000000..c93a5a1d29af --- /dev/null +++ b/core/modules/system/tests/modules/conneg_test/src/Controller/TestController.php @@ -0,0 +1,76 @@ +<?php + +/** + * @file + * Contains \Drupal\conneg_test\Controller\Test. + */ + +namespace Drupal\conneg_test\Controller; + +use Symfony\Component\HttpFoundation\JsonResponse; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; + +/** + * Test controller for content negotation tests. + */ +class TestController { + + /** + * Returns a json response. + * + * @return \Symfony\Component\HttpFoundation\JsonResponse + */ + public function simple() { + return new JsonResponse(['some' => 'data']); + } + + /** + * Returns a simple render array. + * + * @return array + */ + public function html() { + return [ + '#markup' => 'here', + ]; + } + + /** + * Returns different responses dependening on the request format. + * + * @param \Symfony\Component\HttpFoundation\Request $request + * The request + * + * @return \Symfony\Component\HttpFoundation\Response + * The response. + */ + public function format(Request $request) { + switch ($request->getRequestFormat()) { + case 'json': + return new JsonResponse(['some' => 'data']); + + case 'xml': + return new Response('<xml></xml>', Response::HTTP_OK, ['Content-Type' => 'application/xml']); + + default: + return new Response($request->getRequestFormat()); + } + } + + /** + * Returns a render array depending on some passed in value. + * + * @param string $plugin_id + * The plugin ID. + * + * @return array + * The render array + */ + public function variable($plugin_id) { + return [ + '#markup' => $plugin_id, + ]; + } + +} diff --git a/core/modules/system/tests/modules/system_test/src/Controller/PageCacheAcceptHeaderController.php b/core/modules/system/tests/modules/system_test/src/Controller/PageCacheAcceptHeaderController.php index 43cded1d98c1..c24d7c51429b 100644 --- a/core/modules/system/tests/modules/system_test/src/Controller/PageCacheAcceptHeaderController.php +++ b/core/modules/system/tests/modules/system_test/src/Controller/PageCacheAcceptHeaderController.php @@ -25,7 +25,7 @@ class PageCacheAcceptHeaderController { * @return mixed */ public function content(Request $request) { - if ($request->headers->get('Accept') == 'application/json') { + if ($request->getRequestFormat() === 'json') { return new JsonResponse(array('content' => 'oh hai this is json')); } else { diff --git a/core/modules/views_ui/src/Tests/DisplayTest.php b/core/modules/views_ui/src/Tests/DisplayTest.php index a735c219bf4f..20667de6f7fc 100644 --- a/core/modules/views_ui/src/Tests/DisplayTest.php +++ b/core/modules/views_ui/src/Tests/DisplayTest.php @@ -188,7 +188,7 @@ public function testPageContextualLinks() { // Get server-rendered contextual links. // @see \Drupal\contextual\Tests\ContextualDynamicContextTest:renderContextualLinks() $post = array('ids[0]' => $id); - $response = $this->drupalPost('contextual/render', 'application/json', $post, array('query' => array('destination' => 'test-display'))); + $response = $this->drupalPostWithFormat('contextual/render', 'json', $post, array('query' => array('destination' => 'test-display'))); $this->assertResponse(200); $json = Json::decode($response); $this->assertIdentical($json[$id], '<ul class="contextual-links"><li class="entityviewedit-form"><a href="' . base_path() . 'admin/structure/views/view/test_display/edit/page_1">Edit view</a></li></ul>'); diff --git a/core/tests/Drupal/Tests/Core/ContentNegotiationTest.php b/core/tests/Drupal/Tests/Core/ContentNegotiationTest.php index d5f3081a9740..9c54eae2b4b9 100644 --- a/core/tests/Drupal/Tests/Core/ContentNegotiationTest.php +++ b/core/tests/Drupal/Tests/Core/ContentNegotiationTest.php @@ -44,36 +44,13 @@ public function testAjaxIframeUpload() { } /** - * Tests the getContentType() method when a priority format is found. - * - * @dataProvider priorityFormatProvider - * @covers ::getContentType - */ - public function testAPriorityFormatIsFound($priority, $format) { - $request = new Request(); - $request->setFormat($format['format'], $format['mime_type']); - $request->headers->set('Accept', sprintf('%s,application/json', $format['mime_type'])); - - $this->assertSame($priority, $this->contentNegotiation->getContentType($request)); - } - - public function priorityFormatProvider() - { - return [ - ['html', ['format' => 'html', 'mime_type' => 'text/html']], - ]; - } - - /** - * Tests the getContentType() method when no priority format is found but a valid one is found. - * - * @covers ::getContentType + * Tests the specifying a format via query parameters gets used. */ - public function testNoPriorityFormatIsFoundButReturnsTheFirstValidOne() { + public function testFormatViaQueryParameter() { $request = new Request(); - $request->headers->set('Accept', 'application/rdf+xml'); + $request->query->set('_format', 'bob'); - $this->assertSame('rdf', $this->contentNegotiation->getContentType($request)); + $this->assertSame('bob', $this->contentNegotiation->getContentType($request)); } /** diff --git a/core/tests/Drupal/Tests/Core/Routing/RequestFormatRouteFilterTest.php b/core/tests/Drupal/Tests/Core/Routing/RequestFormatRouteFilterTest.php new file mode 100644 index 000000000000..7c1db9661011 --- /dev/null +++ b/core/tests/Drupal/Tests/Core/Routing/RequestFormatRouteFilterTest.php @@ -0,0 +1,85 @@ +<?php + +/** + * @file + * Contains \Drupal\Tests\Core\Routing\RequestFormatRouteFilterTest. + */ + +namespace Drupal\Tests\Core\Routing; + +use Drupal\Core\Routing\RequestFormatRouteFilter; +use Drupal\Tests\UnitTestCase; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\Routing\Route; +use Symfony\Component\Routing\RouteCollection; + +/** + * @coversDefaultClass \Drupal\Core\Routing\RequestFormatRouteFilter + * @group Routing + */ +class RequestFormatRouteFilterTest extends UnitTestCase { + + /** + * @covers ::applies + */ + public function testAppliesWithoutFormat() { + $route_filter = new RequestFormatRouteFilter(); + $route = new Route('/test'); + $this->assertFalse($route_filter->applies($route)); + } + + /** + * @covers ::applies + */ + public function testAppliesWithFormat() { + $route_filter = new RequestFormatRouteFilter(); + $route = new Route('/test'); + $route->setRequirement('_format', 'json'); + $this->assertTrue($route_filter->applies($route)); + } + + /** + * @covers ::filter + */ + public function testFilter() { + $route_filter = new RequestFormatRouteFilter(); + + $route_without_format = new Route('/test'); + $route_with_format = $route = new Route('/test'); + $route_with_format->setRequirement('_format', 'json'); + $route_with_multiple_formats = $route = new Route('/test'); + $route_with_multiple_formats->setRequirement('_format', 'json|xml'); + + $collection = new RouteCollection(); + $collection->add('test_0', $route_without_format); + $collection->add('test_1', $route_with_format); + $collection->add('test_2', $route_with_multiple_formats); + + $request = new Request(); + $request->setRequestFormat('xml'); + $collection = $route_filter->filter($collection, $request); + + $this->assertCount(2, $collection); + $this->assertEquals(array_keys($collection->all())[0], 'test_2'); + $this->assertEquals(array_keys($collection->all())[1], 'test_0'); + } + + /** + * @covers ::filter + * @expectedException \Symfony\Component\HttpKernel\Exception\NotAcceptableHttpException + * @expectedExceptionMessage No route found for the specified format xml. + */ + public function testNoRouteFound() { + $collection = new RouteCollection(); + $route_with_format = $route = new Route('/test'); + $route_with_format->setRequirement('_format', 'json'); + $collection->add('test_0', $route_with_format); + $collection->add('test_1', clone $route_with_format); + + $request = Request::create('test?_format=xml', 'GET'); + $request->setRequestFormat('xml'); + $route_filter = new RequestFormatRouteFilter(); + $route_filter->filter($collection, $request); + } + +} -- GitLab