Issue #2528178 by dawehner, amateescu, larowlan, tim.plunkett, jibran, catch, jhodgdon, effulgentsia, Berdir: Provide an upgrade path for blocks context IDs #2354889 (context manager)

8.0.x
Nathaniel Catchpole 2015-07-27 22:57:29 +01:00
parent 444c476a4b
commit 6f96411451
9 changed files with 441 additions and 1 deletions

View File

@ -16,3 +16,131 @@ function block_install() {
// invalidate all cached HTML output.
Cache::invalidateTags(['rendered']);
}
/**
* @addtogroup updates-8.0.0-beta
* @{
*/
/**
* Update block visibility context mapping.
*/
function block_update_8001() {
// This update function updates blocks for the change from
// https://www.drupal.org/node/2354889.
// Core visibility context plugins are updated automatically; blocks with
// unknown plugins are disabled and their previous visibility settings are
// saved in key value storage; see change record
// https://www.drupal.org/node/2527840 for more explanation.
// These are all the contexts that Drupal core provides.
$context_service_id_map = [
'node.node' => '@node.node_route_context:node',
'user.current_user' => '@user.current_user_context:current_user',
];
foreach (array_keys(\Drupal::languageManager()->getDefinedLanguageTypesInfo()) as $language_type_id) {
$context_service_id_map['language.' . $language_type_id] = '@language.current_language_context:' . $language_type_id;
}
// Contributed modules should leverage hook_update_dependencies() in order to
// be executed before block_update_8002(), so they can update their context
// mappings, if wanted.
$config_factory = \Drupal::configFactory();
$backup_values = $update_backup = [];
foreach ($config_factory->listAll('block.block.') as $block_config_name) {
$block = $config_factory->getEditable($block_config_name);
if ($visibility = $block->get('visibility')) {
foreach ($visibility as $condition_plugin_id => &$condition) {
foreach ($condition['context_mapping'] as $key => $context) {
if (!isset($context_service_id_map[$context])) {
// Remove the visibility condition for unknown context mapping
// entries, so the update process itself runs through and users can
// fix their block placements manually OR alternatively contributed
// modules can run their own update functions to update mappings
// that they provide.
$backup_values[$context][] = $condition_plugin_id;
unset($visibility[$condition_plugin_id]);
continue;
}
// Replace the context ID based on the defined mapping.
$condition['context_mapping'][$key] = $context_service_id_map[$context];
}
}
$block->set('visibility', $visibility);
if ($backup_values) {
// We not only store the missing context mappings but also the previous
// block status, in order to allow contributed and custom modules to do
// their own updates.
$update_backup[$block->get('id')] = [
'missing_context_ids' => $backup_values,
'status' => $block->get('status')
];
}
}
// Mark the resulting configuration as trusted data. This avoids issues with
// future schema changes.
$block->save(TRUE);
}
if ($update_backup) {
\Drupal::keyValue('update_backup')->set('block_update_8001', $update_backup);
}
return t('Block context IDs updated.');
}
/**
* Disable all blocks with missing context IDs in block_update_8001().
*/
function block_update_8002() {
$block_update_8001 = \Drupal::keyValue('update_backup')->get('block_update_8001', []);
$block_ids = array_keys($block_update_8001);
$config_factory = \Drupal::configFactory();
/** @var \Drupal\Core\Config\Config[] $blocks */
$blocks = [];
foreach ($block_ids as $block_id) {
$blocks[$block_id] = $block = $config_factory->getEditable('block.block.' . $block_id);
// This block will have an invalid context mapping service and must be
// disabled in order to prevent information disclosure.
// Disable currently enabled blocks.
if ($block_update_8001[$block_id]['status']) {
$block->set('status', FALSE);
$block->save(TRUE);
}
}
// Provides a list of plugin labels, keyed by plugin ID.
$condition_plugin_id_label_map = array_column(\Drupal::service('plugin.manager.condition')->getDefinitions(), 'label', 'id');
// Override with the UI labels we are aware of. Sadly they are not machine
// accessible, see
// \Drupal\node\Plugin\Condition\NodeType::buildConfigurationForm().
$condition_plugin_id_label_map['node_type'] = t('Content types');
$condition_plugin_id_label_map['request_path'] = t('Pages');
$condition_plugin_id_label_map['user_role'] = t('Roles');
if (count($block_ids) > 0) {
$message = t('Encountered an unknown context mapping key coming probably from a contributed or custom module: One or more mappings could not be updated. Please manually review your visibility settings for the following blocks, which are disabled now:');
$message .= '<ul>';
foreach ($blocks as $disabled_block_id => $disabled_block) {
$message .= '<li>' . t('@label (Visibility: @plugin_ids)', array(
'@label' => $disabled_block->get('settings.label'),
'@plugin_ids' => implode(', ', array_intersect_key($condition_plugin_id_label_map, array_flip(array_keys($block_update_8001[$disabled_block_id]['missing_context_ids']))))
)) . '</li>';
}
$message .= '</ul>';
return $message;
}
}
/**
* @} End of "addtogroup updates-8.0.0-beta".
*/

View File

@ -0,0 +1,110 @@
<?php
/**
* @file
* Contains \Drupal\block\Tests\Update\BlockContextMappingUpdateTest.
*/
namespace Drupal\block\Tests\Update;
use Drupal\block\Entity\Block;
use Drupal\node\Entity\Node;
use Drupal\system\Tests\Update\UpdatePathTestBase;
/**
* Tests the upgrade path for block context mapping renames.
*
* @see https://www.drupal.org/node/2354889
*
* @group Update
*/
class BlockContextMappingUpdateTest extends UpdatePathTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = ['block_test', 'language'];
/**
* {@inheritdoc}
*/
protected function setUp() {
$this->databaseDumpFiles = [
__DIR__ . '/../../../../system/tests/fixtures/update/drupal-8.bare.standard.php.gz',
__DIR__ . '/../../../../system/tests/fixtures/update/drupal-8.block-context-manager-2354889.php',
];
parent::setUp();
}
/**
* Tests that block context mapping is updated properly.
*/
public function testUpdateHookN() {
$this->runUpdates();
$this->assertRaw('Encountered an unknown context mapping key coming probably from a contributed or custom module: One or more mappings could not be updated. Please manually review your visibility settings for the following blocks, which are disabled now:<ul><li>User login (Visibility: Baloney spam)</li></ul>');
// Disable maintenance mode.
\Drupal::state()->set('system.maintenance_mode', FALSE);
// We finished updating so we can login the user now.
$this->drupalLogin($this->rootUser);
// The block that we are testing has the following visibility rules:
// - only visible on node pages
// - only visible to authenticated users.
$block_title = 'Test for 2354889';
// Create two nodes, a page and an article.
$page = Node::create([
'type' => 'page',
'title' => 'Page node',
]);
$page->save();
$article = Node::create([
'type' => 'article',
'title' => 'Article node',
]);
$article->save();
// Check that the block appears only on Page nodes for authenticated users.
$this->drupalGet('node/' . $page->id());
$this->assertRaw($block_title, 'Test block is visible on a Page node as an authenticated user.');
$this->drupalGet('node/' . $article->id());
$this->assertNoRaw($block_title, 'Test block is not visible on a Article node as an authenticated user.');
$this->drupalLogout();
// Check that the block does not appear on any page for anonymous users.
$this->drupalGet('node/' . $page->id());
$this->assertNoRaw($block_title, 'Test block is not visible on a Page node as an anonymous user.');
$this->drupalGet('node/' . $article->id());
$this->assertNoRaw($block_title, 'Test block is not visible on a Article node as an anonymous user.');
// Ensure that all the context mappings got updated properly.
$block = Block::load('testfor2354889');
$visibility = $block->get('visibility');
$this->assertEqual('@node.node_route_context:node', $visibility['node_type']['context_mapping']['node']);
$this->assertEqual('@user.current_user_context:current_user', $visibility['user_role']['context_mapping']['user']);
$this->assertEqual('@language.current_language_context:language_interface', $visibility['language']['context_mapping']['language']);
// Check that a block with invalid context is being disabled and that it can
// still be edited afterward.
$disabled_block = Block::load('thirdtestfor2354889');
$this->assertFalse($disabled_block->status(), 'Block with invalid context is disabled');
$this->assertEqual(['thirdtestfor2354889' => ['missing_context_ids' => ['baloney.spam' => ['node_type']], 'status' => TRUE]], \Drupal::keyValue('update_backup')->get('block_update_8001'));
$disabled_block_visibility = $disabled_block->get('visibility');
$this->assertTrue(!isset($disabled_block_visibility['node_type']), 'The problematic visibility condition has been removed.');
$admin_user = $this->drupalCreateUser(['administer blocks']);
$this->drupalLogin($admin_user);
$this->drupalGet('admin/structure/block/manage/thirdtestfor2354889');
$this->assertResponse('200');
}
}

View File

@ -0,0 +1,37 @@
<?php
/**
* @file
* Contains \Drupal\block_test\Plugin\Condition\BaloneySpam.
*/
namespace Drupal\block_test\Plugin\Condition;
use Drupal\Core\Condition\ConditionPluginBase;
/**
* Provides a 'baloney.spam' condition.
*
* @Condition(
* id = "baloney.spam",
* label = @Translation("Baloney spam"),
* )
*
*/
class BaloneySpam extends ConditionPluginBase {
/**
* {@inheritdoc}
*/
public function evaluate() {
return TRUE;
}
/**
* {@inheritdoc}
*/
public function summary() {
return 'Summary';
}
}

View File

@ -185,7 +185,13 @@ abstract class UpdatePathTestBase extends WebTestBase {
$this->fail('Missing zlib requirement for upgrade tests.');
return FALSE;
}
$this->drupalLogin($this->rootUser);
// The site might be broken at the time so logging in using the UI might
// not work, so we use the API itself.
drupal_rewrite_settings(['settings' => ['update_free_access' => (object) [
'value' => TRUE,
'required' => TRUE,
]]]);
$this->drupalGet($this->updateUrl);
$this->clickLink(t('Continue'));

View File

@ -454,6 +454,8 @@ system.db_update:
_title: 'Drupal database update'
_controller: '\Drupal\system\Controller\DbUpdateController::handle'
op: 'info'
options:
_maintenance_access: TRUE
requirements:
_access_system_update: 'TRUE'

View File

@ -0,0 +1,23 @@
uuid: C499B41D-035E-432E-9462-36410C43C49F
langcode: en
status: true
dependencies:
module:
- search
theme:
- bartik
id: secondtestfor2354889
theme: bartik
region: sidebar_first
weight: -6
provider: null
plugin: search_form_block
settings:
id: search_form_block
label: Search
provider: search
label_display: visible
cache:
max_age: -1
status: true
visibility: { }

View File

@ -0,0 +1,51 @@
uuid: 9d204071-a923-4707-8200-c298a540fb0c
langcode: en
status: true
dependencies:
content:
- 'block_content:basic:c1895145-893e-460b-a24e-78cd2cefbb1f'
module:
- block_content
- node
- user
theme:
- bartik
id: testfor2354889
theme: bartik
region: content
weight: -6
provider: null
plugin: 'block_content:c1895145-893e-460b-a24e-78cd2cefbb1f'
settings:
id: 'block_content:c1895145-893e-460b-a24e-78cd2cefbb1f'
label: 'Test for 2354889'
provider: block_content
label_display: visible
cache:
max_age: -1
status: true
info: ''
view_mode: full
visibility:
node_type:
id: node_type
bundles:
page: page
negate: false
context_mapping:
node: node.node
user_role:
id: user_role
roles:
authenticated: authenticated
negate: false
context_mapping:
user: user.current_user
language:
id: language
langcodes:
en: en
de: de
negate: false
context_mapping:
language: language.language_interface

View File

@ -0,0 +1,30 @@
uuid: 4558907D-2918-48FE-B56F-8A007B5FBDD5
langcode: en
status: true
dependencies:
module:
- user
theme:
- bartik
id: thirdtestfor2354889
theme: bartik
region: sidebar_first
weight: -6
provider: null
plugin: user_login_block
settings:
id: user_login_block
label: 'User login'
provider: user
label_display: visible
cache:
max_age: -1
status: true
visibility:
node_type:
id: node_type
bundles:
page: page
negate: false
context_mapping:
baloney: baloney.spam

View File

@ -0,0 +1,53 @@
<?php
/**
* @file
* Contains database additions to drupal-8.bare.standard.php.gz for testing the
* upgrade path of https://www.drupal.org/node/2354889.
*/
use Drupal\Core\Database\Database;
$connection = Database::getConnection();
// A custom block with visibility settings.
$block_configs[] = \Drupal\Component\Serialization\Yaml::decode(file_get_contents(__DIR__ . '/block.block.testfor2354889.yml'));
// A custom block without any visibility settings.
$block_configs[] = \Drupal\Component\Serialization\Yaml::decode(file_get_contents(__DIR__ . '/block.block.secondtestfor2354889.yml'));
// A custom block with visibility settings that contain a non-existing context
// mapping.
$block_configs[] = \Drupal\Component\Serialization\Yaml::decode(file_get_contents(__DIR__ . '/block.block.thirdtestfor2354889.yml'));
foreach ($block_configs as $block_config) {
$connection->insert('config')
->fields(array(
'collection',
'name',
'data',
))
->values(array(
'collection' => '',
'name' => 'block.block.' . $block_config['id'],
'data' => serialize($block_config),
))
->execute();
}
// Update the config entity query "index".
$existing_blocks = $connection->select('key_value')
->fields('key_value', ['value'])
->condition('collection', 'config.entity.key_store.block')
->condition('name', 'theme:bartik')
->execute()
->fetchField();
$existing_blocks = unserialize($existing_blocks);
$connection->update('key_value')
->fields([
'value' => serialize(array_merge($existing_blocks, ['block.block.testfor2354889', 'block.block.secondtestfor2354889', 'block.block.thirdtestfor2354889']))
])
->condition('collection', 'config.entity.key_store.block')
->condition('name', 'theme:bartik')
->execute();