From 4b1d3103e996e619f752a338e73271cd0cad85b1 Mon Sep 17 00:00:00 2001
From: Nathaniel Catchpole <catch@35733.no-reply.drupal.org>
Date: Wed, 5 Feb 2014 13:22:50 +0000
Subject: [PATCH] Issue #2171269 by effulgentsia, Wim Leers, InternetDevels,
 snig: Handle block rendering's #attributes correctly (fixes in-place editing
 of custom blocks).

---
 core/modules/block/block.module               | 28 ---------
 .../lib/Drupal/block/BlockViewBuilder.php     | 42 ++++++++++---
 .../Drupal/block/Tests/BlockHtmlIdTest.php    | 56 -----------------
 .../lib/Drupal/block/Tests/BlockHtmlTest.php  | 61 +++++++++++++++++++
 .../block/Tests/BlockInvalidRegionTest.php    |  2 +-
 .../block/Tests/BlockStorageUnitTest.php      | 12 ++--
 .../config/block.block.test_block.yml         |  4 +-
 .../block_test/Plugin/Block/TestHtmlBlock.php | 32 ++++++++++
 .../Plugin/Block/TestHtmlIdBlock.php          | 19 ------
 core/modules/menu/menu.module                 |  2 +-
 .../Tests/Entity/EntityCrudHookTest.php       |  4 +-
 core/modules/system/system.module             | 19 ++++++
 12 files changed, 159 insertions(+), 122 deletions(-)
 delete mode 100644 core/modules/block/lib/Drupal/block/Tests/BlockHtmlIdTest.php
 create mode 100644 core/modules/block/lib/Drupal/block/Tests/BlockHtmlTest.php
 create mode 100644 core/modules/block/tests/modules/block_test/lib/Drupal/block_test/Plugin/Block/TestHtmlBlock.php
 delete mode 100644 core/modules/block/tests/modules/block_test/lib/Drupal/block_test/Plugin/Block/TestHtmlIdBlock.php

diff --git a/core/modules/block/block.module b/core/modules/block/block.module
index ba915a93e46f..daea82cc973e 100644
--- a/core/modules/block/block.module
+++ b/core/modules/block/block.module
@@ -7,7 +7,6 @@
 
 use Drupal\block\BlockInterface;
 use Drupal\Component\Plugin\Exception\PluginException;
-use Drupal\Component\Utility\NestedArray;
 use Symfony\Cmf\Component\Routing\RouteObjectInterface;
 
 /**
@@ -240,23 +239,6 @@ function _block_get_renderable_region($list = array()) {
         ),
       );
     }
-
-    // Add contextual links for this block; skip the main content block, since
-    // contextual links are basically output as tabs/local tasks already. Also
-    // skip the help block, since we assume that most users do not need or want
-    // to perform contextual actions on the help block, and the links needlessly
-    // draw attention on it.
-    if (isset($build[$key]) && !in_array($block->get('plugin'), array('system_help_block', 'system_main_block'))) {
-      $build[$key]['#contextual_links']['block'] = array(
-        'route_parameters' => array('block' => $key),
-      );
-
-      // If there are any nested contextual links, move them to the top level.
-      if (isset($build[$key]['content']['#contextual_links'])) {
-        $build[$key]['#contextual_links'] += $build[$key]['content']['#contextual_links'];
-        unset($build[$key]['content']['#contextual_links']);
-      }
-    }
   }
   return $build;
 }
@@ -501,16 +483,6 @@ function template_preprocess_block(&$variables) {
   $variables['attributes']['class'][] = 'block';
   $variables['attributes']['class'][] = drupal_html_class('block-' . $variables['configuration']['module']);
 
-  // The block template provides a wrapping element for the content. Render the
-  // #attributes of the content on this wrapping element rather than passing
-  // them through to the content's #theme function/template. This allows the
-  // content to not require a function/template at all, or if it does use one,
-  // to not require it to output an extra wrapping element.
-  if (isset($variables['content']['#attributes'])) {
-    $variables['content_attributes'] = NestedArray::mergeDeep($variables['content_attributes'], $variables['content']['#attributes']);
-    unset($variables['content']['#attributes']);
-  }
-
   // Add default class for block content.
   $variables['content_attributes']['class'][] = 'content';
 
diff --git a/core/modules/block/lib/Drupal/block/BlockViewBuilder.php b/core/modules/block/lib/Drupal/block/BlockViewBuilder.php
index 73cabaae29e0..200b428ddde7 100644
--- a/core/modules/block/lib/Drupal/block/BlockViewBuilder.php
+++ b/core/modules/block/lib/Drupal/block/BlockViewBuilder.php
@@ -36,7 +36,8 @@ public function view(EntityInterface $entity, $view_mode = 'full', $langcode = N
    */
   public function viewMultiple(array $entities = array(), $view_mode = 'full', $langcode = NULL) {
     $build = array();
-    foreach ($entities as $entity_id => $entity) {
+    foreach ($entities as $key => $entity) {
+      $entity_id = $entity->id();
       $plugin = $entity->getPlugin();
       $plugin_id = $plugin->getPluginId();
       $base_id = $plugin->getBasePluginId();
@@ -44,24 +45,51 @@ public function viewMultiple(array $entities = array(), $view_mode = 'full', $la
 
       if ($content = $plugin->build()) {
         $configuration = $plugin->getConfiguration();
-        $build[$entity_id] = array(
+
+        // Create the render array for the block as a whole.
+        // @see template_preprocess_block().
+        $build[$key] = array(
           '#theme' => 'block',
-          'content' => $content,
+          '#attributes' => array(),
+          '#contextual_links' => array(
+            'block' => array(
+              'route_parameters' => array('block' => $entity_id),
+            ),
+          ),
           '#configuration' => $configuration,
           '#plugin_id' => $plugin_id,
           '#base_plugin_id' => $base_id,
           '#derivative_plugin_id' => $derivative_id,
         );
-        $build[$entity_id]['#configuration']['label'] = check_plain($configuration['label']);
+        $build[$key]['#configuration']['label'] = check_plain($configuration['label']);
+
+        // Place the $content returned by the block plugin into a 'content'
+        // child element, as a way to allow the plugin to have complete control
+        // of its properties and rendering (e.g., its own #theme) without
+        // conflicting with the properties used above, or alternate ones used
+        // by alternate block rendering approaches in contrib (e.g., Panels).
+        // However, the use of a child element is an implementation detail of
+        // this particular block rendering approach. Semantically, the content
+        // returned by the plugin "is the" block, and in particular,
+        // #attributes and #contextual_links is information about the *entire*
+        // block. Therefore, we must move these properties from $content and
+        // merge them into the top-level element.
+        foreach (array('#attributes', '#contextual_links') as $property) {
+          if (isset($content[$property])) {
+            $build[$key][$property] += $content[$property];
+            unset($content[$property]);
+          }
+        }
+        $build[$key]['content'] = $content;
       }
       else {
-        $build[$entity_id] = array();
+        $build[$key] = array();
       }
 
-      $this->moduleHandler()->alter(array('block_view', "block_view_$base_id"), $build[$entity_id], $plugin);
+      $this->moduleHandler()->alter(array('block_view', "block_view_$base_id"), $build[$key], $plugin);
 
       // @todo Remove after fixing http://drupal.org/node/1989568.
-      $build[$entity_id]['#block'] = $entity;
+      $build[$key]['#block'] = $entity;
     }
     return $build;
   }
diff --git a/core/modules/block/lib/Drupal/block/Tests/BlockHtmlIdTest.php b/core/modules/block/lib/Drupal/block/Tests/BlockHtmlIdTest.php
deleted file mode 100644
index de18e91f6f1d..000000000000
--- a/core/modules/block/lib/Drupal/block/Tests/BlockHtmlIdTest.php
+++ /dev/null
@@ -1,56 +0,0 @@
-<?php
-
-/**
- * @file
- * Definition of Drupal\block\Tests\BlockHtmlIdTest.
- */
-
-namespace Drupal\block\Tests;
-
-use Drupal\simpletest\WebTestBase;
-
-/**
- * Tests block HTML ID validity.
- */
-class BlockHtmlIdTest extends WebTestBase {
-
-  /**
-   * Modules to enable.
-   *
-   * @var array
-   */
-  public static $modules = array('block', 'block_test');
-
-  public static function getInfo() {
-    return array(
-      'name' => 'Block HTML ID',
-      'description' => 'Tests block HTML ID validity.',
-      'group' => 'Block',
-    );
-  }
-
-  function setUp() {
-    parent::setUp();
-
-    $this->drupalLogin($this->root_user);
-
-    // Make sure the block has some content so it will appear.
-    $current_content = $this->randomName();
-    \Drupal::state()->set('block_test.content', $current_content);
-
-    // Enable our test blocks.
-    $this->drupalPlaceBlock('system_menu_block:tools');
-    $this->drupalPlaceBlock('test_html_id', array('id' => 'test_id_block'));
-  }
-
-  /**
-   * Tests for a valid HTML ID for a block.
-   */
-  function testHtmlId() {
-    $this->drupalGet('');
-    $this->assertRaw('id="block-test-id-block"', 'HTML ID for test block is valid.');
-    $elements = $this->xpath('//div[contains(@class, :div-class)]/div/ul[contains(@class, :ul-class)]/li', array(':div-class' => 'block-system', ':ul-class' => 'menu'));
-    $this->assertTrue(!empty($elements), 'The proper block markup was found.');
-  }
-
-}
diff --git a/core/modules/block/lib/Drupal/block/Tests/BlockHtmlTest.php b/core/modules/block/lib/Drupal/block/Tests/BlockHtmlTest.php
new file mode 100644
index 000000000000..a6bf31686040
--- /dev/null
+++ b/core/modules/block/lib/Drupal/block/Tests/BlockHtmlTest.php
@@ -0,0 +1,61 @@
+<?php
+
+/**
+ * @file
+ * Definition of Drupal\block\Tests\BlockHtmlTest.
+ */
+
+namespace Drupal\block\Tests;
+
+use Drupal\simpletest\WebTestBase;
+
+/**
+ * Tests block HTML ID validity.
+ */
+class BlockHtmlTest extends WebTestBase {
+
+  /**
+   * Modules to enable.
+   *
+   * @var array
+   */
+  public static $modules = array('block', 'block_test');
+
+  public static function getInfo() {
+    return array(
+      'name' => 'Block HTML',
+      'description' => 'Tests block HTML validity.',
+      'group' => 'Block',
+    );
+  }
+
+  function setUp() {
+    parent::setUp();
+
+    $this->drupalLogin($this->root_user);
+
+    // Enable the test_html block, to test HTML ID and attributes.
+    \Drupal::state()->set('block_test.attributes', array('data-custom-attribute' => 'foo'));
+    \Drupal::state()->set('block_test.content', $this->randomName());
+    $this->drupalPlaceBlock('test_html', array('id' => 'test_html_block'));
+
+    // Enable a menu block, to test more complicated HTML.
+    $this->drupalPlaceBlock('system_menu_block:tools');
+  }
+
+  /**
+   * Tests for valid HTML for a block.
+   */
+  function testHtml() {
+    $this->drupalGet('');
+
+    // Ensure that a block's ID is converted to an HTML valid ID, and that
+    // block-specific attributes are added to the same DOM element.
+    $this->assertFieldByXPath('//div[@id="block-test-html-block" and @data-custom-attribute="foo"]', NULL, 'HTML ID and attributes for test block are valid and on the same DOM element.');
+
+    // Ensure expected markup for a menu block.
+    $elements = $this->xpath('//div[contains(@class, :div-class)]/div/ul[contains(@class, :ul-class)]/li', array(':div-class' => 'block-system', ':ul-class' => 'menu'));
+    $this->assertTrue(!empty($elements), 'The proper block markup was found.');
+  }
+
+}
diff --git a/core/modules/block/lib/Drupal/block/Tests/BlockInvalidRegionTest.php b/core/modules/block/lib/Drupal/block/Tests/BlockInvalidRegionTest.php
index 1faddcb128d7..b44c4e26423d 100644
--- a/core/modules/block/lib/Drupal/block/Tests/BlockInvalidRegionTest.php
+++ b/core/modules/block/lib/Drupal/block/Tests/BlockInvalidRegionTest.php
@@ -45,7 +45,7 @@ function setUp() {
    */
   function testBlockInInvalidRegion() {
     // Enable a test block and place it in an invalid region.
-    $block = $this->drupalPlaceBlock('test_html_id');
+    $block = $this->drupalPlaceBlock('test_html');
     $block->set('region', 'invalid_region');
     $block->save();
 
diff --git a/core/modules/block/lib/Drupal/block/Tests/BlockStorageUnitTest.php b/core/modules/block/lib/Drupal/block/Tests/BlockStorageUnitTest.php
index e88f0ba7a1b0..d9cb5b10a1f1 100644
--- a/core/modules/block/lib/Drupal/block/Tests/BlockStorageUnitTest.php
+++ b/core/modules/block/lib/Drupal/block/Tests/BlockStorageUnitTest.php
@@ -9,7 +9,7 @@
 
 use Drupal\Core\Config\Entity\ConfigStorageController;
 use Drupal\simpletest\DrupalUnitTestBase;
-use Drupal\block_test\Plugin\Block\TestHtmlIdBlock;
+use Drupal\block_test\Plugin\Block\TestHtmlBlock;
 use Drupal\Component\Plugin\Exception\PluginException;
 use Drupal\block\Entity\Block;
 use Drupal\block\BlockInterface;
@@ -78,7 +78,7 @@ protected function createTests() {
     $entity = $this->controller->create(array(
       'id' => 'test_block',
       'theme' => 'stark',
-      'plugin' => 'test_html_id',
+      'plugin' => 'test_html',
     ));
     $entity->save();
 
@@ -97,18 +97,18 @@ protected function createTests() {
       'langcode' => language_default()->id,
       'theme' => 'stark',
       'region' => '-1',
-      'plugin' => 'test_html_id',
+      'plugin' => 'test_html',
       'settings' => array(
-        'cache' => 1,
         'label' => '',
         'module' => 'block_test',
         'label_display' => BlockInterface::BLOCK_LABEL_VISIBLE,
+        'cache' => DRUPAL_NO_CACHE,
       ),
       'visibility' => NULL,
     );
     $this->assertIdentical($actual_properties, $expected_properties);
 
-    $this->assertTrue($entity->getPlugin() instanceof TestHtmlIdBlock, 'The entity has an instance of the correct block plugin.');
+    $this->assertTrue($entity->getPlugin() instanceof TestHtmlBlock, 'The entity has an instance of the correct block plugin.');
   }
 
   /**
@@ -153,7 +153,7 @@ protected function renderTests() {
     $entity = $this->controller->create(array(
       'id' => 'test_block2',
       'theme' => 'stark',
-      'plugin' => 'test_html_id',
+      'plugin' => 'test_html',
       'settings' => array(
         'label' => 'Powered by Bananas',
       ),
diff --git a/core/modules/block/tests/modules/block_test/config/block.block.test_block.yml b/core/modules/block/tests/modules/block_test/config/block.block.test_block.yml
index bb651744481b..7c75e4d13a77 100644
--- a/core/modules/block/tests/modules/block_test/config/block.block.test_block.yml
+++ b/core/modules/block/tests/modules/block_test/config/block.block.test_block.yml
@@ -4,9 +4,9 @@ weight: 0
 status: true
 langcode: en
 region: '-1'
-plugin: test_html_id
+plugin: test_html
 settings:
-  label: 'Test block html id'
+  label: 'Test HTML block'
   module: block_test
   label_display: 'hidden'
   cache: 1
diff --git a/core/modules/block/tests/modules/block_test/lib/Drupal/block_test/Plugin/Block/TestHtmlBlock.php b/core/modules/block/tests/modules/block_test/lib/Drupal/block_test/Plugin/Block/TestHtmlBlock.php
new file mode 100644
index 000000000000..1c6138fe00c1
--- /dev/null
+++ b/core/modules/block/tests/modules/block_test/lib/Drupal/block_test/Plugin/Block/TestHtmlBlock.php
@@ -0,0 +1,32 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\block_test\Plugin\Block\TestHtmlBlock.
+ */
+
+namespace Drupal\block_test\Plugin\Block;
+
+use Drupal\block\BlockBase;
+
+/**
+ * Provides a block to test HTML.
+ *
+ * @Block(
+ *   id = "test_html",
+ *   admin_label = @Translation("Test HTML block")
+ * )
+ */
+class TestHtmlBlock extends BlockBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function build() {
+    return array(
+      '#attributes' => \Drupal::state()->get('block_test.attributes'),
+      '#children' => \Drupal::state()->get('block_test.content'),
+    );
+  }
+
+}
diff --git a/core/modules/block/tests/modules/block_test/lib/Drupal/block_test/Plugin/Block/TestHtmlIdBlock.php b/core/modules/block/tests/modules/block_test/lib/Drupal/block_test/Plugin/Block/TestHtmlIdBlock.php
deleted file mode 100644
index 5d3437aec06c..000000000000
--- a/core/modules/block/tests/modules/block_test/lib/Drupal/block_test/Plugin/Block/TestHtmlIdBlock.php
+++ /dev/null
@@ -1,19 +0,0 @@
-<?php
-
-/**
- * @file
- * Contains \Drupal\block_test\Plugin\Block\TestHtmlIdBlock.
- */
-
-namespace Drupal\block_test\Plugin\Block;
-
-/**
- * Provides a block to test HTML IDs.
- *
- * @Block(
- *   id = "test_html_id",
- *   admin_label = @Translation("Test block html id")
- * )
- */
-class TestHtmlIdBlock extends TestCacheBlock {
-}
diff --git a/core/modules/menu/menu.module b/core/modules/menu/menu.module
index 64a171b4dcc1..e849a3a47656 100644
--- a/core/modules/menu/menu.module
+++ b/core/modules/menu/menu.module
@@ -335,7 +335,7 @@ function menu_block_view_system_menu_block_alter(array &$build, BlockPluginInter
   $menu_name = $block->getDerivativeId();
   if (isset($menus[$menu_name]) && isset($build['content'])) {
     foreach (element_children($build['content']) as $key) {
-      $build['content']['#contextual_links']['menu'] = array(
+      $build['#contextual_links']['menu'] = array(
         'route_parameters' => array('menu' => $build['content'][$key]['#original_link']['menu_name']),
       );
     }
diff --git a/core/modules/system/lib/Drupal/system/Tests/Entity/EntityCrudHookTest.php b/core/modules/system/lib/Drupal/system/Tests/Entity/EntityCrudHookTest.php
index 784b0acee5dd..bd36e10cb970 100644
--- a/core/modules/system/lib/Drupal/system/Tests/Entity/EntityCrudHookTest.php
+++ b/core/modules/system/lib/Drupal/system/Tests/Entity/EntityCrudHookTest.php
@@ -78,8 +78,8 @@ protected function assertHookMessageOrder($messages) {
    */
   public function testBlockHooks() {
     $entity = entity_create('block', array(
-      'id' => 'stark.test_html_id',
-      'plugin' => 'test_html_id',
+      'id' => 'stark.test_html',
+      'plugin' => 'test_html',
     ));
 
     $this->assertHookMessageOrder(array(
diff --git a/core/modules/system/system.module b/core/modules/system/system.module
index 20e229da1098..a311ea1c2f53 100644
--- a/core/modules/system/system.module
+++ b/core/modules/system/system.module
@@ -9,6 +9,7 @@
 use Drupal\Core\Cache\Cache;
 use Drupal\Core\Language\Language;
 use Drupal\Core\Utility\ModuleInfo;
+use Drupal\block\BlockPluginInterface;
 use Drupal\menu_link\MenuLinkInterface;
 use Drupal\user\UserInterface;
 use Symfony\Component\HttpFoundation\JsonResponse;
@@ -3091,6 +3092,24 @@ function system_page_alter(&$page) {
   }
 }
 
+/**
+ * Implements hook_block_view_BASE_BLOCK_ID_alter().
+ */
+function system_block_view_system_main_block_alter(array &$build, BlockPluginInterface $block) {
+  // Contextual links on the system_main block would basically duplicate the
+  // tabs/local tasks, so reduce the clutter.
+  unset($build['#contextual_links']);
+}
+
+/**
+ * Implements hook_block_view_BASE_BLOCK_ID_alter().
+ */
+function system_block_view_system_help_block_alter(array &$build, BlockPluginInterface $block) {
+  // Assume that most users do not need or want to perform contextual actions on
+  // the help block, so don't needlessly draw attention to it.
+  unset($build['#contextual_links']);
+}
+
 /**
  * Run the automated cron if enabled.
  */
-- 
GitLab