diff --git a/core/modules/rest/src/Plugin/views/display/RestExport.php b/core/modules/rest/src/Plugin/views/display/RestExport.php index 74a27624db..b251a440d1 100644 --- a/core/modules/rest/src/Plugin/views/display/RestExport.php +++ b/core/modules/rest/src/Plugin/views/display/RestExport.php @@ -70,7 +70,7 @@ class RestExport extends PathPluginBase implements ResponseDisplayPluginInterfac * * @var string */ - protected $mimeType; + protected $mimeType = 'application/json'; /** * The renderer. @@ -140,20 +140,11 @@ public function initDisplay(ViewExecutable $view, array &$display, array &$optio parent::initDisplay($view, $display, $options); $request_content_type = $this->view->getRequest()->getRequestFormat(); - // Only use the requested content type if it's not 'html'. If it is then - // default to 'json' to aid debugging. - // @todo Remove the need for this when we have better content negotiation. - if ($request_content_type != 'html') { + + if ($request_content_type !== 'html') { $this->setContentType($request_content_type); + $this->setMimeType($this->view->getRequest()->getMimeType($request_content_type)); } - // If the requested content type is 'html' and the default 'json' is not - // selected as a format option in the view display, fallback to the first - // format in the array. - elseif (!empty($options['style']['options']['formats']) && !isset($options['style']['options']['formats'][$this->getContentType()])) { - $this->setContentType(reset($options['style']['options']['formats'])); - } - - $this->setMimeType($this->view->getRequest()->getMimeType($this->contentType)); } /** @@ -327,16 +318,12 @@ public function collectRoutes(RouteCollection $collection) { if ($route = $collection->get("view.$view_id.$display_id")) { $style_plugin = $this->getPlugin('style'); - // REST exports should only respond to get methods. + // REST exports should only respond to GET methods. $route->setMethods(['GET']); // Format as a string using pipes as a delimiter. 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'])); + $route->setRequirement('_format', implode('|', $formats)); } // Add authentication to the route if it was set. If no authentication was // set, the default authentication will be used, which is cookie based by @@ -421,15 +408,15 @@ public function render() { $build['#suffix'] = ''; unset($build['#markup']); } - elseif ($this->view->getRequest()->getFormat($this->view->element['#content_type']) !== 'html') { - // This display plugin is primarily for returning non-HTML formats. - // However, we still invoke the renderer to collect cacheability metadata. - // Because the renderer is designed for HTML rendering, it filters - // #markup for XSS unless it is already known to be safe, but that filter - // only works for HTML. Therefore, we mark the contents as safe to bypass - // the filter. So long as we are returning this in a non-HTML response - // (checked above), this is safe, because an XSS attack only works when - // executed by an HTML agent. + else { + // This display plugin is for returning non-HTML formats. However, we + // still invoke the renderer to collect cacheability metadata. Because the + // renderer is designed for HTML rendering, it filters #markup for XSS + // unless it is already known to be safe, but that filter only works for + // HTML. Therefore, we mark the contents as safe to bypass the filter. So + // long as we are returning this in a non-HTML response, + // this is safe, because an XSS attack only works when executed by an HTML + // agent. // @todo Decide how to support non-HTML in the render API in // https://www.drupal.org/node/2501313. $build['#markup'] = ViewsRenderPipelineMarkup::create($build['#markup']); diff --git a/core/modules/rest/src/Tests/Views/StyleSerializerTest.php b/core/modules/rest/src/Tests/Views/StyleSerializerTest.php index 4c004fcafc..a0a8dadb06 100644 --- a/core/modules/rest/src/Tests/Views/StyleSerializerTest.php +++ b/core/modules/rest/src/Tests/Views/StyleSerializerTest.php @@ -89,6 +89,9 @@ public function testRestViewsAuthentication() { $url = $this->buildUrl('test/serialize/auth_with_perm'); $response = \Drupal::httpClient()->get($url, [ 'auth' => [$this->adminUser->getUsername(), $this->adminUser->pass_raw], + 'query' => [ + '_format' => 'json', + ], ]); // Ensure that any changes to variables in the other thread are picked up. @@ -364,14 +367,14 @@ public function testResponseFormatConfiguration() { // Should return a 200. // @todo This should be fixed when we have better content negotiation. - $this->drupalGet('test/serialize/field'); + $this->drupalGetWithFormat('test/serialize/field', 'json'); $this->assertHeader('content-type', 'application/json'); $this->assertResponse(200, 'A 200 response was returned when any format was requested.'); // Should return a 200. Emulates a sample Firefox header. $this->drupalGet('test/serialize/field', array(), array('Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8')); $this->assertHeader('content-type', 'application/json'); - $this->assertResponse(200, 'A 200 response was returned when a browser accept header was requested.'); + $this->assertResponse(406, 'A 200 response was returned when a browser accept header was requested.'); // Should return a 200. $this->drupalGetWithFormat('test/serialize/field', 'json'); @@ -392,7 +395,7 @@ public function testResponseFormatConfiguration() { $this->assertHeader('content-type', 'application/json'); $this->assertResponse(200, 'A 200 response was returned when HTML was requested.'); - // Now configure now format, so all of them should be allowed. + // Now configure no 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. @@ -606,7 +609,7 @@ public function testSerializerViewsUI() { $this->assertResponse(200); // Check if we receive the expected result. $result = $this->xpath('//div[@id="views-live-preview"]/pre'); - $this->assertIdentical($this->drupalGet('test/serialize/field'), (string) $result[0], 'The expected JSON preview output was found.'); + $this->assertIdentical($this->drupalGetJSON('test/serialize/field'), (string) $result[0], 'The expected JSON preview output was found.'); } /** diff --git a/core/modules/views/src/Plugin/views/display/Page.php b/core/modules/views/src/Plugin/views/display/Page.php index d79b75b833..6d1f460a66 100644 --- a/core/modules/views/src/Plugin/views/display/Page.php +++ b/core/modules/views/src/Plugin/views/display/Page.php @@ -84,6 +84,18 @@ public static function create(ContainerInterface $container, array $configuratio ); } + /** + * {@inheritdoc} + */ + protected function getRoute($view_id, $display_id) { + $route = parent::getRoute($view_id, $display_id); + + // Explicitly set HTML as the format for Page displays. + $route->setRequirement('_format', 'html'); + + return $route; + } + /** * Sets the current page views render array. * diff --git a/core/modules/views_ui/src/ViewListBuilder.php b/core/modules/views_ui/src/ViewListBuilder.php index b38a15f75b..cc0bb53264 100644 --- a/core/modules/views_ui/src/ViewListBuilder.php +++ b/core/modules/views_ui/src/ViewListBuilder.php @@ -9,6 +9,7 @@ use Drupal\Core\Entity\EntityTypeInterface; use Drupal\Core\Url; use Symfony\Component\DependencyInjection\ContainerInterface; +use Symfony\Component\HttpKernel\Exception\NotAcceptableHttpException; /** * Defines a class to build a listing of view entities. @@ -255,9 +256,17 @@ protected function getDisplaysList(EntityInterface $view) { if ($display->hasPath()) { $path = $display->getPath(); if ($view->status() && strpos($path, '%') === FALSE) { - // @todo Views should expect and store a leading /. See: - // https://www.drupal.org/node/2423913 - $rendered_path = \Drupal::l('/' . $path, Url::fromUserInput('/' . $path)); + // Wrap this in a try/catch as tryng to generate links to some + // routes may throw a NotAcceptableHttpException if they do not + // respond to HTML, such as RESTExports. + try { + // @todo Views should expect and store a leading /. See: + // https://www.drupal.org/node/2423913 + $rendered_path = \Drupal::l('/' . $path, Url::fromUserInput('/' . $path)); + } + catch (NotAcceptableHttpException $e) { + $rendered_path = '/' . $path; + } } else { $rendered_path = '/' . $path;