diff --git a/core/lib/Drupal/Core/Render/HtmlResponseAttachmentsProcessor.php b/core/lib/Drupal/Core/Render/HtmlResponseAttachmentsProcessor.php index df18779..0b001c7 100644 --- a/core/lib/Drupal/Core/Render/HtmlResponseAttachmentsProcessor.php +++ b/core/lib/Drupal/Core/Render/HtmlResponseAttachmentsProcessor.php @@ -161,7 +161,7 @@ public function processAttachments(AttachmentsInterface $response) { ['html_head', 'feed', 'html_head_link', 'http_header', 'library', 'html_response_attachment_placeholders', 'placeholders', 'drupalSettings'] ); if (!empty($unsupported_types)) { - throw new \LogicException(sprintf('You are not allowed to use %s in #attached', implode(', ', $unsupported_types))); + throw new \LogicException(sprintf('You are not allowed to use %s in #attached.', implode(', ', $unsupported_types))); } // Get the placeholders from attached and then remove them. @@ -212,7 +212,7 @@ public function processAttachments(AttachmentsInterface $response) { // Now replace the attachment placeholders. $this->renderHtmlResponseAttachmentPlaceholders($response, $attachment_placeholders, $variables); - // Set the HTTP headers on the response if any bubbled. + // Set the HTTP headers and status code on the response if any bubbled. if (!empty($attached['http_header'])) { $this->setHeaders($response, $attached['http_header']); } @@ -318,6 +318,9 @@ protected function processAssetLibraries(array $attached, array $placeholders) { /** * Renders HTML response attachment placeholders. * + * This is the last step where all of the attachments are placed into the + * response object's contents. + * * @param \Drupal\Core\Render\HtmlResponse $response * The HTML response to update. * @param array $placeholders @@ -343,7 +346,13 @@ protected function renderHtmlResponseAttachmentPlaceholders(HtmlResponse $respon * @param \Drupal\Core\Render\HtmlResponse $response * The HTML response to update. * @param array $headers - * The headers to set. + * The headers to set, as an array. The items in this array should be as + * follows: + * - The header name. + * - The header value. + * - (optional) Whether to replace a current value with the new one, or add + * it to the others. If the value is not replaced, it will be appended, + * resulting in a header like this: 'Header: value1,value2' */ protected function setHeaders(HtmlResponse $response, array $headers) { foreach ($headers as $values) { @@ -392,14 +401,13 @@ protected function processHtmlHead(array $html_head) { * depending on options in the render array. Processing it can add to both the * html_head and http_header sections. * - * Each head link is specified by a two-element array: + * @param array $html_head_link + * The 'html_head_link' value of a render array. Each head link is specified + * by a two-element array: * - An array specifying the attributes of the link. * - A boolean specifying whether the link should also be a Link: HTTP * header. * - * @param array $html_head_link - * The 'html_head_link' value of a render array. - * * @return array * An ['#attached'] section of a render array. This allows us to easily * merge the results with other render arrays. The array could contain the @@ -432,6 +440,10 @@ protected function processHtmlHeadLink(array $html_head_link) { } /** + * Transform a 'feed' attachment into an 'html_head_link' attachment. + * + * The RSS feed is a special case of 'html_head_link', so we just turn it into + * one. * * @param array $attached_feed * The ['#attached']['feed'] portion of a render array. @@ -446,7 +458,7 @@ protected function processFeed($attached_feed) { $feed_link = [ 'href' => $item[0], 'rel' => 'alternate', - 'title' => empty($attached_feed[1]) ? '' : $item[1], + 'title' => empty($item[1]) ? '' : $item[1], 'type' => 'application/rss+xml', ]; $html_head_link[] = [$feed_link, FALSE]; diff --git a/core/modules/system/src/Tests/Render/HtmlResponseAttachmentsTest.php b/core/modules/system/src/Tests/Render/HtmlResponseAttachmentsTest.php index d8bcddb..d95b3fb 100644 --- a/core/modules/system/src/Tests/Render/HtmlResponseAttachmentsTest.php +++ b/core/modules/system/src/Tests/Render/HtmlResponseAttachmentsTest.php @@ -36,37 +36,31 @@ public function testAttachments() { $this->assertResponse(418); $this->assertHeader('X-Drupal-Cache', 'HIT'); - // Test ['#attached']['html_head'] with various replacement rules. - $this->drupalGet('/render_attached_test/head'); - $this->assertHeader('X-Test-Teapot', 'Teapot Mode Active'); - $this->assertHeader('X-Test-Teapot-Replace', 'Teapot replaced'); - $this->assertHeader('X-Test-Teapot-No-Replace', 'This value is not replaced,This one is added'); + // Test ['#attached']['http_header'] with various replacement rules. + $this->drupalGet('/render_attached_test/header'); + $this->assertTeapotHeaders(); $this->assertHeader('X-Drupal-Cache', 'MISS'); // Repeat for the cache. - $this->drupalGet('/render_attached_test/head'); + $this->drupalGet('/render_attached_test/header'); $this->assertHeader('X-Drupal-Cache', 'HIT'); // Test ['#attached']['feed']. $this->drupalGet('/render_attached_test/feed'); - // Discover the DOM element for the feed link. - $link = $this->xpath('//head/link[@href="test://url"]'); - $this->assertEqual(1, count($link), 'Link has URL.'); - // Reconcile the other attributes. - $link_attributes = [ - 'href' => 'test://url', - 'rel' => 'alternate', - 'type' => 'application/rss+xml', - 'title' => '', - ]; - $link = reset($link); - foreach ($link->attributes() as $attribute => $value) { - $this->assertEqual($value, $link_attributes[$attribute]); - } + $this->assertHeader('X-Drupal-Cache', 'MISS'); + $this->assertFeed(); // Repeat for the cache. $this->drupalGet('/render_attached_test/feed'); $this->assertHeader('X-Drupal-Cache', 'HIT'); - // Now repeat the process using drupal_process_attached(). + // Test ['#attached']['html_head']. + $this->drupalGet('/render_attached_test/head'); + $this->assertHeader('X-Drupal-Cache', 'MISS'); + $this->assertHead(); + // Repeat for the cache. + $this->drupalGet('/render_attached_test/head'); + $this->assertHeader('X-Drupal-Cache', 'HIT'); + + // Now repeat all of the preceeding using drupal_process_attached(). // Test ['#attached']['http_header] = ['Status', $code]. $this->drupalGet('/render_attached_test/teapot_dpa'); $this->assertResponse(418); @@ -76,29 +70,29 @@ public function testAttachments() { $this->assertResponse(418); $this->assertHeader('X-Drupal-Cache', 'HIT'); - // Test ['#attached']['html_head'] with various replacement rules. - $this->drupalGet('/render_attached_test/head_dpa'); - $this->assertHeader('X-Test-Teapot', 'Teapot Mode Active'); - $this->assertHeader('X-Test-Teapot-Replace', 'Teapot replaced'); - $this->assertHeader('X-Test-Teapot-No-Replace', 'This value is not replaced,This one is added'); + // Test ['#attached']['http_header'] with various replacement rules. + $this->drupalGet('/render_attached_test/header_dpa'); $this->assertHeader('X-Drupal-Cache', 'MISS'); + $this->assertTeapotHeaders(); // Repeat for the cache. - $this->drupalGet('/render_attached_test/head_dpa'); + $this->drupalGet('/render_attached_test/header_dpa'); $this->assertHeader('X-Drupal-Cache', 'HIT'); // Test ['#attached']['feed']. $this->drupalGet('/render_attached_test/feed_dpa'); - // Discover the DOM element for the feed link. - $link = $this->xpath('//head/link[@href="test://url"]'); - $this->assertEqual(1, count($link), 'Link has URL.'); - // Reconcile the other attributes. - $link = reset($link); - foreach ($link->attributes() as $attribute => $value) { - $this->assertEqual($value, $link_attributes[$attribute]); - } + $this->assertHeader('X-Drupal-Cache', 'MISS'); + $this->assertFeed(); // Repeat for the cache. $this->drupalGet('/render_attached_test/feed_dpa'); $this->assertHeader('X-Drupal-Cache', 'HIT'); + + // Test ['#attached']['html_head']. + $this->drupalGet('/render_attached_test/head_dpa'); + $this->assertHeader('X-Drupal-Cache', 'MISS'); + $this->assertHead(); + // Repeat for the cache. + $this->drupalGet('/render_attached_test/head_dpa'); + $this->assertHeader('X-Drupal-Cache', 'HIT'); } /** @@ -112,15 +106,60 @@ public function testSmartCache() { $this->drupalGet(''); // Make sure our block is visible. $this->assertText('Headers handled by drupal_process_attached().'); - // The header added in the block build should be present. - $this->assertHeader('X-DPA-Cache-Test', 'HIT'); + // Test that all our attached items are present. + $this->assertFeed(); + $this->assertHead(); + $this->assertResponse(418); + $this->assertTeapotHeaders(); // Reload the page, to test caching. $this->drupalGet(''); // Make sure our block is visible. $this->assertText('Headers handled by drupal_process_attached().'); // The header should be present again. - $this->assertHeader('X-DPA-Cache-Test', 'HIT'); + $this->assertHeader('X-Test-Teapot', 'Teapot Mode Active'); + } + + /** + * Helper function to make assertions about added HTTP headers. + */ + protected function assertTeapotHeaders() { + $this->assertHeader('X-Test-Teapot', 'Teapot Mode Active'); + $this->assertHeader('X-Test-Teapot-Replace', 'Teapot replaced'); + $this->assertHeader('X-Test-Teapot-No-Replace', 'This value is not replaced,This one is added'); + } + + /** + * Helper function to make assertions about the presence of an RSS feed. + */ + protected function assertFeed() { + // Discover the DOM element for the feed link. + $test_meta = $this->xpath('//head/link[@href="test://url"]'); + $this->assertEqual(1, count($test_meta), 'Link has URL.'); + // Reconcile the other attributes. + $test_meta_attributes = [ + 'href' => 'test://url', + 'rel' => 'alternate', + 'type' => 'application/rss+xml', + 'title' => 'Your RSS feed.', + ]; + $test_meta = reset($test_meta); + foreach ($test_meta->attributes() as $attribute => $value) { + $this->assertEqual($value, $test_meta_attributes[$attribute]); + } + } + + /** + * Helper function to make assertions about HTML head elements. + */ + protected function assertHead() { + // Discover the DOM element for the meta link. + $test_meta = $this->xpath('//head/meta[@test-attribute="testy"]'); + $this->assertEqual(1, count($test_meta), 'There\'s only one test attribute.'); + // Grab the only DOM element. + $test_meta = reset($test_meta); + $test_meta_attributes = $test_meta->attributes(); + $this->assertEqual($test_meta_attributes['test-attribute'], 'testy'); } } diff --git a/core/modules/system/tests/modules/render_attached_test/render_attached_test.routing.yml b/core/modules/system/tests/modules/render_attached_test/render_attached_test.routing.yml index 40f8229..d3d578b 100644 --- a/core/modules/system/tests/modules/render_attached_test/render_attached_test.routing.yml +++ b/core/modules/system/tests/modules/render_attached_test/render_attached_test.routing.yml @@ -5,6 +5,13 @@ render_attached.teapot: requirements: _access: 'TRUE' +render_attached.header: + path: '/render_attached_test/header' + defaults: + _controller: '\Drupal\render_attached_test\Controller\TestController::header' + requirements: + _access: 'TRUE' + render_attached.head: path: '/render_attached_test/head' defaults: @@ -26,6 +33,13 @@ render_attached.teapot_dpa: requirements: _access: 'TRUE' +render_attached.header_dpa: + path: '/render_attached_test/header_dpa' + defaults: + _controller: '\Drupal\render_attached_test\Controller\TestController::headerDpa' + requirements: + _access: 'TRUE' + render_attached.head_dpa: path: '/render_attached_test/head_dpa' defaults: diff --git a/core/modules/system/tests/modules/render_attached_test/src/Controller/TestController.php b/core/modules/system/tests/modules/render_attached_test/src/Controller/TestController.php index 01898ef..3d158b3 100644 --- a/core/modules/system/tests/modules/render_attached_test/src/Controller/TestController.php +++ b/core/modules/system/tests/modules/render_attached_test/src/Controller/TestController.php @@ -28,9 +28,9 @@ public function teapotHeaderStatus() { * Test attached HTML head rendering. * * @return array - * A render array using the 'html_head' directive. + * A render array using the 'http_head' directive. */ - public function head() { + public function header() { $render = []; $render['#attached']['http_header'][] = ['X-Test-Teapot-Replace', 'This value gets replaced']; $render['#attached']['http_header'][] = ['X-Test-Teapot-Replace', 'Teapot replaced', TRUE]; @@ -41,6 +41,28 @@ public function head() { } /** + * Test attached HTML head rendering. + * + * @return array + * A render array using the 'html_head' directive. + */ + public function head() { + $head = [ + [ + '#tag' => 'meta', + '#attributes' => [ + 'test-attribute' => 'testy', + ], + ], + 'test_head_attribute', + ]; + + $render = []; + $render['#attached']['html_head'][] = $head; + return $render; + } + + /** * Test attached feed rendering. * * @return array @@ -59,9 +81,18 @@ public function feed() { * A generic render array. */ public function teapotHeaderStatusDpa() { - $render = []; - $render['#attached']['http_header'][] = ['Status', "418 I'm a teapot."]; - \drupal_process_attached($render); + \drupal_process_attached($this->teapotHeaderStatus()); + return ['#markup' => "I'm some markup here to fool the kernel into rendering this page."]; + } + + /** + * Test attached HTML head rendering as a side-effect. + * + * @return array + * A render array using the 'http_header' directive. + */ + public function headerDpa() { + \drupal_process_attached($this->header()); return ['#markup' => "I'm some markup here to fool the kernel into rendering this page."]; } @@ -72,13 +103,7 @@ public function teapotHeaderStatusDpa() { * A render array using the 'html_head' directive. */ public function headDpa() { - $render = []; - $render['#attached']['http_header'][] = ['X-Test-Teapot-Replace', 'This value gets replaced']; - $render['#attached']['http_header'][] = ['X-Test-Teapot-Replace', 'Teapot replaced', TRUE]; - $render['#attached']['http_header'][] = ['X-Test-Teapot-No-Replace', 'This value is not replaced']; - $render['#attached']['http_header'][] = ['X-Test-Teapot-No-Replace', 'This one is added', FALSE]; - $render['#attached']['http_header'][] = ['X-Test-Teapot', 'Teapot Mode Active']; - \drupal_process_attached($render); + \drupal_process_attached($this->head()); return ['#markup' => "I'm some markup here to fool the kernel into rendering this page."]; } @@ -89,9 +114,7 @@ public function headDpa() { * A render array using the 'feed' directive. */ public function feedDpa() { - $render = []; - $render['#attached']['feed'][] = ['test://url', 'Your RSS feed.']; - \drupal_process_attached($render); + \drupal_process_attached($this->feed()); return ['#markup' => "I'm some markup here to fool the kernel into rendering this page."]; } diff --git a/core/modules/system/tests/modules/render_attached_test/src/Plugin/Block/DrupalProcessAttachedBlock.php b/core/modules/system/tests/modules/render_attached_test/src/Plugin/Block/DrupalProcessAttachedBlock.php index 9ab5400..3e7b342 100644 --- a/core/modules/system/tests/modules/render_attached_test/src/Plugin/Block/DrupalProcessAttachedBlock.php +++ b/core/modules/system/tests/modules/render_attached_test/src/Plugin/Block/DrupalProcessAttachedBlock.php @@ -7,8 +7,10 @@ namespace Drupal\render_attached_test\Plugin\Block; +use Drupal\render_attached_test\Controller\TestController; use Drupal\Core\Block\BlockBase; use Drupal\Core\Cache\Cache; +use Drupal\Core\Render\BubbleableMetadata; /** * A block we can use to test caching of #attached headers. @@ -26,8 +28,16 @@ class DrupalProcessAttachedBlock extends BlockBase { * {@inheritdoc} */ public function build() { - // Use drupal_process_attached() to attach an HTTP header. - \drupal_process_attached(['#attached' => ['http_header' => [['X-DPA-Cache-Test', 'HIT']]]]); + // Grab test attachment fixtures from + // Drupal\render_attached_test\Controller\TestController. + $controller = new TestController(); + $attached = BubbleableMetadata::mergeAttachments($controller->feed(), $controller->head()); + $attached = BubbleableMetadata::mergeAttachments($attached, $controller->header()); + $attached = BubbleableMetadata::mergeAttachments($attached, $controller->teapotHeaderStatus()); + + // Use drupal_process_attached() to attach all the #attached stuff. + drupal_process_attached($attached); + // Return some arbitrary markup so the block doesn't disappear. return ['#markup' => 'Headers handled by drupal_process_attached().']; }