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..20b0284414 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. @@ -178,7 +181,7 @@ public function testSerializerResponses() { $this->assertIdentical($actual_json, $expected, 'The expected HAL output was found.'); $this->assertCacheTags($expected_cache_tags); - // Change the default format to xml. + // Change the format to xml. $view->setDisplay('rest_export_1'); $view->getDisplay()->setOption('style', array( 'type' => 'serializer', @@ -191,7 +194,7 @@ public function testSerializerResponses() { )); $view->save(); $expected = $serializer->serialize($entities, 'xml'); - $actual_xml = $this->drupalGet('test/serialize/entity'); + $actual_xml = $this->drupalGetWithFormat('test/serialize/entity', 'xml'); $this->assertIdentical($actual_xml, $expected, 'The expected XML output was found.'); $this->assertCacheContexts(['languages:language_interface', 'theme', 'entity_test_view_grants', 'request_format']); @@ -364,14 +367,19 @@ 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. + // Should return a 406. 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->assertHeader('content-type', 'text/html; charset=UTF-8'); + $this->assertResponse(406, 'A 406 response was returned when a browser accept header was requested.'); + + // Should return a 406. + $this->drupalGetWithFormat('test/serialize/field', 'html'); + $this->assertHeader('content-type', 'text/html; charset=UTF-8'); + $this->assertResponse(406, 'A 406 response was returned when HTML was requested.'); // Should return a 200. $this->drupalGetWithFormat('test/serialize/field', 'json'); @@ -385,14 +393,8 @@ public function testResponseFormatConfiguration() { $this->assertResponse(200, 'A 200 response was returned when XML was requested'); $headers = $this->drupalGetHeaders(); $this->assertTrue(strpos($headers['content-type'], 'text/xml') !== FALSE, 'The header Content-type is correct.'); - // Should return a 406. - $this->drupalGetWithFormat('test/serialize/field', 'html'); - // We want to show the first format by default, see - // \Drupal\rest\Plugin\views\style\Serializer::render. - $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. @@ -403,12 +405,6 @@ public function testResponseFormatConfiguration() { $this->drupalGetWithFormat('test/serialize/field', 'xml'); $this->assertHeader('content-type', 'text/xml; charset=UTF-8'); $this->assertResponse(200, 'A 200 response was returned when XML was requested'); - // Should return a 200. - $this->drupalGetWithFormat('test/serialize/field', 'html'); - // We want to show the first format by default, see - // \Drupal\rest\Plugin\views\style\Serializer::render. - $this->assertHeader('content-type', 'application/json'); - $this->assertResponse(200, 'A 200 response was returned when HTML was requested.'); } /** @@ -606,7 +602,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->drupalGetWithFormat('test/serialize/field', 'json'), (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/ViewEditForm.php b/core/modules/views_ui/src/ViewEditForm.php index 6e81939591..860453631a 100644 --- a/core/modules/views_ui/src/ViewEditForm.php +++ b/core/modules/views_ui/src/ViewEditForm.php @@ -15,6 +15,7 @@ use Drupal\views\Views; use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\HttpFoundation\RequestStack; +use Symfony\Component\HttpKernel\Exception\NotAcceptableHttpException; /** * Form controller for the Views edit form. @@ -414,15 +415,25 @@ public function getDisplayDetails($view, $display) { // path. elseif ($view->status() && $view->getExecutable()->displayHandlers->get($display['id'])->hasPath()) { $path = $view->getExecutable()->displayHandlers->get($display['id'])->getPath(); + if ($path && (strpos($path, '%') === FALSE)) { - if (!parse_url($path, PHP_URL_SCHEME)) { - // @todo Views should expect and store a leading /. See: - // https://www.drupal.org/node/2423913 - $url = Url::fromUserInput('/' . ltrim($path, '/')); + // Wrap this in a try/catch as trying to generate links to some + // routes may throw a NotAcceptableHttpException if they do not + // respond to HTML, such as RESTExports. + try { + if (!parse_url($path, PHP_URL_SCHEME)) { + // @todo Views should expect and store a leading /. See: + // https://www.drupal.org/node/2423913 + $url = Url::fromUserInput('/' . ltrim($path, '/')); + } + else { + $url = Url::fromUri("base:$path"); + } } - else { - $url = Url::fromUri("base:$path"); + catch (NotAcceptableHttpException $e) { + $url = '/' . $path; } + $build['top']['actions']['path'] = array( '#type' => 'link', '#title' => $this->t('View @display_title', ['@display_title' => $display_title]), 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;