diff --git a/core/lib/Drupal/Component/Plugin/Discovery/DiscoveryInterface.php b/core/lib/Drupal/Component/Plugin/Discovery/DiscoveryInterface.php index bf5e0aae270..66e6a2ec038 100644 --- a/core/lib/Drupal/Component/Plugin/Discovery/DiscoveryInterface.php +++ b/core/lib/Drupal/Component/Plugin/Discovery/DiscoveryInterface.php @@ -33,6 +33,8 @@ interface DiscoveryInterface { * @return mixed[] * An array of plugin definitions (empty array if no definitions were * found). Keys are plugin IDs. + * + * @see \Drupal\Core\Plugin\FilteredPluginManagerInterface::getFilteredDefinitions() */ public function getDefinitions(); diff --git a/core/lib/Drupal/Core/Block/BlockManager.php b/core/lib/Drupal/Core/Block/BlockManager.php index 30b52b61256..467e63565ef 100644 --- a/core/lib/Drupal/Core/Block/BlockManager.php +++ b/core/lib/Drupal/Core/Block/BlockManager.php @@ -6,8 +6,8 @@ use Drupal\Component\Plugin\FallbackPluginManagerInterface; use Drupal\Core\Cache\CacheBackendInterface; use Drupal\Core\Extension\ModuleHandlerInterface; use Drupal\Core\Plugin\CategorizingPluginManagerTrait; -use Drupal\Core\Plugin\Context\ContextAwarePluginManagerTrait; use Drupal\Core\Plugin\DefaultPluginManager; +use Drupal\Core\Plugin\FilteredPluginManagerTrait; /** * Manages discovery and instantiation of block plugins. @@ -21,7 +21,7 @@ class BlockManager extends DefaultPluginManager implements BlockManagerInterface use CategorizingPluginManagerTrait { getSortedDefinitions as traitGetSortedDefinitions; } - use ContextAwarePluginManagerTrait; + use FilteredPluginManagerTrait; /** * Constructs a new \Drupal\Core\Block\BlockManager object. @@ -37,10 +37,17 @@ class BlockManager extends DefaultPluginManager implements BlockManagerInterface public function __construct(\Traversable $namespaces, CacheBackendInterface $cache_backend, ModuleHandlerInterface $module_handler) { parent::__construct('Plugin/Block', $namespaces, $module_handler, 'Drupal\Core\Block\BlockPluginInterface', 'Drupal\Core\Block\Annotation\Block'); - $this->alterInfo('block'); + $this->alterInfo($this->getType()); $this->setCacheBackend($cache_backend, 'block_plugins'); } + /** + * {@inheritdoc} + */ + protected function getType() { + return 'block'; + } + /** * {@inheritdoc} */ diff --git a/core/lib/Drupal/Core/Block/BlockManagerInterface.php b/core/lib/Drupal/Core/Block/BlockManagerInterface.php index 3455f23b638..7b5d5c6332b 100644 --- a/core/lib/Drupal/Core/Block/BlockManagerInterface.php +++ b/core/lib/Drupal/Core/Block/BlockManagerInterface.php @@ -4,10 +4,11 @@ namespace Drupal\Core\Block; use Drupal\Component\Plugin\CategorizingPluginManagerInterface; use Drupal\Core\Plugin\Context\ContextAwarePluginManagerInterface; +use Drupal\Core\Plugin\FilteredPluginManagerInterface; /** * Provides an interface for the discovery and instantiation of block plugins. */ -interface BlockManagerInterface extends ContextAwarePluginManagerInterface, CategorizingPluginManagerInterface { +interface BlockManagerInterface extends ContextAwarePluginManagerInterface, CategorizingPluginManagerInterface, FilteredPluginManagerInterface { } diff --git a/core/lib/Drupal/Core/Condition/ConditionManager.php b/core/lib/Drupal/Core/Condition/ConditionManager.php index 2c6b61b6ae8..89fe216a441 100644 --- a/core/lib/Drupal/Core/Condition/ConditionManager.php +++ b/core/lib/Drupal/Core/Condition/ConditionManager.php @@ -8,8 +8,9 @@ use Drupal\Core\Executable\ExecutableManagerInterface; use Drupal\Core\Executable\ExecutableInterface; use Drupal\Core\Extension\ModuleHandlerInterface; use Drupal\Core\Plugin\CategorizingPluginManagerTrait; -use Drupal\Core\Plugin\Context\ContextAwarePluginManagerTrait; use Drupal\Core\Plugin\DefaultPluginManager; +use Drupal\Core\Plugin\FilteredPluginManagerInterface; +use Drupal\Core\Plugin\FilteredPluginManagerTrait; /** * A plugin manager for condition plugins. @@ -20,10 +21,10 @@ use Drupal\Core\Plugin\DefaultPluginManager; * * @ingroup plugin_api */ -class ConditionManager extends DefaultPluginManager implements ExecutableManagerInterface, CategorizingPluginManagerInterface { +class ConditionManager extends DefaultPluginManager implements ExecutableManagerInterface, CategorizingPluginManagerInterface, FilteredPluginManagerInterface { use CategorizingPluginManagerTrait; - use ContextAwarePluginManagerTrait; + use FilteredPluginManagerTrait; /** * Constructs a ConditionManager object. @@ -43,6 +44,13 @@ class ConditionManager extends DefaultPluginManager implements ExecutableManager parent::__construct('Plugin/Condition', $namespaces, $module_handler, 'Drupal\Core\Condition\ConditionInterface', 'Drupal\Core\Condition\Annotation\Condition'); } + /** + * {@inheritdoc} + */ + protected function getType() { + return 'condition'; + } + /** * {@inheritdoc} */ diff --git a/core/lib/Drupal/Core/Layout/LayoutPluginManager.php b/core/lib/Drupal/Core/Layout/LayoutPluginManager.php index 179d1ede9ab..a5a46529377 100644 --- a/core/lib/Drupal/Core/Layout/LayoutPluginManager.php +++ b/core/lib/Drupal/Core/Layout/LayoutPluginManager.php @@ -12,12 +12,15 @@ use Drupal\Core\Plugin\Discovery\AnnotatedClassDiscovery; use Drupal\Core\Plugin\Discovery\ContainerDerivativeDiscoveryDecorator; use Drupal\Core\Plugin\Discovery\YamlDiscoveryDecorator; use Drupal\Core\Layout\Annotation\Layout; +use Drupal\Core\Plugin\FilteredPluginManagerTrait; /** * Provides a plugin manager for layouts. */ class LayoutPluginManager extends DefaultPluginManager implements LayoutPluginManagerInterface { + use FilteredPluginManagerTrait; + /** * The theme handler. * @@ -42,8 +45,16 @@ class LayoutPluginManager extends DefaultPluginManager implements LayoutPluginMa parent::__construct('Plugin/Layout', $namespaces, $module_handler, LayoutInterface::class, Layout::class); $this->themeHandler = $theme_handler; - $this->setCacheBackend($cache_backend, 'layout'); - $this->alterInfo('layout'); + $type = $this->getType(); + $this->setCacheBackend($cache_backend, $type); + $this->alterInfo($type); + } + + /** + * {@inheritdoc} + */ + protected function getType() { + return 'layout'; } /** diff --git a/core/lib/Drupal/Core/Layout/LayoutPluginManagerInterface.php b/core/lib/Drupal/Core/Layout/LayoutPluginManagerInterface.php index c0e606d41ff..340a80d39ba 100644 --- a/core/lib/Drupal/Core/Layout/LayoutPluginManagerInterface.php +++ b/core/lib/Drupal/Core/Layout/LayoutPluginManagerInterface.php @@ -3,11 +3,12 @@ namespace Drupal\Core\Layout; use Drupal\Component\Plugin\CategorizingPluginManagerInterface; +use Drupal\Core\Plugin\FilteredPluginManagerInterface; /** * Provides the interface for a plugin manager of layouts. */ -interface LayoutPluginManagerInterface extends CategorizingPluginManagerInterface { +interface LayoutPluginManagerInterface extends CategorizingPluginManagerInterface, FilteredPluginManagerInterface { /** * Gets theme implementations for layouts. diff --git a/core/lib/Drupal/Core/Plugin/Context/ContextAwarePluginManagerInterface.php b/core/lib/Drupal/Core/Plugin/Context/ContextAwarePluginManagerInterface.php index 5c29411f32b..60013491cd2 100644 --- a/core/lib/Drupal/Core/Plugin/Context/ContextAwarePluginManagerInterface.php +++ b/core/lib/Drupal/Core/Plugin/Context/ContextAwarePluginManagerInterface.php @@ -20,6 +20,8 @@ interface ContextAwarePluginManagerInterface extends PluginManagerInterface { * * @return array * An array of plugin definitions. + * + * @see \Drupal\Core\Plugin\FilteredPluginManagerInterface::getFilteredDefinitions() */ public function getDefinitionsForContexts(array $contexts = []); diff --git a/core/lib/Drupal/Core/Plugin/Context/ContextHandler.php b/core/lib/Drupal/Core/Plugin/Context/ContextHandler.php index 56dcdb2a1c1..d276a553aa1 100644 --- a/core/lib/Drupal/Core/Plugin/Context/ContextHandler.php +++ b/core/lib/Drupal/Core/Plugin/Context/ContextHandler.php @@ -17,7 +17,9 @@ class ContextHandler implements ContextHandlerInterface { public function filterPluginDefinitionsByContexts(array $contexts, array $definitions) { return array_filter($definitions, function ($plugin_definition) use ($contexts) { // If this plugin doesn't need any context, it is available to use. - if (!isset($plugin_definition['context'])) { + // @todo Support object-based plugin definitions in + // https://www.drupal.org/project/drupal/issues/2961822. + if (!is_array($plugin_definition) || !isset($plugin_definition['context'])) { return TRUE; } diff --git a/core/lib/Drupal/Core/Plugin/FilteredPluginManagerInterface.php b/core/lib/Drupal/Core/Plugin/FilteredPluginManagerInterface.php new file mode 100644 index 00000000000..0404a66f8ee --- /dev/null +++ b/core/lib/Drupal/Core/Plugin/FilteredPluginManagerInterface.php @@ -0,0 +1,36 @@ +getDefinitionsForContexts($contexts); + } + else { + $definitions = $this->getDefinitions(); + } + + $type = $this->getType(); + $hooks = []; + $hooks[] = "plugin_filter_{$type}"; + $hooks[] = "plugin_filter_{$type}__{$consumer}"; + $this->moduleHandler()->alter($hooks, $definitions, $extra, $consumer); + $this->themeManager()->alter($hooks, $definitions, $extra, $consumer); + return $definitions; + } + + /** + * A string identifying the plugin type. + * + * This string should be unique and generally will correspond to the string + * used by the discovery, e.g. the annotation class or the YAML file name. + * + * @return string + * A string identifying the plugin type. + */ + abstract protected function getType(); + + /** + * Wraps the module handler. + * + * @return \Drupal\Core\Extension\ModuleHandlerInterface + * The module handler. + */ + protected function moduleHandler() { + if (property_exists($this, 'moduleHandler') && $this->moduleHandler instanceof ModuleHandlerInterface) { + return $this->moduleHandler; + } + + return \Drupal::service('module_handler'); + } + + /** + * Wraps the theme manager. + * + * @return \Drupal\Core\Theme\ThemeManagerInterface + * The theme manager. + */ + protected function themeManager() { + if (property_exists($this, 'themeManager') && $this->themeManager instanceof ThemeManagerInterface) { + return $this->themeManager; + } + + return \Drupal::service('theme.manager'); + } + +} diff --git a/core/lib/Drupal/Core/Plugin/plugin.api.php b/core/lib/Drupal/Core/Plugin/plugin.api.php new file mode 100644 index 00000000000..5c85c1162ec --- /dev/null +++ b/core/lib/Drupal/Core/Plugin/plugin.api.php @@ -0,0 +1,68 @@ +entity->getVisibility(); - foreach ($this->manager->getDefinitionsForContexts($form_state->getTemporaryValue('gathered_contexts')) as $condition_id => $definition) { + $definitions = $this->manager->getFilteredDefinitions('block_ui', $form_state->getTemporaryValue('gathered_contexts'), ['block' => $this->entity]); + foreach ($definitions as $condition_id => $definition) { // Don't display the current theme condition. if ($condition_id == 'current_theme') { continue; diff --git a/core/modules/block/src/Controller/BlockLibraryController.php b/core/modules/block/src/Controller/BlockLibraryController.php index 959c642bbaf..cf5f20a0baf 100644 --- a/core/modules/block/src/Controller/BlockLibraryController.php +++ b/core/modules/block/src/Controller/BlockLibraryController.php @@ -101,8 +101,14 @@ class BlockLibraryController extends ControllerBase { ['data' => $this->t('Operations')], ]; + $region = $request->query->get('region'); + $weight = $request->query->get('weight'); + // Only add blocks which work without any available context. - $definitions = $this->blockManager->getDefinitionsForContexts($this->contextRepository->getAvailableContexts()); + $definitions = $this->blockManager->getFilteredDefinitions('block_ui', $this->contextRepository->getAvailableContexts(), [ + 'theme' => $theme, + 'region' => $region, + ]); // Order by category, and then by admin label. $definitions = $this->blockManager->getSortedDefinitions($definitions); // Filter out definitions that are not intended to be placed by the UI. @@ -110,8 +116,6 @@ class BlockLibraryController extends ControllerBase { return empty($definition['_block_ui_hidden']); }); - $region = $request->query->get('region'); - $weight = $request->query->get('weight'); $rows = []; foreach ($definitions as $plugin_id => $plugin_definition) { $row = []; diff --git a/core/modules/layout_builder/src/Controller/ChooseBlockController.php b/core/modules/layout_builder/src/Controller/ChooseBlockController.php index 85ea7d83709..d046b4fab82 100644 --- a/core/modules/layout_builder/src/Controller/ChooseBlockController.php +++ b/core/modules/layout_builder/src/Controller/ChooseBlockController.php @@ -63,7 +63,10 @@ class ChooseBlockController implements ContainerInjectionInterface { $build['#type'] = 'container'; $build['#attributes']['class'][] = 'block-categories'; - $definitions = $this->blockManager->getDefinitionsForContexts($this->getAvailableContexts($section_storage)); + $definitions = $this->blockManager->getFilteredDefinitions('layout_builder', $this->getAvailableContexts($section_storage), [ + 'section_storage' => $section_storage, + 'region' => $region, + ]); foreach ($this->blockManager->getGroupedDefinitions($definitions) as $category => $blocks) { $build[$category]['#type'] = 'details'; $build[$category]['#open'] = TRUE; diff --git a/core/modules/layout_builder/src/Controller/ChooseSectionController.php b/core/modules/layout_builder/src/Controller/ChooseSectionController.php index 488480883d5..3405692e53b 100644 --- a/core/modules/layout_builder/src/Controller/ChooseSectionController.php +++ b/core/modules/layout_builder/src/Controller/ChooseSectionController.php @@ -62,7 +62,8 @@ class ChooseSectionController implements ContainerInjectionInterface { $output['#title'] = $this->t('Choose a layout'); $items = []; - foreach ($this->layoutManager->getDefinitions() as $plugin_id => $definition) { + $definitions = $this->layoutManager->getFilteredDefinitions('layout_builder', [], ['section_storage' => $section_storage]); + foreach ($definitions as $plugin_id => $definition) { $layout = $this->layoutManager->createInstance($plugin_id); $item = [ '#type' => 'link', diff --git a/core/modules/layout_builder/tests/modules/layout_builder_test/layout_builder_test.info.yml b/core/modules/layout_builder/tests/modules/layout_builder_test/layout_builder_test.info.yml new file mode 100644 index 00000000000..607877e7808 --- /dev/null +++ b/core/modules/layout_builder/tests/modules/layout_builder_test/layout_builder_test.info.yml @@ -0,0 +1,6 @@ +name: 'Layout Builder test' +type: module +description: 'Support module for testing layout building.' +package: Testing +version: VERSION +core: 8.x diff --git a/core/modules/layout_builder/tests/modules/layout_builder_test/layout_builder_test.module b/core/modules/layout_builder/tests/modules/layout_builder_test/layout_builder_test.module new file mode 100644 index 00000000000..6d159850e83 --- /dev/null +++ b/core/modules/layout_builder/tests/modules/layout_builder_test/layout_builder_test.module @@ -0,0 +1,29 @@ + $definition) { + // Field block IDs are in the form 'field_block:{entity}:{bundle}:{name}', + // for example 'field_block:node:article:revision_timestamp'. + preg_match('/field_block:.*:.*:(.*)/', $plugin_id, $parts); + if (isset($parts[1]) && in_array($parts[1], $disallowed_fields, TRUE)) { + // Unset any field blocks that match our predefined list. + unset($definitions[$plugin_id]); + } + } +} diff --git a/core/modules/layout_builder/tests/src/Functional/LayoutBuilderTest.php b/core/modules/layout_builder/tests/src/Functional/LayoutBuilderTest.php index a78e9eeeb79..b99c273f551 100644 --- a/core/modules/layout_builder/tests/src/Functional/LayoutBuilderTest.php +++ b/core/modules/layout_builder/tests/src/Functional/LayoutBuilderTest.php @@ -19,6 +19,7 @@ class LayoutBuilderTest extends BrowserTestBase { 'layout_test', 'block', 'node', + 'layout_builder_test', ]; /** @@ -319,4 +320,33 @@ class LayoutBuilderTest extends BrowserTestBase { $this->clickLink('Cancel Layout'); } + /** + * {@inheritdoc} + */ + public function testLayoutBuilderChooseBlocksAlter() { + // See layout_builder_test_plugin_filter_block__layout_builder_alter(). + $assert_session = $this->assertSession(); + + $this->drupalLogin($this->drupalCreateUser([ + 'configure any layout', + 'administer node display', + 'administer node fields', + ])); + + // From the manage display page, go to manage the layout. + $this->drupalGet('admin/structure/types/manage/bundle_with_section_field/display/default'); + $this->clickLink('Manage layout'); + + // Add a new block. + $this->clickLink('Add Block'); + + // Verify that blocks not modified are present. + $assert_session->linkExists('Powered by Drupal'); + $assert_session->linkExists('Default revision'); + + // Verify that blocks explicitly removed are not present. + $assert_session->linkNotExists('Help'); + $assert_session->linkNotExists('Sticky at top of lists'); + } + } diff --git a/core/tests/Drupal/Tests/Core/Plugin/FilteredPluginManagerTraitTest.php b/core/tests/Drupal/Tests/Core/Plugin/FilteredPluginManagerTraitTest.php new file mode 100644 index 00000000000..9642949de25 --- /dev/null +++ b/core/tests/Drupal/Tests/Core/Plugin/FilteredPluginManagerTraitTest.php @@ -0,0 +1,121 @@ + 'plugin1']; + $definitions['plugin2'] = ['id' => 'plugin2']; + + $type = 'the_type'; + $consumer = 'the_consumer'; + $extra = ['foo' => 'bar']; + + $context_handler = $this->prophesize(ContextHandlerInterface::class); + // Remove the second plugin when context1 is provided. + $context_handler->filterPluginDefinitionsByContexts(['context1' => 'fake context'], $definitions) + ->willReturn(['plugin1' => $definitions['plugin1']]); + // Remove the first plugin when no contexts are provided. + $context_handler->filterPluginDefinitionsByContexts([], $definitions) + ->willReturn(['plugin2' => $definitions['plugin2']]); + + // After context filtering, the alter hook will be invoked. + $module_handler = $this->prophesize(ModuleHandlerInterface::class); + $hooks = ["plugin_filter_{$type}", "plugin_filter_{$type}__{$consumer}"]; + $module_handler->alter($hooks, $expected, $extra, $consumer)->shouldBeCalled(); + + $theme_manager = $this->prophesize(ThemeManagerInterface::class); + $theme_manager->alter($hooks, $expected, $extra, $consumer)->shouldBeCalled(); + + $plugin_manager = new TestFilteredPluginManager($definitions, $module_handler->reveal(), $theme_manager->reveal(), $context_handler->reveal()); + $result = $plugin_manager->getFilteredDefinitions($consumer, $contexts, $extra); + $this->assertSame($expected, $result); + } + + /** + * Provides test data for ::testGetFilteredDefinitions(). + */ + public function providerTestGetFilteredDefinitions() { + $data = []; + $data['populated context'] = [ + ['context1' => 'fake context'], + ['plugin1' => ['id' => 'plugin1']], + ]; + $data['empty context'] = [ + [], + ['plugin2' => ['id' => 'plugin2']], + ]; + $data['null context'] = [ + NULL, + [ + 'plugin1' => ['id' => 'plugin1'], + 'plugin2' => ['id' => 'plugin2'], + ], + ]; + return $data; + } + +} + +/** + * Class that allows testing the trait. + */ +class TestFilteredPluginManager extends PluginManagerBase implements FilteredPluginManagerInterface { + + use FilteredPluginManagerTrait; + + protected $definitions = []; + + protected $moduleHandler; + + protected $themeManager; + + protected $contextHandler; + + public function __construct(array $definitions, ModuleHandlerInterface $module_handler, ThemeManagerInterface $theme_manager, ContextHandlerInterface $context_handler) { + $this->definitions = $definitions; + $this->moduleHandler = $module_handler; + $this->themeManager = $theme_manager; + $this->contextHandler = $context_handler; + } + + protected function contextHandler() { + return $this->contextHandler; + } + + protected function moduleHandler() { + return $this->moduleHandler; + } + + protected function themeManager() { + return $this->themeManager; + } + + protected function getType() { + return 'the_type'; + } + + public function getDefinitions() { + return $this->definitions; + } + +}