Initial Drupal 11 with DDEV setup

This commit is contained in:
gluebox
2025-10-08 11:39:17 -04:00
commit 89ef74b305
25344 changed files with 2599172 additions and 0 deletions

View File

@ -0,0 +1,266 @@
<?php
/**
* @file
* Hooks provided by the Block module.
*/
use Drupal\Core\Block\BlockPluginInterface;
use Drupal\block\Entity\Block;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\Access\AccessResult;
/**
* @defgroup block_api Block API
* @{
* Information about the classes and interfaces that make up the Block API.
*
* @section sec_overview Overview
* Blocks are a combination of a configuration entity and a plugin. The
* configuration entity stores placement information (theme, region, weight) and
* any other configuration that is specific to the block. The block plugin does
* the work of rendering the block's content for display.
*
* @section sec_requirements Basic requirements
* To define a block in a module you need to:
* - Define a Block plugin by creating a new class that implements the
* \Drupal\Core\Block\BlockPluginInterface, in namespace Plugin\Block under
* your module namespace. For more information about creating plugins, see the
* @link plugin_api Plugin API topic. @endlink
* - Usually you will want to extend the \Drupal\Core\Block\BlockBase class,
* which provides a common configuration form and utility methods for getting
* and setting configuration in the block configuration entity.
* - Block plugins use the annotations defined by
* \Drupal\Core\Block\Annotation\Block. See the
* @link annotation Annotations topic @endlink for more information about
* annotations.
*
* This is an example of a basic block plugin class:
* @code
* namespace Drupal\my_module\Plugin\Block;
*
* use Drupal\Core\Block\BlockBase;
* #[Block(
* id: "my_block",
* admin_label: new TranslatableMarkup("My Block"),
* )]
* class MyBlock extends BlockBase {
* public function build() {
* return [
* '#type' => '#markup',
* '#markup' => 'Example block',
* ];
* }
* }
* @endcode
*
* More examples are available at the links below.
*
* @section sec_extending Extending blocks with conditions and hooks
* The Block API also makes use of Condition plugins, for conditional block
* placement. Condition plugins have interface
* \Drupal\Core\Condition\ConditionInterface, base class
* \Drupal\Core\Condition\ConditionPluginBase, and go in plugin namespace
* Plugin\Condition. Again, see the Plugin API and Annotations topics for
* details of how to create a plugin class and annotate it.
*
* There are also several block-related hooks, which allow you to affect
* the content and access permissions for blocks:
* - hook_block_view_alter()
* - hook_block_view_BASE_BLOCK_ID_alter()
* - hook_block_access()
*
* @section sec_further_information Further information
* - \Drupal\system\Plugin\Block\SystemPoweredByBlock provides a simple example
* of defining a block.
* - \Drupal\user\Plugin\Condition\UserRole is a straightforward example of a
* block placement condition plugin.
* - \Drupal\system\Plugin\Block\SystemMenuBlock is an example of a block with
* a custom configuration form.
* - For a more in-depth discussion of the Block API, see
* https://www.drupal.org/docs/drupal-apis/block-api/block-api-overview.
* - The Examples for Developers project also provides a Block example in
* https://www.drupal.org/project/examples.
* @}
*/
/**
* @addtogroup hooks
* @{
*/
/**
* Alter the result of \Drupal\Core\Block\BlockBase::build().
*
* This hook is called after the content has been assembled in a structured
* array and may be used for doing processing which requires that the complete
* block content structure has been built.
*
* If the module wishes to act on the rendered HTML of the block rather than
* the structured content array, it may use this hook to add a #post_render
* callback. Alternatively, it could also implement hook_preprocess_HOOK() for
* block.html.twig. See \Drupal\Core\Render\RendererInterface::render()
* documentation or the @link themeable Default theme implementations topic
* @endlink for details.
*
* In addition to hook_block_view_alter(), which is called for all blocks, there
* is hook_block_view_BASE_BLOCK_ID_alter(), which can be used to target a
* specific block or set of similar blocks.
*
* @param array &$build
* A renderable array of data, as returned from the build() implementation of
* the plugin that defined the block:
* - #title: The default localized title of the block.
* @param \Drupal\Core\Block\BlockPluginInterface $block
* The block plugin instance.
*
* @see hook_block_view_BASE_BLOCK_ID_alter()
* @see entity_crud
*
* @ingroup block_api
*/
function hook_block_view_alter(array &$build, BlockPluginInterface $block) {
// Remove the contextual links on all blocks that provide them.
if (isset($build['#contextual_links'])) {
unset($build['#contextual_links']);
}
}
/**
* Provide a block plugin specific block_view alteration.
*
* In this hook name, BASE_BLOCK_ID refers to the block implementation's plugin
* id, regardless of whether the plugin supports derivatives. For example, for
* the \Drupal\system\Plugin\Block\SystemPoweredByBlock block, this would be
* 'system_powered_by_block' as per that class's annotation. And for the
* \Drupal\system\Plugin\Block\SystemMenuBlock block, it would be
* 'system_menu_block' as per that class's annotation, regardless of which menu
* the derived block is for.
*
* @param array $build
* A renderable array of data, as returned from the build() implementation of
* the plugin that defined the block:
* - #title: The default localized title of the block.
* @param \Drupal\Core\Block\BlockPluginInterface $block
* The block plugin instance.
*
* @see hook_block_view_alter()
* @see entity_crud
*
* @ingroup block_api
*/
function hook_block_view_BASE_BLOCK_ID_alter(array &$build, BlockPluginInterface $block) {
// Change the title of the specific block.
$build['#title'] = t('New title of the block');
}
/**
* Alter the result of \Drupal\Core\Block\BlockBase::build().
*
* Unlike hook_block_view_alter(), this hook is called very early, before the
* block is being assembled. Therefore, it is early enough to alter the
* cacheability metadata (change #cache), or to explicitly placeholder the block
* (set #create_placeholder).
*
* In addition to hook_block_build_alter(), which is called for all blocks,
* there is hook_block_build_BASE_BLOCK_ID_alter(), which can be used to target
* a specific block or set of similar blocks.
*
* @param array &$build
* A renderable array of data, only containing #cache.
* @param \Drupal\Core\Block\BlockPluginInterface $block
* The block plugin instance.
*
* @see hook_block_build_BASE_BLOCK_ID_alter()
* @see entity_crud
*
* @ingroup block_api
*/
function hook_block_build_alter(array &$build, BlockPluginInterface $block) {
// Add the 'user' cache context to some blocks.
if ($block->label() === 'some condition') {
$build['#cache']['contexts'][] = 'user';
}
}
/**
* Provide a block plugin specific block_build alteration.
*
* In this hook name, BASE_BLOCK_ID refers to the block implementation's plugin
* id, regardless of whether the plugin supports derivatives. For example, for
* the \Drupal\system\Plugin\Block\SystemPoweredByBlock block, this would be
* 'system_powered_by_block' as per that class's annotation. And for the
* \Drupal\system\Plugin\Block\SystemMenuBlock block, it would be
* 'system_menu_block' as per that class's annotation, regardless of which menu
* the derived block is for.
*
* @param array $build
* A renderable array of data, only containing #cache.
* @param \Drupal\Core\Block\BlockPluginInterface $block
* The block plugin instance.
*
* @see hook_block_build_alter()
* @see entity_crud
*
* @ingroup block_api
*/
function hook_block_build_BASE_BLOCK_ID_alter(array &$build, BlockPluginInterface $block) {
// Explicitly enable placeholdering of the specific block.
$build['#create_placeholder'] = TRUE;
}
/**
* Control access to a block instance.
*
* Modules may implement this hook if they want to have a say in whether or not
* a given user has access to perform a given operation on a block instance.
*
* @param \Drupal\block\Entity\Block $block
* The block instance.
* @param string $operation
* The operation to be performed; for instance, 'view', 'create', 'delete', or
* 'update'.
* @param \Drupal\Core\Session\AccountInterface $account
* The user object to perform the access check operation on.
*
* @return \Drupal\Core\Access\AccessResultInterface
* The access result. If all implementations of this hook return
* AccessResultInterface objects whose value is !isAllowed() and
* !isForbidden(), then default access rules from
* \Drupal\block\BlockAccessControlHandler::checkAccess() are used.
*
* @see \Drupal\Core\Entity\EntityAccessControlHandler::access()
* @see \Drupal\block\BlockAccessControlHandler::checkAccess()
* @ingroup block_api
*/
function hook_block_access(Block $block, $operation, AccountInterface $account) {
// Example code that would prevent displaying the 'Powered by Drupal' block in
// a region different than the footer.
if ($operation == 'view' && $block->getPluginId() == 'system_powered_by_block') {
return AccessResult::forbiddenIf($block->getRegion() != 'footer')->addCacheableDependency($block);
}
// No opinion.
return AccessResult::neutral();
}
/**
* Allow modules to alter the block plugin definitions.
*
* @param array[] $definitions
* The array of block definitions, keyed by plugin ID.
*
* @ingroup block_api
*/
function hook_block_alter(&$definitions) {
foreach ($definitions as $id => $definition) {
if (str_starts_with($id, 'system_menu_block:')) {
// Replace $definition properties: id, deriver, class, provider to ones
// provided by this custom module.
}
}
}
/**
* @} End of "addtogroup hooks".
*/

View File

@ -0,0 +1,6 @@
name: Block
type: module
description: 'Allows users to configure blocks (containing content, forms, etc.) and to place them in the regions of a theme.'
package: Core
version: VERSION
configure: block.admin_display

View File

@ -0,0 +1,25 @@
<?php
/**
* @file
* Contains install and update functions for Block.
*/
use Drupal\Core\Cache\Cache;
/**
* Implements hook_install().
*/
function block_install(): void {
// Because the Block module upon installation unconditionally overrides all
// HTML output by selecting a different page display variant, we must
// invalidate all cached HTML output.
Cache::invalidateTags(['rendered']);
}
/**
* Implements hook_update_last_removed().
*/
function block_update_last_removed(): int {
return 8003;
}

View File

@ -0,0 +1,25 @@
drupal.block:
version: VERSION
js:
js/block.js: {}
dependencies:
- core/jquery
- core/drupal
- core/once
drupal.block.admin:
version: VERSION
js:
js/block.admin.js: {}
css:
theme:
css/block.admin.css: {}
dependencies:
- core/jquery
- core/drupal
- core/drupal.announce
- core/drupal.debounce
- core/drupal.dialog.ajax
- core/drupal.tabledrag
- core/drupal.vertical-tabs
- core/once

View File

@ -0,0 +1,9 @@
block_configure:
title: 'Configure block'
route_name: 'entity.block.edit_form'
group: 'block'
block_remove:
title: 'Remove block'
route_name: 'entity.block.delete_form'
group: 'block'

View File

@ -0,0 +1,5 @@
block.admin_display:
title: 'Block layout'
parent: system.admin_structure
description: 'Configure what block content appears in your site''s sidebars and other regions.'
route_name: block.admin_display

View File

@ -0,0 +1,15 @@
entity.block.edit_form:
title: 'Configure block'
route_name: entity.block.edit_form
base_route: entity.block.edit_form
# Per theme block layout pages.
block.admin_display:
title: 'Block layout'
route_name: block.admin_display
base_route: block.admin_display
block.admin_display_theme:
title: 'Block layout'
route_name: block.admin_display_theme
parent_id: block.admin_display
deriver: 'Drupal\block\Plugin\Derivative\ThemeLocalTask'

View File

@ -0,0 +1,135 @@
<?php
/**
* @file
*/
use Drupal\block\Hook\BlockHooks;
use Drupal\Core\Installer\InstallerKernel;
/**
* Initializes blocks for installed themes.
*
* @param string[] $theme_list
* An array of theme names.
*
* @see block_modules_installed()
*/
function block_themes_installed($theme_list): void {
// Do not create blocks during config sync.
if (\Drupal::service('config.installer')->isSyncing()) {
return;
}
// Disable this functionality prior to install profile installation because
// block configuration is often optional or provided by the install profile
// itself. block_theme_initialize() will be called when the install profile is
// installed.
if (InstallerKernel::installationAttempted() && \Drupal::config('core.extension')->get('module.' . \Drupal::installProfile()) === NULL) {
return;
}
foreach ($theme_list as $theme) {
// Don't initialize themes that are not displayed in the UI.
if (\Drupal::service('theme_handler')->hasUi($theme)) {
block_theme_initialize($theme);
}
}
}
/**
* Assigns an initial, default set of blocks for a theme.
*
* This function is called the first time a new theme is installed. The new
* theme gets a copy of the default theme's blocks, with the difference that if
* a particular region isn't available in the new theme, the block is assigned
* to the new theme's default region.
*
* @param string $theme
* The name of a theme.
*/
function block_theme_initialize($theme): void {
// Initialize theme's blocks if none already registered.
$has_blocks = \Drupal::entityTypeManager()->getStorage('block')->loadByProperties(['theme' => $theme]);
if (!$has_blocks) {
$default_theme = \Drupal::config('system.theme')->get('default');
// Apply only to new theme's visible regions.
$regions = system_region_list($theme, REGIONS_VISIBLE);
$default_theme_blocks = \Drupal::entityTypeManager()->getStorage('block')->loadByProperties(['theme' => $default_theme]);
foreach ($default_theme_blocks as $default_theme_block_id => $default_theme_block) {
if (str_starts_with($default_theme_block_id, $default_theme . '_')) {
$id = str_replace($default_theme . '_', '', $default_theme_block_id);
}
else {
$id = $default_theme_block_id;
}
$id = \Drupal::service('block.repository')->getUniqueMachineName($id, $theme);
$block = $default_theme_block->createDuplicateBlock($id, $theme);
// If the region isn't supported by the theme, assign the block to the
// theme's default region.
if (!isset($regions[$block->getRegion()])) {
$block->setRegion(system_default_region($theme));
}
$block->save();
}
}
}
/**
* Implements hook_theme_suggestions_HOOK().
*/
function block_theme_suggestions_block(array $variables): array {
$suggestions = [];
$suggestions[] = 'block__' . $variables['elements']['#configuration']['provider'];
// Hyphens (-) and underscores (_) play a special role in theme suggestions.
// Theme suggestions should only contain underscores, because within
// drupal_find_theme_templates(), underscores are converted to hyphens to
// match template file names, and then converted back to underscores to match
// pre-processing and other function names. So if your theme suggestion
// contains a hyphen, it will end up as an underscore after this conversion,
// and your function names won't be recognized. So, we need to convert
// hyphens to underscores in block deltas for the theme suggestions.
// We can safely explode on : because we know the Block plugin type manager
// enforces that delimiter for all derivatives.
$parts = explode(':', $variables['elements']['#plugin_id']);
$suggestion = 'block';
while ($part = array_shift($parts)) {
$suggestions[] = $suggestion .= '__' . strtr($part, '-', '_');
}
if (!empty($variables['elements']['#id'])) {
$suggestions[] = 'block__' . $variables['elements']['#id'];
}
return $suggestions;
}
/**
* Prepares variables for block templates.
*
* Default template: block.html.twig.
*
* Prepares the values passed to the theme_block function to be passed
* into a pluggable template engine. Uses block properties to generate a
* series of template file suggestions. If none are found, the default
* block.html.twig is used.
*
* Most themes use their own copy of block.html.twig. The default is located
* inside "core/modules/block/templates/block.html.twig". Look in there for the
* full list of available variables.
*
* @param array $variables
* An associative array containing:
* - elements: An associative array containing the properties of the element.
* Properties used: #block, #configuration, #children, #plugin_id.
*
* @deprecated in drupal:11.2.0 and is removed from drupal:12.0.0. Initial
* template_preprocess functions are registered directly in hook_theme().
*
* @see https://www.drupal.org/node/3504125
*/
function template_preprocess_block(&$variables): void {
@trigger_error(__FUNCTION__ . '() is deprecated in drupal:11.2.0 and is removed from drupal:12.0.0. Initial template_preprocess functions are registered directly in hook_theme(). See https://www.drupal.org/node/3504125', E_USER_DEPRECATED);
\Drupal::service(BlockHooks::class)->preprocessBlock($variables);
}

View File

@ -0,0 +1,2 @@
administer blocks:
title: 'Administer blocks'

View File

@ -0,0 +1,55 @@
<?php
/**
* @file
* Post update functions for Block.
*/
use Drupal\block\BlockInterface;
use Drupal\Core\Config\Entity\ConfigEntityUpdater;
/**
* Implements hook_removed_post_updates().
*/
function block_removed_post_updates(): array {
return [
'block_post_update_disable_blocks_with_missing_contexts' => '9.0.0',
'block_post_update_disabled_region_update' => '9.0.0',
'block_post_update_fix_negate_in_conditions' => '9.0.0',
'block_post_update_replace_node_type_condition' => '10.0.0',
];
}
/**
* Ensures that all block weights are integers.
*/
function block_post_update_make_weight_integer(array &$sandbox = []): void {
\Drupal::classResolver(ConfigEntityUpdater::class)
->update($sandbox, 'block', function (BlockInterface $block): bool {
$weight = $block->getWeight();
if (!is_int($weight)) {
$block->setWeight($weight);
return TRUE;
}
return FALSE;
});
}
/**
* Updates the `depth` setting to NULL if it is 0 in any menu blocks.
*/
function block_post_update_set_menu_block_depth_to_null_if_zero(array &$sandbox = []): void {
\Drupal::classResolver(ConfigEntityUpdater::class)
->update($sandbox, 'block', function (BlockInterface $block): bool {
if ($block->getPlugin()->getBaseId() === 'system_menu_block') {
$settings = $block->get('settings');
// Use `empty()` to account for either integer 0, or '0'.
if (empty($settings['depth'])) {
$settings['depth'] = NULL;
}
$block->set('settings', $settings);
return TRUE;
}
return FALSE;
});
}

View File

@ -0,0 +1,86 @@
block.admin_demo:
path: '/admin/structure/block/demo/{theme}'
defaults:
_controller: '\Drupal\block\Controller\BlockController::demo'
_title_callback: 'theme_handler:getName'
requirements:
_access_theme: 'TRUE'
_permission: 'administer blocks'
options:
_admin_route: FALSE
entity.block.delete_form:
path: '/admin/structure/block/manage/{block}/delete'
defaults:
_entity_form: 'block.delete'
_title: 'Remove block'
requirements:
_permission: 'administer blocks'
entity.block.edit_form:
path: '/admin/structure/block/manage/{block}'
defaults:
_entity_form: 'block.default'
_title: 'Configure block'
requirements:
_entity_access: 'block.update'
entity.block.enable:
path: '/admin/structure/block/manage/{block}/enable'
defaults:
_controller: '\Drupal\block\Controller\BlockController::performOperation'
op: enable
requirements:
_entity_access: 'block.enable'
_csrf_token: 'TRUE'
entity.block.disable:
path: '/admin/structure/block/manage/{block}/disable'
defaults:
_controller: '\Drupal\block\Controller\BlockController::performOperation'
op: disable
requirements:
_entity_access: 'block.disable'
_csrf_token: 'TRUE'
block.admin_display:
path: '/admin/structure/block'
defaults:
_controller: '\Drupal\block\Controller\BlockListController::listing'
_title: 'Block layout'
requirements:
_permission: 'administer blocks'
block.admin_display_theme:
path: '/admin/structure/block/list/{theme}'
defaults:
_controller: '\Drupal\block\Controller\BlockListController::listing'
_title: 'Block layout'
requirements:
_access_theme: 'TRUE'
_permission: 'administer blocks'
block.admin_library:
path: '/admin/structure/block/library/{theme}'
defaults:
_controller: '\Drupal\block\Controller\BlockLibraryController::listBlocks'
_title: 'Place block'
requirements:
_access_theme: 'TRUE'
_permission: 'administer blocks'
block.admin_add:
path: '/admin/structure/block/add/{plugin_id}/{theme}'
defaults:
_controller: '\Drupal\block\Controller\BlockAddController::blockAddConfigureForm'
theme: null
_title: 'Configure block'
requirements:
_permission: 'administer blocks'
block.category_autocomplete:
path: '/block-category/autocomplete'
defaults:
_controller: '\Drupal\block\Controller\CategoryAutocompleteController::autocomplete'
requirements:
_permission: 'administer blocks'

View File

@ -0,0 +1,13 @@
services:
_defaults:
autoconfigure: true
theme.negotiator.block.admin_demo:
class: Drupal\block\Theme\AdminDemoNegotiator
tags:
- { name: theme_negotiator, priority: 1000 }
block.page_display_variant_subscriber:
class: Drupal\block\EventSubscriber\BlockPageDisplayVariantSubscriber
block.repository:
class: Drupal\block\BlockRepository
arguments: ['@entity_type.manager', '@theme.manager', '@context.handler']
Drupal\block\BlockRepositoryInterface: '@block.repository'

View File

@ -0,0 +1,58 @@
# Schema for the configuration files of the Block module.
block.block.*:
type: config_entity
label: 'Block'
constraints:
FullyValidatable: ~
mapping:
id:
type: machine_name
label: 'ID'
# Blocks have atypical machine names: they allow periods for historical reasons.
# @see https://www.drupal.org/project/drupal/issues/2685917
# @see https://www.drupal.org/project/drupal/issues/2043527
constraints:
Regex:
pattern: '/^[a-z0-9_.]+$/'
message: "The %value machine name is not valid."
theme:
type: string
label: 'Theme'
constraints:
NotBlank: []
ExtensionName: []
ExtensionExists: theme
region:
type: string
label: 'Region'
constraints:
NotBlank: []
Callback: ['\Drupal\block\Entity\Block', validateRegion]
weight:
type: weight
label: 'Weight'
provider:
# @todo Deprecate this from config schema and remove it from the `config_export` definition in https://www.drupal.org/project/drupal/issues/3426278
nullable: true
type: string
label: 'Provider'
plugin:
type: string
label: 'Plugin'
constraints:
PluginExists:
manager: plugin.manager.block
interface: Drupal\Core\Block\BlockPluginInterface
# Block plugin IDs may not be valid in blocks that are backed by
# block_content entities that don't exist yet. Therefore, it's okay
# to consider the fallback plugin ID as valid.
allowFallback: true
settings:
type: block.settings.[%parent.plugin]
visibility:
type: sequence
label: 'Visibility Conditions'
sequence:
type: condition.plugin.[id]
label: 'Visibility Condition'

View File

@ -0,0 +1,47 @@
/* Block listing page */
.region-title__action {
display: inline-block;
margin-left: 1em; /* LTR */
}
[dir="rtl"] .region-title__action {
margin-right: 1em;
margin-left: 0;
}
/* Block demo mode */
.block-region {
margin-top: 4px;
margin-bottom: 4px;
padding: 3px;
color: #000;
background-color: #ff6;
}
a.block-demo-backlink,
a.block-demo-backlink:link,
a.block-demo-backlink:visited {
position: fixed;
z-index: 499;
left: 20px; /* LTR */
padding: 5px 10px;
color: #000;
border-radius: 0 0 10px 10px;
background-color: #b4d7f0;
font-family: "Lucida Grande", Verdana, sans-serif;
font-size: small;
line-height: 20px;
}
a.block-demo-backlink:hover {
text-decoration: underline;
}
/* Configure block form - Block description */
.block-form .form-item-settings-admin-label label {
display: inline;
}
.block-form .form-item-settings-admin-label label::after {
content: ":";
}
.block-disabled:not(:hover) {
opacity: 0.675;
background: #fcfcfa;
}

View File

@ -0,0 +1,27 @@
---
label: 'Configuring a previously-placed block'
related:
- block.overview
- core.ui_accessibility
---
{% set layout_link_text %}{% trans %}Block layout{% endtrans %}{% endset %}
{% set layout_link = render_var(help_route_link(layout_link_text, 'block.admin_display')) %}
<h2>{% trans %}Goal{% endtrans %}</h2>
<p>{% trans %}Configure the settings of a block that was previously placed in a region of a theme.{% endtrans %}</p>
<h2>{% trans %}Steps{% endtrans %}</h2>
<ol>
<li>{% trans %}In the <em>Manage</em> administrative menu, navigate to <em>Structure</em> &gt; <em>{{ layout_link }}</em>.{% endtrans %}</li>
<li>{% trans %}Click the name of the theme that contains the block.{% endtrans %}</li>
<li>{% trans %}Optionally, click <em>Demonstrate block regions</em> to see the regions of the theme.{% endtrans %}</li>
<li>{% trans %}If you only want to change the region where a block is located, or the ordering of blocks within a region, drag blocks to their desired positions and click <em>Save blocks</em>.{% endtrans %}</li>
<li>{% trans %}If you want to change additional settings, find the region where the block you want to update is currently located, and click <em>Configure</em> in the line of the block description.{% endtrans %}</li>
<li>{% trans %}Edit the block's settings. The available settings vary depending on the module that provides the block, but for all blocks you can change:{% endtrans %}
<ul>
<li>{% trans %}<em>Block title</em>: The heading for the block on your site -- for some blocks, you will need to check the <em>Override title</em> checkbox in order to enter a title{% endtrans %}</li>
<li>{% trans %}<em>Display title</em>: Check the box if you want the title displayed{% endtrans %}</li>
<li>{% trans %}<em>Visibility</em>: Add conditions for when the block should be displayed{% endtrans %}</li>
<li>{% trans %}<em>Region</em>: Change the theme region the block is displayed in{% endtrans %}</li>
</ul>
</li>
<li>{% trans %}Click <em>Save block</em>.{% endtrans %}</li>
</ol>

View File

@ -0,0 +1,20 @@
---
label: 'Managing blocks'
top_level: true
related:
- core.content_structure
---
<h2>{% trans %}What are blocks?{% endtrans %}</h2>
<p>{% trans %}Blocks are boxes of content rendered into an area, or region, of a web page of your site. Blocks are placed and configured specifically for each theme.{% endtrans %}</p>
<h2>{% trans %}What are content blocks?{% endtrans %}</h2>
<p>{% trans %}Content blocks are blocks whose content you can edit. You can define one or more <em>block types</em>, and attach fields to each block type. Content blocks can be placed just like blocks provided by other modules.{% endtrans %}</p>
<h2>{% trans %}What is the block description?{% endtrans %}</h2>
<p>{% trans %}The block description is an identification name for a block, which is shown in the administrative interface. It is not displayed on the site.{% endtrans %}</p>
<h2>{% trans %}What is the block title?{% endtrans %}</h2>
<p>{% trans %}The block title is the heading that is optionally shown to site visitors when the block is placed in a region.{% endtrans %}</p>
<h2>{% trans %}Overview for managing blocks{% endtrans %}</h2>
<p>{% trans %}The <em>Block</em> module allows you to place blocks in regions of your installed themes, and configure block settings. The <em>Block Content</em> module allows you to manage block types and content blocks. See the related topics listed below for specific tasks.{% endtrans %}</p>
<h2>{% trans %}Additional resources{% endtrans %}</h2>
<ul>
<li>{% trans %}<a href="https://www.drupal.org/docs/user_guide/en/blocks-chapter.html">Blocks (Drupal User Guide)</a>{% endtrans %}</li>
</ul>

View File

@ -0,0 +1,22 @@
---
label: 'Placing a block'
related:
- block.overview
- block.configure
---
{% set layout_link_text %}
{% trans %}Block layout{% endtrans %}
{% endset %}
{% set layout_link = render_var(help_route_link(layout_link_text, 'block.admin_display')) %}
{% set configure_topic = render_var(help_topic_link('block.configure')) %}
<h2>{% trans %}Goal{% endtrans %}</h2>
<p>{% trans %}Place a block into a theme's region. {% endtrans %}</p>
<h2>{% trans %}Steps{% endtrans %}</h2>
<ol>
<li>{% trans %}In the <em>Manage</em> administrative menu, navigate to <em>Structure</em> &gt; {{ layout_link }}.{% endtrans %}</li>
<li>{% trans %}Click the name of the theme that you want to place the block in.{% endtrans %}</li>
<li>{% trans %}Optionally, click <em>Demonstrate block regions</em> to see the regions of the theme.{% endtrans %}</li>
<li>{% trans %}Find the region where you want the block, and click <em>Place block</em> in that region. A modal dialog will pop up.{% endtrans %}</li>
<li>{% trans %}Find the block you want to place and click <em>Place block</em>. A <em>Configure block</em> modal dialog will pop up.{% endtrans %}</li>
<li>{% trans %}Configure the block and click <em>Save block</em>; see {{ configure_topic }} for configuration details.{% endtrans %}</li>
</ol>

View File

@ -0,0 +1,105 @@
/**
* @file
* Block admin behaviors.
*/
(function ($, Drupal, debounce, once) {
/**
* Filters the block list by a text input search string.
*
* The text input will have the selector `input.block-filter-text`.
*
* The target element to do searching in will be in the selector
* `input.block-filter-text[data-element]`
*
* The text source where the text should be found will have the selector
* `.block-filter-text-source`
*
* @type {Drupal~behavior}
*
* @prop {Drupal~behaviorAttach} attach
* Attaches the behavior for the block filtering.
*/
Drupal.behaviors.blockFilterByText = {
attach(context, settings) {
const $input = $(once('block-filter-text', 'input.block-filter-text'));
const $table = $($input.attr('data-element'));
let $filterRows;
/**
* Filters the block list.
*
* @param {jQuery.Event} e
* The jQuery event for the keyup event that triggered the filter.
*/
function filterBlockList(e) {
const query = e.target.value.toLowerCase();
/**
* Shows or hides the block entry based on the query.
*
* @param {number} index
* The index in the loop, as provided by `jQuery.each`
* @param {HTMLElement} label
* The label of the block.
*/
function toggleBlockEntry(index, label) {
const $row = $(label).parent().parent();
const textMatch = label.textContent.toLowerCase().includes(query);
$row.toggle(textMatch);
}
// Filter if the length of the query is at least 2 characters.
if (query.length >= 2) {
$filterRows.each(toggleBlockEntry);
Drupal.announce(
Drupal.formatPlural(
$table.find('tr:visible').length - 1,
'1 block is available in the modified list.',
'@count blocks are available in the modified list.',
),
);
} else {
$filterRows.each(function (index) {
$(this).parent().parent().show();
});
}
}
if ($table.length) {
$filterRows = $table.find('div.block-filter-text-source');
$input.on('keyup', debounce(filterBlockList, 200));
}
},
};
/**
* Highlights the block that was just placed into the block listing.
*
* @type {Drupal~behavior}
*
* @prop {Drupal~behaviorAttach} attach
* Attaches the behavior for the block placement highlighting.
*/
Drupal.behaviors.blockHighlightPlacement = {
attach(context, settings) {
// Ensure that the block we are attempting to scroll to actually exists.
if (settings.blockPlacement && $('.js-block-placed').length) {
once(
'block-highlight',
'[data-drupal-selector="edit-blocks"]',
context,
).forEach((container) => {
const $container = $(container);
window.scrollTo({
top:
$('.js-block-placed').offset().top -
$container.offset().top +
$container.scrollTop(),
behavior: 'smooth',
});
});
}
},
};
})(jQuery, Drupal, Drupal.debounce, once);

View File

@ -0,0 +1,258 @@
/**
* @file
* Block behaviors.
*/
(function ($, window, Drupal, once) {
/**
* Provide the summary information for the block settings vertical tabs.
*
* @type {Drupal~behavior}
*
* @prop {Drupal~behaviorAttach} attach
* Attaches the behavior for the block settings summaries.
*/
Drupal.behaviors.blockSettingsSummary = {
attach() {
// The drupalSetSummary method required for this behavior is not available
// on the Blocks administration page, so we need to make sure this
// behavior is processed only if drupalSetSummary is defined.
if (typeof $.fn.drupalSetSummary === 'undefined') {
return;
}
/**
* Create a summary for checkboxes in the provided context.
*
* @param {Document|HTMLElement} context
* A context where one would find checkboxes to summarize.
*
* @return {string}
* A string with the summary.
*/
function checkboxesSummary(context) {
const values = [];
const $checkboxes = $(context).find(
'input[type="checkbox"]:checked + label',
);
const il = $checkboxes.length;
for (let i = 0; i < il; i++) {
values.push($($checkboxes[i]).html());
}
if (!values.length) {
values.push(Drupal.t('Not restricted'));
}
return values.join(', ');
}
$(
'[data-drupal-selector="edit-visibility-node-type"], [data-drupal-selector="edit-visibility-entity-bundlenode"], [data-drupal-selector="edit-visibility-language"], [data-drupal-selector="edit-visibility-user-role"], [data-drupal-selector="edit-visibility-response-status"]',
).drupalSetSummary(checkboxesSummary);
$(
'[data-drupal-selector="edit-visibility-request-path"]',
).drupalSetSummary((context) => {
const $pages = $(context).find(
'textarea[name="visibility[request_path][pages]"]',
);
if (!$pages.length || !$pages[0].value) {
return Drupal.t('Not restricted');
}
return Drupal.t('Restricted to certain pages');
});
},
};
/**
* Move a block in the blocks table between regions via select list.
*
* This behavior is dependent on the tableDrag behavior, since it uses the
* objects initialized in that behavior to update the row.
*
* @type {Drupal~behavior}
*
* @prop {Drupal~behaviorAttach} attach
* Attaches the tableDrag behavior for blocks in block administration.
*/
Drupal.behaviors.blockDrag = {
attach(context, settings) {
// tableDrag is required and we should be on the blocks admin page.
if (typeof Drupal?.tableDrag?.blocks === 'undefined') {
return;
}
/**
* Function to check empty regions and toggle classes based on this.
*
* @param {jQuery} table
* The jQuery object representing the table to inspect.
* @param {Drupal.tableDrag.row} rowObject
* Drupal table drag row dropped.
*/
function checkEmptyRegions(table, rowObject) {
table.find('tr.region-message').each(function () {
const $this = $(this);
// If the dragged row is in this region, but above the message row,
// swap it down one space.
if ($this.prev('tr').get(0) === rowObject.element) {
// Prevent a recursion problem when using the keyboard to move rows
// up.
if (
rowObject.method !== 'keyboard' ||
rowObject.direction === 'down'
) {
rowObject.swap('after', this);
}
}
// This region has become empty.
if (
$this.next('tr').length === 0 ||
!$this.next('tr')[0].matches('.draggable')
) {
$this.removeClass('region-populated').addClass('region-empty');
}
// This region has become populated.
else if (this.matches('.region-empty')) {
$this.removeClass('region-empty').addClass('region-populated');
}
});
}
/**
* Function to update the last placed row with the correct classes.
*
* @param {jQuery} table
* The jQuery object representing the table to inspect.
* @param {Drupal.tableDrag.row} rowObject
* Drupal table drag row dropped.
*/
function updateLastPlaced(table, rowObject) {
// Remove the color-success class from new block if applicable.
table.find('.color-success').removeClass('color-success');
const $rowObject = $(rowObject);
if (!rowObject.element.matches('.drag-previous')) {
table.find('.drag-previous').removeClass('drag-previous');
$rowObject.addClass('drag-previous');
}
}
/**
* Update block weights in the given region.
*
* @param {jQuery} table
* Table with draggable items.
* @param {string} region
* Machine name of region containing blocks to update.
*/
function updateBlockWeights(table, region) {
// Calculate minimum weight.
let weight = -Math.round(table.find('.draggable').length / 2);
// Update the block weights.
table
.find(`.region-${region}-message`)
.nextUntil('.region-title')
.find('select.block-weight')
.each(function () {
// Increment the weight before assigning it to prevent using the
// absolute minimum available weight. This way we always have an
// unused upper and lower bound, which makes manually setting the
// weights easier for users who prefer to do it that way.
this.value = ++weight;
});
}
const table = $('#blocks');
// Get the blocks tableDrag object.
const tableDrag = Drupal.tableDrag.blocks;
// Add a handler for when a row is swapped, update empty regions.
tableDrag.row.prototype.onSwap = function (swappedRow) {
checkEmptyRegions(table, this);
updateLastPlaced(table, this);
};
// Add a handler so when a row is dropped, update fields dropped into
// new regions.
tableDrag.onDrop = function () {
const dragObject = this;
const $rowElement = $(dragObject.rowObject.element);
// Use "region-message" row instead of "region" row because
// "region-{region_name}-message" is less prone to regexp match errors.
const regionRow = $rowElement.prevAll('tr.region-message').get(0);
const regionName = regionRow.className.replace(
/([^ ]+[ ]+)*region-([^ ]+)-message([ ]+[^ ]+)*/,
'$2',
);
const regionField = $rowElement.find('select.block-region-select');
// Check whether the newly picked region is available for this block.
if (regionField.find(`option[value=${regionName}]`).length === 0) {
// If not, alert the user and keep the block in its old region
// setting.
window.alert(Drupal.t('The block cannot be placed in this region.'));
// Simulate that there was a selected element change, so the row is
// put back to from where the user tried to drag it.
regionField.trigger('change');
}
// Update region and weight fields if the region has been changed.
if (!regionField[0].matches(`.block-region-${regionName}`)) {
const weightField = $rowElement.find('select.block-weight');
const oldRegionName = weightField[0].className.replace(
/([^ ]+[ ]+)*block-weight-([^ ]+)([ ]+[^ ]+)*/,
'$2',
);
regionField
.removeClass(`block-region-${oldRegionName}`)
.addClass(`block-region-${regionName}`);
weightField
.removeClass(`block-weight-${oldRegionName}`)
.addClass(`block-weight-${regionName}`);
regionField[0].value = regionName;
}
updateBlockWeights(table, regionName);
};
// Add the behavior to each region select list.
$(once('block-region-select', 'select.block-region-select', context)).on(
'change',
function (event) {
// Make our new row and select field.
const row = $(this).closest('tr');
const select = $(this);
// Find the correct region and insert the row as the last in the
// region.
tableDrag.rowObject = new tableDrag.row(row[0]);
const regionMessage = table.find(
`.region-${select[0].value}-message`,
);
const regionItems = regionMessage.nextUntil(
'.region-message, .region-title',
);
if (regionItems.length) {
regionItems.last().after(row);
}
// We found that regionMessage is the last row.
else {
regionMessage.after(row);
}
updateBlockWeights(table, select[0].value);
// Modify empty regions with added or removed fields.
checkEmptyRegions(table, tableDrag.rowObject);
// Update last placed block indication.
updateLastPlaced(table, tableDrag.rowObject);
// Show unsaved changes warning.
if (!tableDrag.changed) {
$(Drupal.theme('tableDragChangedWarning'))
.insertBefore(tableDrag.table)
.hide()
.fadeIn('slow');
tableDrag.changed = true;
}
// Remove focus from selectbox.
select.trigger('blur');
},
);
},
};
})(jQuery, window, Drupal, once);

View File

@ -0,0 +1,130 @@
id: d6_block
label: Blocks
# This configuration migration depends on the d6_custom_block content migration.
migration_tags:
- Drupal 6
- Configuration
source:
plugin: block
process:
# Block status is not a thing in Drupal 8, so this is how we skip over
# disabled blocks.
status:
plugin: skip_on_empty
method: row
source: status
id:
# We need something unique, so book, book_1 etc will do.
plugin: make_unique_entity_field
entity_type: block
field: id
postfix: _
length: 29
source: module
_block_module_plugin_id:
-
plugin: static_map
source:
- module
map:
block: block
default_value: ''
-
plugin: skip_on_empty
method: process
-
plugin: migration_lookup
migration:
- d6_custom_block
source:
- delta
plugin:
-
plugin: static_map
bypass: true
source:
- module
- delta
map:
book:
0: book_navigation
comment:
0: views_block:comments_recent-block_1
forum:
0: forum_active_block
1: forum_new_block
locale:
0: language_block
search:
0: search_form_block
statistics:
0: statistics_popular_block
system:
0: system_powered_by_block
user:
0: user_login_block
1: system_menu_block:tools
2: views_block:who_s_new-block_1
3: views_block:who_s_online-who_s_online_block
-
plugin: block_plugin_id
-
plugin: skip_on_empty
method: row
theme:
plugin: block_theme
source:
- theme
- default_theme
- admin_theme
region:
plugin: block_region
source:
- theme
- '@theme'
- region
map:
garland:
olivero:
# Garland 6.x --> Olivero
header: header
footer: footer_top
left: sidebar
right: sidebar
# If mapping fails, put the block in the content region.
default_value: content
weight:
-
plugin: get
source: weight
-
# Block weights must be integers.
plugin: callback
callable: intval
settings:
plugin: block_settings
source:
- '@plugin'
- delta
- settings
- title
_role_ids:
plugin: roles_lookup
migration: d6_user_role
visibility:
plugin: block_visibility
source:
- visibility
- pages
- roles
# If the block uses PHP visibility, don't migrate it unless the PHP module
# is enabled.
skip_php: true
destination:
plugin: entity:block
migration_dependencies:
required:
- d6_menu
- d6_custom_block
- d6_user_role

View File

@ -0,0 +1,150 @@
# cspell:ignore firstcolumn secondcolumn thirdcolumn fourthcolumn
id: d7_block
label: Blocks
# This configuration migration depends on the d7_custom_block content migration.
migration_tags:
- Drupal 7
- Configuration
source:
plugin: block
process:
# Block status is not a thing in Drupal 8, so this is how we skip over
# disabled blocks.
status:
plugin: skip_on_empty
method: row
source: status
id:
-
plugin: concat
source:
- theme
- module
- delta
delimiter: _
-
plugin: machine_name
field: id
_block_module_plugin_id:
-
plugin: static_map
source:
- module
map:
block: block
default_value: ''
-
plugin: skip_on_empty
method: process
-
plugin: migration_lookup
migration:
- d7_custom_block
source:
- delta
plugin:
-
plugin: static_map
bypass: true
source:
- module
- delta
map:
book:
navigation: book_navigation
comment:
recent: views_block:comments_recent-block_1
forum:
active: forum_active_block
new: forum_new_block
# locale:
# 0: language_block
search:
form: search_form_block
statistics:
popular: statistics_popular_block
system:
main: system_main_block
'powered-by': system_powered_by_block
user:
login: user_login_block
# 1: system_menu_block:tools
new: views_block:who_s_new-block_1
online: views_block:who_s_online-who_s_online_block
-
plugin: block_plugin_id
-
plugin: skip_on_empty
method: row
theme:
plugin: block_theme
source:
- theme
- default_theme
- admin_theme
region:
plugin: block_region
source:
- theme
- '@theme'
- region
map:
bartik:
# Bartik 7.x --> Olivero 9.4.x
olivero:
featured: highlighted
triptych_first: content_below
triptych_middle: content_below
triptych_last: content_below
footer_firstcolumn: footer_top
footer_secondcolumn: footer_top
footer_thirdcolumn: footer_top
footer_fourthcolumn: footer_top
footer: footer_bottom
seven:
# Seven 7.x --> Claro 9.4.x
claro:
header: 'header'
pre_content: 'pre_content'
breadcrumb: 'breadcrumb'
highlighted: 'highlighted'
help: 'help'
content: 'content'
page_top: 'page_top'
page_bottom: 'page_bottom'
sidebar_first: 'sidebar_first'
# If mapping fails, put the block in the content region.
default_value: content
weight:
-
plugin: get
source: weight
-
# Block weights must be integers.
plugin: callback
callable: intval
settings:
plugin: block_settings
source:
- '@plugin'
- delta
- settings
- title
_role_ids:
plugin: roles_lookup
migration: d7_user_role
visibility:
plugin: block_visibility
source:
- visibility
- pages
- roles
# If the block uses PHP visibility, don't migrate it unless the PHP module
# is enabled.
skip_php: true
destination:
plugin: entity:block
migration_dependencies:
optional:
- d7_custom_block
- d7_user_role

View File

@ -0,0 +1,5 @@
finished:
6:
block: block
7:
block: block

View File

@ -0,0 +1,170 @@
<?php
namespace Drupal\block;
use Drupal\Component\Plugin\Exception\ContextException;
use Drupal\Component\Plugin\Exception\MissingValueContextException;
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Cache\Cache;
use Drupal\Core\Cache\CacheableDependencyInterface;
use Drupal\Core\Condition\ConditionAccessResolverTrait;
use Drupal\Core\Entity\EntityAccessControlHandler;
use Drupal\Core\Entity\EntityHandlerInterface;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Plugin\Context\ContextHandlerInterface;
use Drupal\Core\Plugin\Context\ContextRepositoryInterface;
use Drupal\Core\Plugin\ContextAwarePluginInterface;
use Drupal\Core\Session\AccountInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Defines the access control handler for the content block entity type.
*
* @see \Drupal\block\Entity\Block
*/
class BlockAccessControlHandler extends EntityAccessControlHandler implements EntityHandlerInterface {
use ConditionAccessResolverTrait;
/**
* The plugin context handler.
*
* @var \Drupal\Core\Plugin\Context\ContextHandlerInterface
*/
protected $contextHandler;
/**
* The context manager service.
*
* @var \Drupal\Core\Plugin\Context\ContextRepositoryInterface
*/
protected $contextRepository;
/**
* {@inheritdoc}
*/
public static function createInstance(ContainerInterface $container, EntityTypeInterface $entity_type) {
return new static(
$entity_type,
$container->get('context.handler'),
$container->get('context.repository')
);
}
/**
* Constructs the block access control handler instance.
*
* @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
* The entity type definition.
* @param \Drupal\Core\Plugin\Context\ContextHandlerInterface $context_handler
* The ContextHandler for applying contexts to conditions properly.
* @param \Drupal\Core\Plugin\Context\ContextRepositoryInterface $context_repository
* The lazy context repository service.
*/
public function __construct(EntityTypeInterface $entity_type, ContextHandlerInterface $context_handler, ContextRepositoryInterface $context_repository) {
parent::__construct($entity_type);
$this->contextHandler = $context_handler;
$this->contextRepository = $context_repository;
}
/**
* {@inheritdoc}
*/
protected function checkAccess(EntityInterface $entity, $operation, AccountInterface $account) {
/** @var \Drupal\block\BlockInterface $entity */
if ($operation != 'view') {
return parent::checkAccess($entity, $operation, $account);
}
// Don't grant access to disabled blocks.
if (!$entity->status()) {
return AccessResult::forbidden()->addCacheableDependency($entity);
}
else {
$conditions = [];
$missing_context = FALSE;
$missing_value = FALSE;
foreach ($entity->getVisibilityConditions() as $condition_id => $condition) {
if ($condition instanceof ContextAwarePluginInterface) {
try {
$contexts = $this->contextRepository->getRuntimeContexts(array_values($condition->getContextMapping()));
$this->contextHandler->applyContextMapping($condition, $contexts);
}
catch (MissingValueContextException) {
$missing_value = TRUE;
}
catch (ContextException) {
$missing_context = TRUE;
}
}
$conditions[$condition_id] = $condition;
}
if ($missing_context) {
// If any context is missing then we might be missing cacheable
// metadata, and don't know based on what conditions the block is
// accessible or not. Make sure the result cannot be cached.
$access = AccessResult::forbidden()->setCacheMaxAge(0);
}
elseif ($missing_value) {
// The contexts exist but have no value. Deny access without
// disabling caching. For example the node type condition will have a
// missing context on any non-node route like the frontpage.
$access = AccessResult::forbidden();
}
elseif ($this->resolveConditions($conditions, 'and') !== FALSE) {
// Delegate to the plugin.
$block_plugin = $entity->getPlugin();
try {
if ($block_plugin instanceof ContextAwarePluginInterface) {
$contexts = $this->contextRepository->getRuntimeContexts(array_values($block_plugin->getContextMapping()));
$this->contextHandler->applyContextMapping($block_plugin, $contexts);
}
$access = $block_plugin->access($account, TRUE);
}
catch (MissingValueContextException) {
// The contexts exist but have no value. Deny access without
// disabling caching.
$access = AccessResult::forbidden();
}
catch (ContextException) {
// If any context is missing then we might be missing cacheable
// metadata, and don't know based on what conditions the block is
// accessible or not. Make sure the result cannot be cached.
$access = AccessResult::forbidden()->setCacheMaxAge(0);
}
}
else {
$reason = count($conditions) > 1
? "One of the block visibility conditions ('%s') denied access."
: "The block visibility condition '%s' denied access.";
$access = AccessResult::forbidden(sprintf($reason, implode("', '", array_keys($conditions))));
}
$this->mergeCacheabilityFromConditions($access, $conditions);
// Ensure that access is evaluated again when the block changes.
return $access->addCacheableDependency($entity);
}
}
/**
* Merges cacheable metadata from conditions onto the access result object.
*
* @param \Drupal\Core\Access\AccessResult $access
* The access result object.
* @param \Drupal\Core\Condition\ConditionInterface[] $conditions
* List of visibility conditions.
*/
protected function mergeCacheabilityFromConditions(AccessResult $access, array $conditions) {
foreach ($conditions as $condition) {
if ($condition instanceof CacheableDependencyInterface) {
$access->addCacheTags($condition->getCacheTags());
$access->addCacheContexts($condition->getCacheContexts());
$access->setCacheMaxAge(Cache::mergeMaxAges($access->getCacheMaxAge(), $condition->getCacheMaxAge()));
}
}
}
}

View File

@ -0,0 +1,414 @@
<?php
namespace Drupal\block;
use Drupal\Component\Utility\Html;
use Drupal\Core\Plugin\PluginFormFactoryInterface;
use Drupal\Core\Block\BlockPluginInterface;
use Drupal\Core\Entity\EntityForm;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Executable\ExecutableManagerInterface;
use Drupal\Core\Extension\ThemeHandlerInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Form\SubformState;
use Drupal\Core\Language\LanguageManagerInterface;
use Drupal\Core\Plugin\ContextAwarePluginInterface;
use Drupal\Core\Plugin\Context\ContextRepositoryInterface;
use Drupal\Core\Plugin\PluginWithFormsInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Provides form for block instance forms.
*
* @internal
*/
class BlockForm extends EntityForm {
/**
* The block entity.
*
* @var \Drupal\block\BlockInterface
*/
protected $entity;
/**
* The block storage.
*
* @var \Drupal\Core\Config\Entity\ConfigEntityStorageInterface
*/
protected $storage;
/**
* The condition plugin manager.
*
* @var \Drupal\Core\Condition\ConditionManager
*/
protected $manager;
/**
* The language manager service.
*
* @var \Drupal\Core\Language\LanguageManagerInterface
*/
protected $language;
/**
* The theme handler.
*
* @var \Drupal\Core\Extension\ThemeHandler
*/
protected $themeHandler;
/**
* The context repository service.
*
* @var \Drupal\Core\Plugin\Context\ContextRepositoryInterface
*/
protected $contextRepository;
/**
* The plugin form manager.
*
* @var \Drupal\Core\Plugin\PluginFormFactoryInterface
*/
protected $pluginFormFactory;
/**
* Constructs a BlockForm object.
*
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity type manager.
* @param \Drupal\Core\Executable\ExecutableManagerInterface $manager
* The ConditionManager for building the visibility UI.
* @param \Drupal\Core\Plugin\Context\ContextRepositoryInterface $context_repository
* The lazy context repository service.
* @param \Drupal\Core\Language\LanguageManagerInterface $language
* The language manager.
* @param \Drupal\Core\Extension\ThemeHandlerInterface $theme_handler
* The theme handler.
* @param \Drupal\Core\Plugin\PluginFormFactoryInterface $plugin_form_manager
* The plugin form manager.
* @param \Drupal\block\BlockRepositoryInterface $blockRepository
* The block repository service.
*/
public function __construct(EntityTypeManagerInterface $entity_type_manager, ExecutableManagerInterface $manager, ContextRepositoryInterface $context_repository, LanguageManagerInterface $language, ThemeHandlerInterface $theme_handler, PluginFormFactoryInterface $plugin_form_manager, protected BlockRepositoryInterface $blockRepository) {
$this->storage = $entity_type_manager->getStorage('block');
$this->manager = $manager;
$this->contextRepository = $context_repository;
$this->language = $language;
$this->themeHandler = $theme_handler;
$this->pluginFormFactory = $plugin_form_manager;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('entity_type.manager'),
$container->get('plugin.manager.condition'),
$container->get('context.repository'),
$container->get('language_manager'),
$container->get('theme_handler'),
$container->get('plugin_form.factory'),
$container->get('block.repository')
);
}
/**
* {@inheritdoc}
*/
public function form(array $form, FormStateInterface $form_state) {
$entity = $this->entity;
// Store the gathered contexts in the form state for other objects to use
// during form building.
$form_state->setTemporaryValue('gathered_contexts', $this->contextRepository->getAvailableContexts());
$form['#tree'] = TRUE;
$form['settings'] = [];
$subform_state = SubformState::createForSubform($form['settings'], $form, $form_state);
$form['settings'] = $this->getPluginForm($entity->getPlugin())->buildConfigurationForm($form['settings'], $subform_state);
$form['visibility'] = $this->buildVisibilityInterface([], $form_state);
// If creating a new block, calculate a safe default machine name.
$form['id'] = [
'#type' => 'machine_name',
'#maxlength' => 64,
'#description' => $this->t('A unique name for this block instance. Must be alpha-numeric and underscore separated.'),
'#default_value' => !$entity->isNew() ? $entity->id() : $this->getUniqueMachineName($entity),
'#machine_name' => [
'exists' => '\Drupal\block\Entity\Block::load',
'replace_pattern' => '[^a-z0-9_.]+',
'source' => ['settings', 'label'],
],
'#required' => TRUE,
'#disabled' => !$entity->isNew(),
];
// Theme settings.
if ($theme = $entity->getTheme()) {
$form['theme'] = [
'#type' => 'value',
'#value' => $theme,
];
}
else {
$theme = $this->config('system.theme')->get('default');
$theme_options = [];
foreach ($this->themeHandler->listInfo() as $theme_name => $theme_info) {
if (!empty($theme_info->status)) {
$theme_options[$theme_name] = $theme_info->info['name'];
}
}
$form['theme'] = [
'#type' => 'select',
'#options' => $theme_options,
'#title' => $this->t('Theme'),
'#default_value' => $theme,
'#ajax' => [
'callback' => '::themeSwitch',
'wrapper' => 'edit-block-region-wrapper',
],
];
}
// Hidden weight setting.
$weight = $entity->isNew() ? $this->getRequest()->query->getInt('weight') : $entity->getWeight();
$form['weight'] = [
'#type' => 'hidden',
'#default_value' => $weight,
];
// Region settings.
$entity_region = $entity->getRegion();
$region = $entity->isNew() ? $this->getRequest()->query->get('region', $entity_region) : $entity_region;
$form['region'] = [
'#type' => 'select',
'#title' => $this->t('Region'),
'#description' => $this->t('Select the region where this block should be displayed.'),
'#default_value' => $region,
'#required' => TRUE,
'#options' => system_region_list($form_state->getValue('theme', $theme), REGIONS_VISIBLE),
'#prefix' => '<div id="edit-block-region-wrapper">',
'#suffix' => '</div>',
];
$form['#attached']['library'][] = 'block/drupal.block.admin';
return $form;
}
/**
* Handles switching the available regions based on the selected theme.
*/
public function themeSwitch($form, FormStateInterface $form_state) {
return $form['region'];
}
/**
* Helper function for building the visibility UI form.
*
* @param array $form
* An associative array containing the structure of the form.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The current state of the form.
*
* @return array
* The form array with the visibility UI added in.
*/
protected function buildVisibilityInterface(array $form, FormStateInterface $form_state) {
$form['visibility_tabs'] = [
'#type' => 'vertical_tabs',
'#title' => $this->t('Visibility'),
'#parents' => ['visibility_tabs'],
'#attached' => [
'library' => [
'block/drupal.block',
],
],
];
// @todo Allow list of conditions to be configured in
// https://www.drupal.org/node/2284687.
$visibility = $this->entity->getVisibility();
$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;
}
// Don't display the language condition until we have multiple languages.
if ($condition_id == 'language' && !$this->language->isMultilingual()) {
continue;
}
/** @var \Drupal\Core\Condition\ConditionInterface $condition */
$condition = $this->manager->createInstance($condition_id, $visibility[$condition_id] ?? []);
$form_state->set(['conditions', $condition_id], $condition);
$condition_form = $condition->buildConfigurationForm([], $form_state);
$condition_form['#type'] = 'details';
$condition_form['#title'] = $condition->getPluginDefinition()['label'];
$condition_form['#group'] = 'visibility_tabs';
$form[$condition_id] = $condition_form;
}
// Disable negation for specific conditions.
$disable_negation = [
'entity_bundle:node',
'language',
'response_status',
'user_role',
];
foreach ($disable_negation as $condition) {
if (isset($form[$condition])) {
$form[$condition]['negate']['#type'] = 'value';
$form[$condition]['negate']['#value'] = $form[$condition]['negate']['#default_value'];
}
}
if (isset($form['user_role'])) {
$form['user_role']['#title'] = $this->t('Roles');
unset($form['user_role']['roles']['#description']);
}
if (isset($form['request_path'])) {
$form['request_path']['#title'] = $this->t('Pages');
$form['request_path']['negate']['#type'] = 'radios';
$form['request_path']['negate']['#default_value'] = (int) $form['request_path']['negate']['#default_value'];
$form['request_path']['negate']['#title_display'] = 'invisible';
$form['request_path']['negate']['#options'] = [
$this->t('Show for the listed pages'),
$this->t('Hide for the listed pages'),
];
}
return $form;
}
/**
* {@inheritdoc}
*/
protected function actions(array $form, FormStateInterface $form_state) {
$actions = parent::actions($form, $form_state);
$actions['submit']['#value'] = $this->t('Save block');
$actions['delete']['#title'] = $this->t('Remove block');
return $actions;
}
/**
* {@inheritdoc}
*/
public function validateForm(array &$form, FormStateInterface $form_state) {
parent::validateForm($form, $form_state);
$form_state->setValue('weight', (int) $form_state->getValue('weight'));
// The Block Entity form puts all block plugin form elements in the
// settings form element, so just pass that to the block for validation.
$this->getPluginForm($this->entity->getPlugin())->validateConfigurationForm($form['settings'], SubformState::createForSubform($form['settings'], $form, $form_state));
$this->validateVisibility($form, $form_state);
}
/**
* Helper function to independently validate the visibility UI.
*
* @param array $form
* A nested array form elements comprising the form.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The current state of the form.
*/
protected function validateVisibility(array $form, FormStateInterface $form_state) {
// Validate visibility condition settings.
foreach ($form_state->getValue('visibility') as $condition_id => $values) {
// Allow the condition to validate the form.
$condition = $form_state->get(['conditions', $condition_id]);
$condition->validateConfigurationForm($form['visibility'][$condition_id], SubformState::createForSubform($form['visibility'][$condition_id], $form, $form_state));
}
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
parent::submitForm($form, $form_state);
$entity = $this->entity;
// The Block Entity form puts all block plugin form elements in the
// settings form element, so just pass that to the block for submission.
$sub_form_state = SubformState::createForSubform($form['settings'], $form, $form_state);
// Call the plugin submit handler.
$block = $entity->getPlugin();
$this->getPluginForm($block)->submitConfigurationForm($form, $sub_form_state);
// If this block is context-aware, set the context mapping.
if ($block instanceof ContextAwarePluginInterface && $block->getContextDefinitions()) {
$context_mapping = $sub_form_state->getValue('context_mapping', []);
$block->setContextMapping($context_mapping);
}
$this->submitVisibility($form, $form_state);
}
/**
* {@inheritdoc}
*/
public function save(array $form, FormStateInterface $form_state) {
$value = parent::save($form, $form_state);
$this->messenger()->addStatus($this->t('The block configuration has been saved.'));
$form_state->setRedirect(
'block.admin_display_theme',
[
'theme' => $form_state->getValue('theme'),
],
['query' => ['block-placement' => Html::getClass($this->entity->id())]]
);
return $value;
}
/**
* Helper function to independently submit the visibility UI.
*
* @param array $form
* A nested array form elements comprising the form.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The current state of the form.
*/
protected function submitVisibility(array $form, FormStateInterface $form_state) {
foreach ($form_state->getValue('visibility') as $condition_id => $values) {
// Allow the condition to submit the form.
$condition = $form_state->get(['conditions', $condition_id]);
$condition->submitConfigurationForm($form['visibility'][$condition_id], SubformState::createForSubform($form['visibility'][$condition_id], $form, $form_state));
$condition_configuration = $condition->getConfiguration();
// Update the visibility conditions on the block.
$this->entity->getVisibilityConditions()->addInstanceId($condition_id, $condition_configuration);
}
}
/**
* Generates a unique machine name for a block based on a suggested string.
*
* @param \Drupal\block\BlockInterface $block
* The block entity.
*
* @return string
* Returns the unique name.
*/
public function getUniqueMachineName(BlockInterface $block) {
$suggestion = $block->getPlugin()->getMachineNameSuggestion();
return $this->blockRepository->getUniqueMachineName($suggestion, $block->getTheme());
}
/**
* Retrieves the plugin form for a given block and operation.
*
* @param \Drupal\Core\Block\BlockPluginInterface $block
* The block plugin.
*
* @return \Drupal\Core\Plugin\PluginFormInterface
* The plugin form for the block.
*/
protected function getPluginForm(BlockPluginInterface $block) {
if ($block instanceof PluginWithFormsInterface) {
return $this->pluginFormFactory->createInstance($block, 'configure');
}
return $block;
}
}

View File

@ -0,0 +1,125 @@
<?php
namespace Drupal\block;
use Drupal\Core\Config\Entity\ConfigEntityInterface;
/**
* Provides an interface defining a block entity.
*/
interface BlockInterface extends ConfigEntityInterface {
/**
* Returns the plugin instance.
*
* @return \Drupal\Core\Block\BlockPluginInterface
* The plugin instance for this block.
*/
public function getPlugin();
/**
* Returns the plugin ID.
*
* @return string
* The plugin ID for this block.
*/
public function getPluginId();
/**
* Returns the region this block is placed in.
*
* @return string
* The region this block is placed in.
*/
public function getRegion();
/**
* Returns the theme ID.
*
* @return string
* The theme ID for this block instance.
*/
public function getTheme();
/**
* Returns an array of visibility condition configurations.
*
* @return array
* An array of visibility condition configuration keyed by the condition ID.
*/
public function getVisibility();
/**
* Gets conditions for this block.
*
* @return \Drupal\Core\Condition\ConditionInterface[]|\Drupal\Core\Condition\ConditionPluginCollection
* An array or collection of configured condition plugins.
*/
public function getVisibilityConditions();
/**
* Gets a visibility condition plugin instance.
*
* @param string $instance_id
* The condition plugin instance ID.
*
* @return \Drupal\Core\Condition\ConditionInterface
* A condition plugin.
*/
public function getVisibilityCondition($instance_id);
/**
* Sets the visibility condition configuration.
*
* @param string $instance_id
* The condition instance ID.
* @param array $configuration
* The condition configuration.
*
* @return $this
*/
public function setVisibilityConfig($instance_id, array $configuration);
/**
* Returns the weight of this block (used for sorting).
*
* @return int
* The block weight.
*/
public function getWeight();
/**
* Sets the region this block is placed in.
*
* @param string $region
* The region to place this block in.
*
* @return $this
*/
public function setRegion($region);
/**
* Sets the block weight.
*
* @param int $weight
* The desired weight.
*
* @return $this
*/
public function setWeight($weight);
/**
* Creates a duplicate of the block entity.
*
* @param string $new_id
* (optional) The new ID on the duplicate block.
* @param string $new_theme
* (optional) The theme on the duplicate block.
*
* @return static
* A clone of $this with all identifiers unset, so saving it inserts a new
* entity into the storage system.
*/
public function createDuplicateBlock($new_id = NULL, $new_theme = NULL);
}

View File

@ -0,0 +1,405 @@
<?php
namespace Drupal\block;
use Drupal\Component\Utility\Html;
use Drupal\Component\Serialization\Json;
use Drupal\Core\Config\Entity\ConfigEntityListBuilder;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityStorageInterface;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Form\FormBuilderInterface;
use Drupal\Core\Form\FormInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Messenger\MessengerInterface;
use Drupal\Core\Theme\ThemeManagerInterface;
use Drupal\Core\Url;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpFoundation\Request;
/**
* Defines a class to build a listing of block entities.
*
* @see \Drupal\block\Entity\Block
*/
class BlockListBuilder extends ConfigEntityListBuilder implements FormInterface {
/**
* The theme containing the blocks.
*
* @var string
*/
protected $theme;
/**
* The current request.
*
* @var \Symfony\Component\HttpFoundation\Request
*/
protected $request;
/**
* The theme manager.
*
* @var \Drupal\Core\Theme\ThemeManagerInterface
*/
protected $themeManager;
/**
* The form builder.
*
* @var \Drupal\Core\Form\FormBuilderInterface
*/
protected $formBuilder;
/**
* The messenger.
*
* @var \Drupal\Core\Messenger\MessengerInterface
*/
protected $messenger;
/**
* Constructs a new BlockListBuilder object.
*
* @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
* The entity type definition.
* @param \Drupal\Core\Entity\EntityStorageInterface $storage
* The entity storage class.
* @param \Drupal\Core\Theme\ThemeManagerInterface $theme_manager
* The theme manager.
* @param \Drupal\Core\Form\FormBuilderInterface $form_builder
* The form builder.
* @param \Drupal\Core\Messenger\MessengerInterface $messenger
* The messenger service.
*/
public function __construct(EntityTypeInterface $entity_type, EntityStorageInterface $storage, ThemeManagerInterface $theme_manager, FormBuilderInterface $form_builder, MessengerInterface $messenger) {
parent::__construct($entity_type, $storage);
$this->themeManager = $theme_manager;
$this->formBuilder = $form_builder;
$this->messenger = $messenger;
$this->limit = FALSE;
}
/**
* {@inheritdoc}
*/
public static function createInstance(ContainerInterface $container, EntityTypeInterface $entity_type) {
return new static(
$entity_type,
$container->get('entity_type.manager')->getStorage($entity_type->id()),
$container->get('theme.manager'),
$container->get('form_builder'),
$container->get('messenger')
);
}
/**
* {@inheritdoc}
*
* @param string|null $theme
* (optional) The theme to display the blocks for. If NULL, the current
* theme will be used.
* @param \Symfony\Component\HttpFoundation\Request $request
* The current request.
*
* @return array
* The block list as a renderable array.
*/
public function render($theme = NULL, ?Request $request = NULL) {
$this->request = $request;
$this->theme = $theme;
return $this->formBuilder->getForm($this);
}
/**
* {@inheritdoc}
*/
public function getFormId() {
return 'block_admin_display_form';
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state) {
$form['#attached']['library'][] = 'core/drupal.tableheader';
$form['#attached']['library'][] = 'block/drupal.block';
$form['#attached']['library'][] = 'block/drupal.block.admin';
$form['#attributes']['class'][] = 'clearfix';
// Build the form tree.
$form['blocks'] = $this->buildBlocksForm();
$form['actions'] = [
'#tree' => FALSE,
'#type' => 'actions',
];
$form['actions']['submit'] = [
'#type' => 'submit',
'#value' => $this->t('Save blocks'),
'#button_type' => 'primary',
];
return $form;
}
/**
* Builds the main "Blocks" portion of the form.
*
* @return array
* An array representing the blocks form structure.
*/
protected function buildBlocksForm() {
// Build blocks first for each region.
$blocks = [];
$entities = $this->load();
/** @var \Drupal\block\BlockInterface[] $entities */
foreach ($entities as $entity_id => $entity) {
$definition = $entity->getPlugin()->getPluginDefinition();
$blocks[$entity->getRegion()][$entity_id] = [
'label' => $entity->label(),
'entity_id' => $entity_id,
'weight' => $entity->getWeight(),
'entity' => $entity,
'category' => $definition['category'],
'status' => $entity->status(),
];
}
$form = [
'#type' => 'table',
'#header' => [
$this->t('Block'),
$this->t('Category'),
$this->t('Region'),
$this->t('Weight'),
$this->t('Operations'),
],
'#attributes' => [
'id' => 'blocks',
],
];
// Weights range from -delta to +delta, so delta should be at least half
// of the amount of blocks present. This makes sure all blocks in the same
// region get a unique weight.
$weight_delta = round(count($entities) / 2);
$placement = FALSE;
if ($this->request->query->has('block-placement')) {
$placement = $this->request->query->get('block-placement');
$form['#attached']['drupalSettings']['blockPlacement'] = $placement;
// Remove the block placement from the current request so that it is not
// passed on to any redirect destinations.
$this->request->query->remove('block-placement');
}
// Loop over each region and build blocks.
$regions = $this->systemRegionList($this->getThemeName(), REGIONS_VISIBLE);
foreach ($regions as $region => $title) {
$form['#tabledrag'][] = [
'action' => 'match',
'relationship' => 'sibling',
'group' => 'block-region-select',
'subgroup' => 'block-region-' . $region,
'hidden' => FALSE,
];
$form['#tabledrag'][] = [
'action' => 'order',
'relationship' => 'sibling',
'group' => 'block-weight',
'subgroup' => 'block-weight-' . $region,
];
$form['region-' . $region] = [
'#attributes' => [
'class' => ['region-title', 'region-title-' . $region],
'no_striping' => TRUE,
],
];
$form['region-' . $region]['title'] = [
'#theme_wrappers' => [
'container' => [
'#attributes' => ['class' => 'region-title__action'],
],
],
'#prefix' => $title,
'#type' => 'link',
'#title' => $this->t('Place block <span class="visually-hidden">in the %region region</span>', ['%region' => $title]),
'#url' => Url::fromRoute('block.admin_library', ['theme' => $this->getThemeName()], ['query' => ['region' => $region]]),
'#wrapper_attributes' => [
'colspan' => 5,
],
'#attributes' => [
'class' => ['use-ajax', 'button', 'button--small'],
'data-dialog-type' => 'modal',
'data-dialog-options' => Json::encode([
'width' => 880,
]),
],
];
$form['region-' . $region . '-message'] = [
'#attributes' => [
'class' => [
'region-message',
'region-' . $region . '-message',
empty($blocks[$region]) ? 'region-empty' : 'region-populated',
],
],
];
$form['region-' . $region . '-message']['message'] = [
'#markup' => '<em>' . $this->t('No blocks in this region') . '</em>',
'#wrapper_attributes' => [
'colspan' => 5,
],
];
if (isset($blocks[$region])) {
foreach ($blocks[$region] as $info) {
$entity_id = $info['entity_id'];
$form[$entity_id] = [
'#attributes' => [
'class' => ['draggable'],
],
];
$form[$entity_id]['#attributes']['class'][] = $info['status'] ? 'block-enabled' : 'block-disabled';
if ($placement && $placement == Html::getClass($entity_id)) {
$form[$entity_id]['#attributes']['class'][] = 'color-success';
$form[$entity_id]['#attributes']['class'][] = 'js-block-placed';
}
$form[$entity_id]['info'] = [
'#wrapper_attributes' => [
'class' => ['block'],
],
];
// Ensure that the label is always rendered as plain text. Render
// array #plain_text key is essentially treated same as @ placeholder
// in translatable markup.
if ($info['status']) {
$form[$entity_id]['info']['#plain_text'] = $info['label'];
}
else {
$form[$entity_id]['info']['#markup'] = $this->t('@label (disabled)', ['@label' => $info['label']]);
}
$form[$entity_id]['type'] = [
'#markup' => $info['category'],
];
$form[$entity_id]['region-theme']['region'] = [
'#type' => 'select',
'#default_value' => $region,
'#required' => TRUE,
'#title' => $this->t('Region for @block block', ['@block' => $info['label']]),
'#title_display' => 'invisible',
'#options' => $regions,
'#attributes' => [
'class' => ['block-region-select', 'block-region-' . $region],
],
'#parents' => ['blocks', $entity_id, 'region'],
];
$form[$entity_id]['region-theme']['theme'] = [
'#type' => 'hidden',
'#value' => $this->getThemeName(),
'#parents' => ['blocks', $entity_id, 'theme'],
];
$form[$entity_id]['weight'] = [
'#type' => 'weight',
'#default_value' => $info['weight'],
'#delta' => $weight_delta,
'#title' => $this->t('Weight for @block block', ['@block' => $info['label']]),
'#title_display' => 'invisible',
'#attributes' => [
'class' => ['block-weight', 'block-weight-' . $region],
],
];
$form[$entity_id]['operations'] = $this->buildOperations($info['entity']);
}
}
}
// Do not allow disabling the main system content block when it is present.
if (isset($form['system_main']['region'])) {
$form['system_main']['region']['#required'] = TRUE;
}
return $form;
}
/**
* Gets the name of the theme used for this block listing.
*
* @return string
* The name of the theme.
*/
protected function getThemeName() {
// If no theme was specified, use the current theme.
if (!$this->theme) {
$this->theme = $this->themeManager->getActiveTheme()->getName();
}
return $this->theme;
}
/**
* {@inheritdoc}
*/
protected function getEntityIds() {
return $this->getStorage()->getQuery()
->condition('theme', $this->getThemeName())
->sort($this->entityType->getKey('id'))
->execute();
}
/**
* {@inheritdoc}
*/
public function getDefaultOperations(EntityInterface $entity) {
$operations = parent::getDefaultOperations($entity);
if (isset($operations['edit'])) {
$operations['edit']['title'] = $this->t('Configure');
}
if (isset($operations['delete'])) {
$operations['delete']['title'] = $this->t('Remove');
}
return $operations;
}
/**
* {@inheritdoc}
*/
public function validateForm(array &$form, FormStateInterface $form_state) {
if (empty($form_state->getValue('blocks'))) {
$form_state->setErrorByName('blocks', $this->t('No blocks settings to update.'));
}
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
$blocks = $form_state->getValue('blocks');
$entities = $this->storage->loadMultiple(array_keys($blocks));
/** @var \Drupal\block\BlockInterface[] $entities */
foreach ($entities as $entity_id => $entity) {
$entity_values = $form_state->getValue(['blocks', $entity_id]);
$entity->setWeight($entity_values['weight']);
$entity->setRegion($entity_values['region']);
$entity->save();
}
$this->messenger->addStatus($this->t('The block settings have been updated.'));
}
/**
* Wraps system_region_list().
*/
protected function systemRegionList($theme, $show = REGIONS_ALL) {
return system_region_list($theme, $show);
}
}

View File

@ -0,0 +1,71 @@
<?php
namespace Drupal\block;
use Drupal\Component\Plugin\Exception\PluginException;
use Drupal\Component\Plugin\PluginManagerInterface;
use Drupal\Core\Plugin\DefaultSingleLazyPluginCollection;
/**
* Provides a collection of block plugins.
*/
class BlockPluginCollection extends DefaultSingleLazyPluginCollection {
/**
* The block ID this plugin collection belongs to.
*
* @var string
*/
protected $blockId;
/**
* Constructs a new BlockPluginCollection.
*
* @param \Drupal\Component\Plugin\PluginManagerInterface $manager
* The manager to be used for instantiating plugins.
* @param string $instance_id
* The ID of the plugin instance.
* @param array $configuration
* An array of configuration.
* @param string $block_id
* The unique ID of the block entity using this plugin.
*/
public function __construct(PluginManagerInterface $manager, $instance_id, array $configuration, $block_id) {
parent::__construct($manager, $instance_id, $configuration);
$this->blockId = $block_id;
}
/**
* {@inheritdoc}
*
* @return \Drupal\Core\Block\BlockPluginInterface
* The block plugin instance.
*/
public function &get($instance_id) {
return parent::get($instance_id);
}
/**
* {@inheritdoc}
*/
protected function initializePlugin($instance_id) {
if (!$instance_id) {
throw new PluginException("The block '{$this->blockId}' did not specify a plugin.");
}
try {
parent::initializePlugin($instance_id);
}
catch (PluginException $e) {
$module = $this->configuration['provider'];
// Ignore blocks belonging to uninstalled modules, but re-throw valid
// exceptions when the module is installed and the plugin is
// misconfigured.
if (!$module || \Drupal::moduleHandler()->moduleExists($module)) {
throw $e;
}
}
}
}

View File

@ -0,0 +1,114 @@
<?php
namespace Drupal\block;
use Drupal\Core\Cache\CacheableMetadata;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Plugin\Context\ContextHandlerInterface;
use Drupal\Core\Theme\ThemeManagerInterface;
/**
* Provides a repository for Block config entities.
*/
class BlockRepository implements BlockRepositoryInterface {
/**
* The block storage.
*
* @var \Drupal\Core\Entity\EntityStorageInterface
*/
protected $blockStorage;
/**
* The theme manager.
*
* @var \Drupal\Core\Theme\ThemeManagerInterface
*/
protected $themeManager;
/**
* The context handler.
*
* @var \Drupal\Core\Plugin\Context\ContextHandlerInterface
*/
protected $contextHandler;
/**
* Constructs a new BlockRepository.
*
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity type manager service.
* @param \Drupal\Core\Theme\ThemeManagerInterface $theme_manager
* The theme manager.
* @param \Drupal\Core\Plugin\Context\ContextHandlerInterface $context_handler
* The plugin context handler.
*/
public function __construct(EntityTypeManagerInterface $entity_type_manager, ThemeManagerInterface $theme_manager, ContextHandlerInterface $context_handler) {
$this->blockStorage = $entity_type_manager->getStorage('block');
$this->themeManager = $theme_manager;
$this->contextHandler = $context_handler;
}
/**
* {@inheritdoc}
*/
public function getVisibleBlocksPerRegion(array &$cacheable_metadata = []) {
$active_theme = $this->themeManager->getActiveTheme();
// Build an array of the region names in the right order.
$empty = array_fill_keys($active_theme->getRegions(), []);
$full = [];
foreach ($this->blockStorage->loadByProperties(['theme' => $active_theme->getName()]) as $block_id => $block) {
/** @var \Drupal\block\BlockInterface $block */
$access = $block->access('view', NULL, TRUE);
$region = $block->getRegion();
if (!isset($cacheable_metadata[$region])) {
$cacheable_metadata[$region] = CacheableMetadata::createFromObject($access);
}
else {
$cacheable_metadata[$region] = $cacheable_metadata[$region]->merge(CacheableMetadata::createFromObject($access));
}
// Set the contexts on the block before checking access.
if ($access->isAllowed()) {
$full[$region][$block_id] = $block;
}
}
// Merge it with the actual values to maintain the region ordering.
$assignments = array_intersect_key(array_merge($empty, $full), $empty);
foreach ($assignments as &$assignment) {
uasort($assignment, 'Drupal\block\Entity\Block::sort');
}
return $assignments;
}
/**
* {@inheritdoc}
*/
public function getUniqueMachineName(string $suggestion, ?string $theme = NULL): string {
if ($theme) {
$suggestion = $theme . '_' . $suggestion;
}
// Get all the block machine names that begin with the suggested string.
$query = $this->blockStorage->getQuery();
$query->accessCheck(FALSE);
$query->condition('id', $suggestion, 'CONTAINS');
$block_ids = $query->execute();
$block_ids = array_map(function ($block_id) {
$parts = explode('.', $block_id);
return end($parts);
}, $block_ids);
// Iterate through potential IDs until we get a new one. E.g.
// For example, 'plugin', 'plugin_2', 'plugin_3', etc.
$count = 1;
$machine_default = $suggestion;
while (in_array($machine_default, $block_ids)) {
$machine_default = $suggestion . '_' . ++$count;
}
return $machine_default;
}
}

View File

@ -0,0 +1,50 @@
<?php
namespace Drupal\block;
/**
* Provides an interface for the block repository.
*/
interface BlockRepositoryInterface {
/**
* Return only visible regions.
*
* @see system_region_list()
*/
const REGIONS_VISIBLE = 'visible';
/**
* Return all regions.
*
* @see system_region_list()
*/
const REGIONS_ALL = 'all';
/**
* Returns an array of regions and their block entities.
*
* @param \Drupal\Core\Cache\CacheableMetadata[] $cacheable_metadata
* (optional) List of CacheableMetadata objects, keyed by region. This is
* by reference and is used to pass this information back to the caller.
*
* @return array
* The array is first keyed by region machine name, with the values
* containing an array keyed by block ID, with block entities as the values.
*/
public function getVisibleBlocksPerRegion(array &$cacheable_metadata = []);
/**
* Based on a suggested string generates a unique machine name for a block.
*
* @param string $suggestion
* The suggested block ID.
* @param string $theme
* The machine name of the theme.
*
* @return string
* Returns the unique name.
*/
public function getUniqueMachineName(string $suggestion, string $theme): string;
}

View File

@ -0,0 +1,234 @@
<?php
namespace Drupal\block;
use Drupal\Core\Block\MainContentBlockPluginInterface;
use Drupal\Core\Block\TitleBlockPluginInterface;
use Drupal\Core\Cache\Cache;
use Drupal\Core\Cache\CacheableMetadata;
use Drupal\Core\Entity\EntityViewBuilder;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Plugin\ContextAwarePluginInterface;
use Drupal\Core\Render\Element;
use Drupal\block\Entity\Block;
use Drupal\Core\Security\TrustedCallbackInterface;
/**
* Provides a Block view builder.
*/
class BlockViewBuilder extends EntityViewBuilder implements TrustedCallbackInterface {
/**
* {@inheritdoc}
*/
public function buildComponents(array &$build, array $entities, array $displays, $view_mode) {
}
/**
* {@inheritdoc}
*/
public function view(EntityInterface $entity, $view_mode = 'full', $langcode = NULL) {
$build = $this->viewMultiple([$entity], $view_mode, $langcode);
return reset($build);
}
/**
* {@inheritdoc}
*/
public function viewMultiple(array $entities = [], $view_mode = 'full', $langcode = NULL) {
/** @var \Drupal\block\BlockInterface[] $entities */
$build = [];
foreach ($entities as $entity) {
$entity_id = $entity->id();
$plugin = $entity->getPlugin();
$cache_tags = Cache::mergeTags($this->getCacheTags(), $entity->getCacheTags());
$cache_tags = Cache::mergeTags($cache_tags, $plugin->getCacheTags());
// Create the render array for the block as a whole.
// @see template_preprocess_block().
$build[$entity_id] = [
'#cache' => [
'keys' => ['entity_view', 'block', $entity->id()],
'contexts' => Cache::mergeContexts(
$entity->getCacheContexts(),
$plugin->getCacheContexts()
),
'tags' => $cache_tags,
'max-age' => $plugin->getCacheMaxAge(),
],
'#weight' => $entity->getWeight(),
];
// Allow altering of cacheability metadata or setting #create_placeholder.
$this->moduleHandler->alter(['block_build', "block_build_" . $plugin->getBaseId()], $build[$entity_id], $plugin);
if ($plugin instanceof MainContentBlockPluginInterface || $plugin instanceof TitleBlockPluginInterface) {
// Immediately build a #pre_render-able block, since this block cannot
// be built lazily.
$cacheableMetadata = CacheableMetadata::createFromRenderArray($build[$entity_id]);
$preRenderableBlock = static::buildPreRenderableBlock($entity, $this->moduleHandler());
$cacheableMetadata->addCacheableDependency(CacheableMetadata::createFromRenderArray($preRenderableBlock));
$build[$entity_id] += $preRenderableBlock;
$cacheableMetadata->applyTo($build[$entity_id]);
}
else {
// Assign a #lazy_builder callback, which will generate a #pre_render-
// able block lazily (when necessary).
$build[$entity_id] += [
'#lazy_builder' => [static::class . '::lazyBuilder', [$entity_id, $view_mode, $langcode]],
];
// Only add create_placeholder if it's explicitly set to TRUE, so it can
// be set to TRUE by automatic placeholdering conditions if it's absent.
if ($plugin->createPlaceholder()) {
$build[$entity_id] += [
'#create_placeholder' => TRUE,
];
}
}
}
return $build;
}
/**
* Builds a #pre_render-able block render array.
*
* @param \Drupal\block\BlockInterface $entity
* A block config entity.
* @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
* The module handler service.
*
* @return array
* A render array with a #pre_render callback to render the block.
*/
protected static function buildPreRenderableBlock(BlockInterface $entity, ModuleHandlerInterface $module_handler) {
$plugin = $entity->getPlugin();
$plugin_id = $plugin->getPluginId();
$base_id = $plugin->getBaseId();
$derivative_id = $plugin->getDerivativeId();
$configuration = $plugin->getConfiguration();
// Inject runtime contexts.
if ($plugin instanceof ContextAwarePluginInterface) {
$contexts = \Drupal::service('context.repository')->getRuntimeContexts($plugin->getContextMapping());
\Drupal::service('context.handler')->applyContextMapping($plugin, $contexts);
}
// Create the render array for the block as a whole.
// @see template_preprocess_block().
$build = [
'#theme' => 'block',
'#attributes' => [],
// All blocks get a "Configure block" contextual link.
'#contextual_links' => [
'block' => [
'route_parameters' => ['block' => $entity->id()],
],
],
'#weight' => $entity->getWeight(),
'#configuration' => $configuration,
'#plugin_id' => $plugin_id,
'#base_plugin_id' => $base_id,
'#derivative_plugin_id' => $derivative_id,
'#id' => $entity->id(),
'#pre_render' => [
static::class . '::preRender',
],
// Add the entity so that it can be used in the #pre_render method.
'#block' => $entity,
];
// If an alter hook wants to modify the block contents, it can append
// another #pre_render hook.
$module_handler->alter(['block_view', "block_view_$base_id"], $build, $plugin);
return $build;
}
/**
* {@inheritdoc}
*/
public static function trustedCallbacks() {
return ['preRender', 'lazyBuilder'];
}
/**
* Render API callback: Builds a block that can be pre-rendered.
*
* This function is assigned as a #lazy_builder callback.
*
* @param string $entity_id
* A block config entity ID.
* @param string $view_mode
* The view mode the block is being viewed in.
*
* @return array
* A render array with a #pre_render callback to render the block.
*/
public static function lazyBuilder($entity_id, $view_mode) {
return static::buildPreRenderableBlock(Block::load($entity_id), \Drupal::service('module_handler'));
}
/**
* Render API callback: Builds a block.
*
* This function is assigned as a #pre_render callback.
*
* Renders the content using the provided block plugin, and then:
* - if there is no content, aborts rendering, and makes sure the block won't
* be rendered.
* - if there is content, moves the contextual links from the block content to
* the block itself.
*/
public static function preRender($build) {
$content = $build['#block']->getPlugin()->build();
// Remove the block entity from the render array, to ensure that blocks
// can be rendered without the block config entity.
unset($build['#block']);
if ($content !== NULL && !Element::isEmpty($content)) {
// 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 (for instance, its own #theme) without
// conflicting with the properties used above, or alternate ones used by
// alternate block rendering approaches in contrib (for instance, 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 (['#attributes', '#contextual_links'] as $property) {
if (isset($content[$property])) {
$build[$property] += $content[$property];
unset($content[$property]);
}
}
$build['content'] = $content;
}
// Either the block's content is completely empty, or it consists only of
// cacheability metadata.
else {
// Abort rendering: render as the empty string and ensure this block is
// render cached, so we can avoid the work of having to repeatedly
// determine whether the block is empty. For instance, modifying or adding
// entities could cause the block to no longer be empty.
$build = [
'#markup' => '',
'#cache' => $build['#cache'],
];
// If $content is not empty, then it contains cacheability metadata, and
// we must merge it with the existing cacheability metadata. This allows
// blocks to be empty, yet still bubble cacheability metadata, to indicate
// why they are empty.
if (!empty($content)) {
CacheableMetadata::createFromRenderArray($build)
->merge(CacheableMetadata::createFromRenderArray($content))
->applyTo($build);
}
}
return $build;
}
}

View File

@ -0,0 +1,30 @@
<?php
namespace Drupal\block\Controller;
use Drupal\Core\Controller\ControllerBase;
/**
* Controller for building the block instance add form.
*/
class BlockAddController extends ControllerBase {
/**
* Build the block instance add form.
*
* @param string $plugin_id
* The plugin ID for the block instance.
* @param string $theme
* The name of the theme for the block instance.
*
* @return array
* The block instance edit form.
*/
public function blockAddConfigureForm($plugin_id, $theme) {
// Create a block entity.
$entity = $this->entityTypeManager()->getStorage('block')->create(['plugin' => $plugin_id, 'theme' => $theme]);
return $this->entityFormBuilder()->getForm($entity);
}
}

View File

@ -0,0 +1,108 @@
<?php
namespace Drupal\block\Controller;
use Drupal\Component\Utility\Html;
use Drupal\block\BlockInterface;
use Drupal\Core\Controller\ControllerBase;
use Drupal\Core\Extension\ThemeHandlerInterface;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
/**
* Controller routines for admin block routes.
*/
class BlockController extends ControllerBase {
/**
* The theme handler.
*
* @var \Drupal\Core\Extension\ThemeHandlerInterface
*/
protected $themeHandler;
/**
* Constructs a new BlockController instance.
*
* @param \Drupal\Core\Extension\ThemeHandlerInterface $theme_handler
* The theme handler.
*/
public function __construct(ThemeHandlerInterface $theme_handler) {
$this->themeHandler = $theme_handler;
}
/**
* Calls a method on a block and reloads the listing page.
*
* @param \Drupal\block\BlockInterface $block
* The block being acted upon.
* @param string $op
* The operation to perform, e.g., 'enable' or 'disable'.
*
* @return \Symfony\Component\HttpFoundation\RedirectResponse
* A redirect back to the listing page.
*/
public function performOperation(BlockInterface $block, $op) {
$block->$op()->save();
$this->messenger()->addStatus($this->t('The block settings have been updated.'));
return $this->redirect('block.admin_display');
}
/**
* Returns a block theme demo page.
*
* @param string $theme
* The name of the theme.
*
* @return array
* A #type 'page' render array containing the block region demo.
*/
public function demo($theme) {
if (!$this->themeHandler->hasUi($theme)) {
throw new NotFoundHttpException();
}
$page = [
'#title' => Html::escape($this->themeHandler->getName($theme)),
'#type' => 'page',
'#attached' => [
'drupalSettings' => [
// The block demonstration page is not marked as an administrative
// page by \Drupal::service('router.admin_context')->isAdminRoute()
// function in order to use the frontend theme. Since JavaScript
// relies on a proper separation of admin pages, it needs to know this
// is an actual administrative page.
'path' => ['currentPathIsAdmin' => TRUE],
],
'library' => [
'block/drupal.block.admin',
],
],
];
// Show descriptions in each visible page region, nothing else.
$visible_regions = $this->getVisibleRegionNames($theme);
foreach (array_keys($visible_regions) as $region) {
$page[$region]['block_description'] = [
'#type' => 'inline_template',
'#template' => '<div class="block-region demo-block">{{ region_name }}</div>',
'#context' => ['region_name' => $visible_regions[$region]],
];
}
return $page;
}
/**
* Returns the human-readable list of regions keyed by machine name.
*
* @param string $theme
* The name of the theme.
*
* @return array
* An array of human-readable region names keyed by machine name.
*/
protected function getVisibleRegionNames($theme) {
return system_region_list($theme, REGIONS_VISIBLE);
}
}

View File

@ -0,0 +1,186 @@
<?php
namespace Drupal\block\Controller;
use Drupal\Component\Serialization\Json;
use Drupal\Core\Block\BlockManagerInterface;
use Drupal\Core\Controller\ControllerBase;
use Drupal\Core\EventSubscriber\MainContentViewSubscriber;
use Drupal\Core\Menu\LocalActionManagerInterface;
use Drupal\Core\Plugin\Context\ContextRepositoryInterface;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\Core\Url;
use Symfony\Component\HttpFoundation\Request;
/**
* Provides a list of block plugins to be added to the layout.
*/
class BlockLibraryController extends ControllerBase {
/**
* The block manager.
*
* @var \Drupal\Core\Block\BlockManagerInterface
*/
protected $blockManager;
/**
* The context repository.
*
* @var \Drupal\Core\Plugin\Context\LazyContextRepository
*/
protected $contextRepository;
/**
* The route match.
*
* @var \Drupal\Core\Routing\RouteMatchInterface
*/
protected $routeMatch;
/**
* The local action manager.
*
* @var \Drupal\Core\Menu\LocalActionManagerInterface
*/
protected $localActionManager;
/**
* Constructs a BlockLibraryController object.
*
* @param \Drupal\Core\Block\BlockManagerInterface $block_manager
* The block manager.
* @param \Drupal\Core\Plugin\Context\ContextRepositoryInterface $context_repository
* The context repository.
* @param \Drupal\Core\Routing\RouteMatchInterface $route_match
* The current route match.
* @param \Drupal\Core\Menu\LocalActionManagerInterface $local_action_manager
* The local action manager.
*/
public function __construct(BlockManagerInterface $block_manager, ContextRepositoryInterface $context_repository, RouteMatchInterface $route_match, LocalActionManagerInterface $local_action_manager) {
$this->blockManager = $block_manager;
$this->routeMatch = $route_match;
$this->localActionManager = $local_action_manager;
$this->contextRepository = $context_repository;
}
/**
* Shows a list of blocks that can be added to a theme's layout.
*
* @param \Symfony\Component\HttpFoundation\Request $request
* The current request.
* @param string $theme
* Theme key of the block list.
*
* @return array
* A render array as expected by the renderer.
*/
public function listBlocks(Request $request, $theme) {
// Since modals do not render any other part of the page, we need to render
// them manually as part of this listing.
if ($request->query->get(MainContentViewSubscriber::WRAPPER_FORMAT) === 'drupal_modal') {
$build['local_actions'] = $this->buildLocalActions();
}
$headers = [
['data' => $this->t('Block')],
['data' => $this->t('Category')],
['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->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.
$definitions = array_filter($definitions, function (array $definition) {
return empty($definition['_block_ui_hidden']);
});
$rows = [];
foreach ($definitions as $plugin_id => $plugin_definition) {
$row = [];
$row['title']['data'] = [
'#type' => 'inline_template',
'#template' => '<div class="block-filter-text-source">{{ label }}</div>',
'#context' => [
'label' => $plugin_definition['admin_label'],
],
];
$row['category']['data'] = $plugin_definition['category'];
$links['add'] = [
'title' => $this->t('Place block'),
'url' => Url::fromRoute('block.admin_add', ['plugin_id' => $plugin_id, 'theme' => $theme]),
'attributes' => [
'class' => ['use-ajax'],
'data-dialog-type' => 'modal',
'data-dialog-options' => Json::encode([
'width' => 970,
]),
],
];
if ($region) {
$links['add']['query']['region'] = $region;
}
if (isset($weight)) {
$links['add']['query']['weight'] = $weight;
}
$row['operations']['data'] = [
'#type' => 'operations',
'#links' => $links,
];
$rows[] = $row;
}
$build['#attached']['library'][] = 'block/drupal.block.admin';
$build['filter'] = [
'#type' => 'search',
'#title' => $this->t('Filter'),
'#title_display' => 'invisible',
'#size' => 30,
'#placeholder' => $this->t('Filter by block name'),
'#attributes' => [
'class' => ['block-filter-text'],
'data-element' => '.block-add-table',
'title' => $this->t('Enter a part of the block name to filter by.'),
],
];
$build['blocks'] = [
'#type' => 'table',
'#header' => $headers,
'#rows' => $rows,
'#empty' => $this->t('No blocks available.'),
'#attributes' => [
'class' => ['block-add-table'],
],
];
return $build;
}
/**
* Builds the local actions for this listing.
*
* @return array
* An array of local actions for this listing.
*/
protected function buildLocalActions() {
$build = $this->localActionManager->getActionsForRoute($this->routeMatch->getRouteName());
// Without this workaround, the action links will be rendered as <li> with
// no wrapping <ul> element.
if (!empty($build)) {
$build['#prefix'] = '<ul class="action-links">';
$build['#suffix'] = '</ul>';
}
return $build;
}
}

View File

@ -0,0 +1,53 @@
<?php
namespace Drupal\block\Controller;
use Drupal\Core\Entity\Controller\EntityListController;
use Drupal\Core\Extension\ThemeHandlerInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
/**
* Defines a controller to list blocks.
*/
class BlockListController extends EntityListController {
/**
* The theme handler.
*
* @var \Drupal\Core\Extension\ThemeHandlerInterface
*/
protected $themeHandler;
/**
* Constructs the BlockListController.
*
* @param \Drupal\Core\Extension\ThemeHandlerInterface $theme_handler
* The theme handler.
*/
public function __construct(ThemeHandlerInterface $theme_handler) {
$this->themeHandler = $theme_handler;
}
/**
* Shows the block administration page.
*
* @param string|null $theme
* Theme key of block list.
* @param \Symfony\Component\HttpFoundation\Request $request
* The current request.
*
* @return array
* A render array as expected by
* \Drupal\Core\Render\RendererInterface::render().
*/
public function listing($theme = NULL, ?Request $request = NULL) {
$theme = $theme ?: $this->config('system.theme')->get('default');
if (!$this->themeHandler->hasUi($theme)) {
throw new NotFoundHttpException();
}
return $this->entityTypeManager()->getListBuilder('block')->render($theme, $request);
}
}

View File

@ -0,0 +1,63 @@
<?php
namespace Drupal\block\Controller;
use Drupal\Component\Utility\Html;
use Drupal\Core\Block\BlockManagerInterface;
use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
/**
* Returns autocomplete responses for block categories.
*/
class CategoryAutocompleteController implements ContainerInjectionInterface {
/**
* The block manager.
*
* @var \Drupal\Core\Block\BlockManagerInterface
*/
protected $blockManager;
/**
* Constructs a new CategoryAutocompleteController.
*
* @param \Drupal\Core\Block\BlockManagerInterface $block_manager
* The block manager.
*/
public function __construct(BlockManagerInterface $block_manager) {
$this->blockManager = $block_manager;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('plugin.manager.block')
);
}
/**
* Retrieves suggestions for block category autocompletion.
*
* @param \Symfony\Component\HttpFoundation\Request $request
* The current request.
*
* @return \Symfony\Component\HttpFoundation\JsonResponse
* A JSON response containing autocomplete suggestions.
*/
public function autocomplete(Request $request) {
$typed_category = $request->query->get('q');
$matches = [];
foreach ($this->blockManager->getCategories() as $category) {
if (stripos($category, $typed_category) === 0) {
$matches[] = ['value' => $category, 'label' => Html::escape($category)];
}
}
return new JsonResponse($matches);
}
}

View File

@ -0,0 +1,391 @@
<?php
namespace Drupal\block\Entity;
use Drupal\block\BlockAccessControlHandler;
use Drupal\block\BlockForm;
use Drupal\block\BlockListBuilder;
use Drupal\block\BlockViewBuilder;
use Drupal\block\Form\BlockDeleteForm;
use Drupal\Core\Entity\Attribute\ConfigEntityType;
use Drupal\Core\Cache\Cache;
use Drupal\Core\Condition\ConditionPluginCollection;
use Drupal\Core\Config\Action\Attribute\ActionMethod;
use Drupal\Core\Config\Entity\ConfigEntityBase;
use Drupal\block\BlockPluginCollection;
use Drupal\block\BlockInterface;
use Drupal\Core\Config\Entity\ConfigEntityInterface;
use Drupal\Core\Entity\EntityWithPluginCollectionInterface;
use Drupal\Core\Entity\EntityStorageInterface;
use Symfony\Component\Validator\Context\ExecutionContextInterface;
use Drupal\Core\StringTranslation\TranslatableMarkup;
/**
* Defines a Block configuration entity class.
*/
#[ConfigEntityType(
id: 'block',
label: new TranslatableMarkup('Block'),
label_collection: new TranslatableMarkup('Blocks'),
label_singular: new TranslatableMarkup('block'),
label_plural: new TranslatableMarkup('blocks'),
entity_keys: [
'id' => 'id',
'status' => 'status',
],
handlers: [
'access' => BlockAccessControlHandler::class,
'view_builder' => BlockViewBuilder::class,
'list_builder' => BlockListBuilder::class,
'form' => [
'default' => BlockForm::class,
'delete' => BlockDeleteForm::class,
],
],
links: [
'delete-form' => '/admin/structure/block/manage/{block}/delete',
'edit-form' => '/admin/structure/block/manage/{block}',
'enable' => '/admin/structure/block/manage/{block}/enable',
'disable' => '/admin/structure/block/manage/{block}/disable',
],
admin_permission: 'administer blocks',
label_count: [
'singular' => '@count block',
'plural' => '@count blocks',
],
lookup_keys: [
'theme',
],
config_export: [
'id',
'theme',
'region',
'weight',
'provider',
'plugin',
'settings',
'visibility',
],
)]
class Block extends ConfigEntityBase implements BlockInterface, EntityWithPluginCollectionInterface {
/**
* The ID of the block.
*
* @var string
*/
protected $id;
/**
* The plugin instance settings.
*
* @var array
*/
protected $settings = [];
/**
* The region this block is placed in.
*
* @var string
*/
protected $region;
/**
* The block weight.
*
* @var int
*/
protected $weight = 0;
/**
* The plugin instance ID.
*
* @var string
*/
protected $plugin;
/**
* The visibility settings for this block.
*
* @var array
*/
protected $visibility = [];
/**
* The plugin collection that holds the block plugin for this entity.
*
* @var \Drupal\block\BlockPluginCollection
*/
protected $pluginCollection;
/**
* The available contexts for this block and its visibility conditions.
*
* @var array
*/
protected $contexts = [];
/**
* The visibility collection.
*
* @var \Drupal\Core\Condition\ConditionPluginCollection
*/
protected $visibilityCollection;
/**
* The condition plugin manager.
*
* @var \Drupal\Core\Executable\ExecutableManagerInterface
*/
protected $conditionPluginManager;
/**
* The theme that includes the block plugin for this entity.
*
* @var string
*/
protected $theme;
/**
* {@inheritdoc}
*/
public function getPlugin() {
return $this->getPluginCollection()->get($this->plugin);
}
/**
* Encapsulates the creation of the block's LazyPluginCollection.
*
* @return \Drupal\Component\Plugin\LazyPluginCollection
* The block's plugin collection.
*/
protected function getPluginCollection() {
if (!$this->pluginCollection) {
$this->pluginCollection = new BlockPluginCollection(\Drupal::service('plugin.manager.block'), $this->plugin, $this->get('settings'), $this->id());
}
return $this->pluginCollection;
}
/**
* {@inheritdoc}
*/
public function getPluginCollections() {
return [
'settings' => $this->getPluginCollection(),
'visibility' => $this->getVisibilityConditions(),
];
}
/**
* {@inheritdoc}
*/
public function getPluginId() {
return $this->plugin;
}
/**
* {@inheritdoc}
*/
public function getRegion() {
return $this->region;
}
/**
* {@inheritdoc}
*/
public function getTheme() {
return $this->theme;
}
/**
* {@inheritdoc}
*/
public function getWeight() {
return $this->weight;
}
/**
* {@inheritdoc}
*/
public function label() {
$settings = $this->get('settings');
if ($settings['label']) {
return $settings['label'];
}
else {
$definition = $this->getPlugin()->getPluginDefinition();
return $definition['admin_label'];
}
}
/**
* Sorts active blocks by weight; sorts inactive blocks by name.
*/
public static function sort(ConfigEntityInterface $a, ConfigEntityInterface $b) {
// Separate enabled from disabled.
$status = (int) $b->status() - (int) $a->status();
if ($status !== 0) {
return $status;
}
// Sort by weight.
$weight = $a->getWeight() - $b->getWeight();
if ($weight) {
return $weight;
}
// Sort by label.
return strcmp($a->label(), $b->label());
}
/**
* {@inheritdoc}
*/
public function calculateDependencies() {
parent::calculateDependencies();
$this->addDependency('theme', $this->theme);
return $this;
}
/**
* {@inheritdoc}
*/
public function postSave(EntityStorageInterface $storage, $update = TRUE) {
parent::postSave($storage, $update);
// EntityBase::postSave() calls EntityBase::invalidateTagsOnSave(), which
// only handles the regular cases. The Block entity has one special case: a
// newly created block may *also* appear on any page in the current theme,
// so we must invalidate the associated block's cache tag (which includes
// the theme cache tag).
if (!$update) {
Cache::invalidateTags($this->getCacheTagsToInvalidate());
}
}
/**
* {@inheritdoc}
*/
public function getVisibility() {
return $this->getVisibilityConditions()->getConfiguration();
}
/**
* {@inheritdoc}
*/
public function setVisibilityConfig($instance_id, array $configuration) {
$conditions = $this->getVisibilityConditions();
if (!$conditions->has($instance_id)) {
$configuration['id'] = $instance_id;
$conditions->addInstanceId($instance_id, $configuration);
}
else {
$conditions->setInstanceConfiguration($instance_id, $configuration);
}
return $this;
}
/**
* {@inheritdoc}
*/
public function getVisibilityConditions() {
if (!isset($this->visibilityCollection)) {
$this->visibilityCollection = new ConditionPluginCollection($this->conditionPluginManager(), $this->get('visibility'));
}
return $this->visibilityCollection;
}
/**
* {@inheritdoc}
*/
public function getVisibilityCondition($instance_id) {
return $this->getVisibilityConditions()->get($instance_id);
}
/**
* Gets the condition plugin manager.
*
* @return \Drupal\Core\Executable\ExecutableManagerInterface
* The condition plugin manager.
*/
protected function conditionPluginManager() {
if (!isset($this->conditionPluginManager)) {
$this->conditionPluginManager = \Drupal::service('plugin.manager.condition');
}
return $this->conditionPluginManager;
}
/**
* {@inheritdoc}
*/
#[ActionMethod(adminLabel: new TranslatableMarkup('Set block region'), pluralize: FALSE)]
public function setRegion($region) {
$this->region = $region;
return $this;
}
/**
* {@inheritdoc}
*/
#[ActionMethod(adminLabel: new TranslatableMarkup('Set block weight'), pluralize: FALSE)]
public function setWeight($weight) {
$this->weight = (int) $weight;
return $this;
}
/**
* {@inheritdoc}
*/
public function createDuplicateBlock($new_id = NULL, $new_theme = NULL) {
$duplicate = parent::createDuplicate();
if (!empty($new_id)) {
$duplicate->id = $new_id;
}
if (!empty($new_theme)) {
$duplicate->theme = $new_theme;
}
return $duplicate;
}
/**
* {@inheritdoc}
*/
public function preSave(EntityStorageInterface $storage) {
parent::preSave($storage);
if (!is_int($this->weight)) {
@trigger_error('Saving a block with a non-integer weight is deprecated in drupal:11.1.0 and removed in drupal:12.0.0. See https://www.drupal.org/node/3462474', E_USER_DEPRECATED);
$this->setWeight((int) $this->weight);
}
// Ensure the region is valid to mirror the behavior of block_rebuild().
// This is done primarily for backwards compatibility support of
// \Drupal\block\BlockInterface::BLOCK_REGION_NONE.
$regions = system_region_list($this->theme);
if (!isset($regions[$this->region]) && $this->status()) {
$this
->setRegion(system_default_region($this->theme))
->disable();
}
}
/**
* Validates that a region exists in the active theme.
*
* @param null|string $region
* The region to validate.
* @param \Symfony\Component\Validator\Context\ExecutionContextInterface $context
* The validation context.
*/
public static function validateRegion(?string $region, ExecutionContextInterface $context): void {
if ($theme = $context->getRoot()->get('theme')->getValue()) {
if (!array_key_exists($region, system_region_list($theme))) {
$context->addViolation('This is not a valid region of the %theme theme.', ['%theme' => $theme]);
}
}
else {
$context->addViolation('This block does not say which theme it appears in.');
}
}
}

View File

@ -0,0 +1,34 @@
<?php
namespace Drupal\block\EventSubscriber;
use Drupal\Core\Render\PageDisplayVariantSelectionEvent;
use Drupal\Core\Render\RenderEvents;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
/**
* Selects the block page display variant.
*
* @see \Drupal\block\Plugin\DisplayVariant\BlockPageVariant
*/
class BlockPageDisplayVariantSubscriber implements EventSubscriberInterface {
/**
* Selects the block page display variant.
*
* @param \Drupal\Core\Render\PageDisplayVariantSelectionEvent $event
* The event to process.
*/
public function onSelectPageDisplayVariant(PageDisplayVariantSelectionEvent $event) {
$event->setPluginId('block_page');
}
/**
* {@inheritdoc}
*/
public static function getSubscribedEvents(): array {
$events[RenderEvents::SELECT_PAGE_DISPLAY_VARIANT][] = ['onSelectPageDisplayVariant'];
return $events;
}
}

View File

@ -0,0 +1,71 @@
<?php
namespace Drupal\block\Form;
use Drupal\Core\Entity\EntityDeleteForm;
use Drupal\Core\Url;
/**
* Provides a deletion confirmation form for the block instance deletion form.
*
* @internal
*/
class BlockDeleteForm extends EntityDeleteForm {
/**
* {@inheritdoc}
*/
public function getCancelUrl() {
return new Url('block.admin_display');
}
/**
* {@inheritdoc}
*/
public function getConfirmText() {
return $this->t('Remove');
}
/**
* {@inheritdoc}
*/
public function getQuestion() {
$entity = $this->getEntity();
$regions = $this->systemRegionList($entity->getTheme(), REGIONS_VISIBLE);
return $this->t('Are you sure you want to remove the @entity-type %label from the %region region?', [
'@entity-type' => $entity->getEntityType()->getSingularLabel(),
'%label' => $entity->label(),
'%region' => $regions[$entity->getRegion()],
]);
}
/**
* {@inheritdoc}
*/
public function getDescription() {
return $this->t('This will remove the block placement. You will need to <a href=":url">place it again</a> in order to undo this action.', [
':url' => Url::fromRoute('block.admin_display_theme', ['theme' => $this->getEntity()->getTheme()])->toString(),
]);
}
/**
* {@inheritdoc}
*/
protected function getDeletionMessage() {
$entity = $this->getEntity();
$regions = $this->systemRegionList($entity->getTheme(), REGIONS_VISIBLE);
return $this->t('The @entity-type %label has been removed from the %region region.', [
'@entity-type' => $entity->getEntityType()->getSingularLabel(),
'%label' => $entity->label(),
'%region' => $regions[$entity->getRegion()],
]);
}
/**
* Wraps system_region_list().
*/
protected function systemRegionList($theme, $show = REGIONS_ALL) {
return system_region_list($theme, $show);
}
}

View File

@ -0,0 +1,269 @@
<?php
namespace Drupal\block\Hook;
use Drupal\Component\Utility\Html;
use Drupal\Core\Block\BlockPluginInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\language\ConfigurableLanguageInterface;
use Drupal\system\Entity\Menu;
use Drupal\block\Entity\Block;
use Drupal\Core\Link;
use Drupal\Core\Url;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\Core\Hook\Attribute\Hook;
/**
* Hook implementations for block.
*/
class BlockHooks {
use StringTranslationTrait;
/**
* Implements hook_help().
*/
#[Hook('help')]
public function help($route_name, RouteMatchInterface $route_match): ?string {
switch ($route_name) {
case 'help.page.block':
$block_content = \Drupal::moduleHandler()->moduleExists('block_content') ? Url::fromRoute('help.page', ['name' => 'block_content'])->toString() : '#';
$output = '';
$output .= '<h2>' . $this->t('About') . '</h2>';
$output .= '<p>' . $this->t('The Block module allows you to place blocks in regions of your installed themes, and configure block settings. For more information, see the <a href=":blocks-documentation">online documentation for the Block module</a>.', [':blocks-documentation' => 'https://www.drupal.org/documentation/modules/block/']) . '</p>';
$output .= '<h2>' . $this->t('Uses') . '</h2>';
$output .= '<dl>';
$output .= '<dt>' . $this->t('Placing and moving blocks') . '</dt>';
$output .= '<dd>' . $this->t('You can place a new block in a region by selecting <em>Place block</em> on the <a href=":blocks">Block layout page</a>. Once a block is placed, it can be moved to a different region by drag-and-drop or by using the <em>Region</em> drop-down list, and then clicking <em>Save blocks</em>.', [':blocks' => Url::fromRoute('block.admin_display')->toString()]) . '</dd>';
$output .= '<dt>' . $this->t('Toggling between different themes') . '</dt>';
$output .= '<dd>' . $this->t('Blocks are placed and configured specifically for each theme. The Block layout page opens with the default theme, but you can toggle to other installed themes.') . '</dd>';
$output .= '<dt>' . $this->t('Demonstrating block regions for a theme') . '</dt>';
$output .= '<dd>' . $this->t('You can see where the regions are for the current theme by clicking the <em>Demonstrate block regions</em> link on the <a href=":blocks">Block layout page</a>. Regions are specific to each theme.', [':blocks' => Url::fromRoute('block.admin_display')->toString()]) . '</dd>';
$output .= '<dt>' . $this->t('Configuring block settings') . '</dt>';
$output .= '<dd>' . $this->t('To change the settings of an individual block click on the <em>Configure</em> link on the <a href=":blocks">Block layout page</a>. The available options vary depending on the module that provides the block. For all blocks you can change the block title and toggle whether to display it.', [':blocks' => Url::fromRoute('block.admin_display')->toString()]) . '</dd>';
$output .= '<dt>' . $this->t('Controlling visibility') . '</dt>';
$output .= '<dd>' . $this->t('You can control the visibility of a block by restricting it to specific pages, content types, and/or roles by setting the appropriate options under <em>Visibility settings</em> of the block configuration.') . '</dd>';
$output .= '<dt>' . $this->t('Adding content blocks') . '</dt>';
$output .= '<dd>' . $this->t('You can add content blocks, if the <em>Block Content</em> module is installed. For more information, see the <a href=":blockcontent-help">Block Content help page</a>.', [':blockcontent-help' => $block_content]) . '</dd>';
$output .= '</dl>';
return $output;
}
if ($route_name == 'block.admin_display' || $route_name == 'block.admin_display_theme') {
$demo_theme = $route_match->getParameter('theme') ?: \Drupal::config('system.theme')->get('default');
$themes = \Drupal::service('theme_handler')->listInfo();
$output = '<p>' . $this->t('Block placement is specific to each theme on your site. Changes will not be saved until you click <em>Save blocks</em> at the bottom of the page.') . '</p>';
$output .= '<p>' . Link::fromTextAndUrl($this->t('Demonstrate block regions (@theme)', ['@theme' => $themes[$demo_theme]->info['name']]), Url::fromRoute('block.admin_demo', ['theme' => $demo_theme]))->toString() . '</p>';
return $output;
}
return NULL;
}
/**
* Implements hook_theme().
*/
#[Hook('theme')]
public function theme() : array {
return [
'block' => [
'render element' => 'elements',
'initial preprocess' => static::class . ':preprocessBlock',
],
];
}
/**
* Prepares variables for block templates.
*
* Default template: block.html.twig.
*
* Prepares the values passed to the theme_block function to be passed
* into a pluggable template engine. Uses block properties to generate a
* series of template file suggestions. If none are found, the default
* block.html.twig is used.
*
* Most themes use their own copy of block.html.twig. The default is located
* inside "core/modules/block/templates/block.html.twig". Look in there for
* the full list of available variables.
*
* @param array $variables
* An associative array containing:
* - elements: An associative array containing the properties of the
* element. Properties used: #block, #configuration, #children,
* and #plugin_id.
*/
public function preprocessBlock(&$variables): void {
$variables['configuration'] = $variables['elements']['#configuration'];
$variables['plugin_id'] = $variables['elements']['#plugin_id'];
$variables['base_plugin_id'] = $variables['elements']['#base_plugin_id'];
$variables['derivative_plugin_id'] = $variables['elements']['#derivative_plugin_id'];
$variables['in_preview'] = $variables['elements']['#in_preview'] ?? FALSE;
$variables['label'] = !empty($variables['configuration']['label_display']) ? $variables['configuration']['label'] : '';
$variables['content'] = $variables['elements']['content'];
// A block's label is configuration: it is static. Allow dynamic labels to
// be set in the render array.
if (isset($variables['elements']['content']['#title']) && !empty($variables['configuration']['label_display'])) {
$variables['label'] = $variables['elements']['content']['#title'];
}
// Create a valid HTML ID and make sure it is unique.
if (!empty($variables['elements']['#id'])) {
$variables['attributes']['id'] = Html::getUniqueId('block-' . $variables['elements']['#id']);
}
// Proactively add aria-describedby if possible to improve accessibility.
if ($variables['label'] && isset($variables['attributes']['role'])) {
$variables['title_attributes']['id'] = Html::getUniqueId($variables['label']);
$variables['attributes']['aria-describedby'] = $variables['title_attributes']['id'];
}
}
/**
* Implements hook_page_top().
*/
#[Hook('page_top')]
public function pageTop(array &$page_top): void {
if (\Drupal::routeMatch()->getRouteName() === 'block.admin_demo') {
$theme = \Drupal::theme()->getActiveTheme()->getName();
$page_top['backlink'] = [
'#type' => 'link',
'#title' => $this->t('Exit block region demonstration'),
'#options' => [
'attributes' => [
'class' => [
'block-demo-backlink',
],
],
],
'#weight' => -10,
];
if (\Drupal::config('system.theme')->get('default') == $theme) {
$page_top['backlink']['#url'] = Url::fromRoute('block.admin_display');
}
else {
$page_top['backlink']['#url'] = Url::fromRoute('block.admin_display_theme', ['theme' => $theme]);
}
}
}
/**
* Implements hook_modules_installed().
*
* @see block_themes_installed()
*/
#[Hook('modules_installed')]
public function modulesInstalled($modules, bool $is_syncing): void {
// Do not create blocks during config sync.
if ($is_syncing) {
return;
}
// BlockHooks::themesInstalled() does not call block_theme_initialize()
// during site installation because block configuration can be optional or
// provided by the profile. Now, when the profile is installed, this
// configuration exists, call block_theme_initialize() for all installed
// themes.
$profile = \Drupal::installProfile();
if (in_array($profile, $modules, TRUE)) {
foreach (\Drupal::service('theme_handler')->listInfo() as $theme => $data) {
block_theme_initialize($theme);
}
}
}
/**
* Implements hook_rebuild().
*/
#[Hook('rebuild')]
public function rebuild(): void {
foreach (\Drupal::service('theme_handler')->listInfo() as $theme => $data) {
if ($data->status) {
$regions = system_region_list($theme);
/** @var \Drupal\block\BlockInterface[] $blocks */
$blocks = \Drupal::entityTypeManager()->getStorage('block')->loadByProperties(['theme' => $theme]);
foreach ($blocks as $block_id => $block) {
// Disable blocks in invalid regions.
if (!isset($regions[$block->getRegion()])) {
if ($block->status()) {
\Drupal::messenger()->addWarning($this->t('The block %info was assigned to the invalid region %region and has been disabled.', ['%info' => $block_id, '%region' => $block->getRegion()]));
}
$block->setRegion(system_default_region($theme))->disable()->save();
}
}
}
}
}
/**
* Implements hook_ENTITY_TYPE_delete() for user_role entities.
*
* Removes deleted role from blocks that use it.
*/
#[Hook('user_role_delete')]
public function userRoleDelete($role): void {
foreach (Block::loadMultiple() as $block) {
/** @var \Drupal\block\BlockInterface $block */
$visibility = $block->getVisibility();
if (isset($visibility['user_role']['roles'][$role->id()])) {
unset($visibility['user_role']['roles'][$role->id()]);
$block->setVisibilityConfig('user_role', $visibility['user_role']);
$block->save();
}
}
}
/**
* Implements hook_ENTITY_TYPE_delete() for menu entities.
*/
#[Hook('menu_delete')]
public function menuDelete(Menu $menu): void {
if (!$menu->isSyncing()) {
foreach (Block::loadMultiple() as $block) {
if ($block->getPluginId() == 'system_menu_block:' . $menu->id()) {
$block->delete();
}
}
}
}
/**
* Implements hook_ENTITY_TYPE_delete() for 'configurable_language'.
*
* Delete the potential block visibility settings of the deleted language.
*/
#[Hook('configurable_language_delete')]
public function configurableLanguageDelete(ConfigurableLanguageInterface $language): void {
// Remove the block visibility settings for the deleted language.
foreach (Block::loadMultiple() as $block) {
/** @var \Drupal\block\BlockInterface $block */
$visibility = $block->getVisibility();
if (isset($visibility['language']['langcodes'][$language->id()])) {
unset($visibility['language']['langcodes'][$language->id()]);
$block->setVisibilityConfig('language', $visibility['language']);
$block->save();
}
}
}
/**
* Implements hook_block_build_BASE_BLOCK_ID_alter().
*/
#[Hook('block_build_local_actions_block_alter')]
public function blockBuildLocalActionsBlockAlter(array &$build, BlockPluginInterface $block): void {
$build['#lazy_builder_preview'] = [
'#type' => 'container',
'#attributes' => [
'class' => [
'invisible',
],
],
'actions' => [
'#theme' => 'menu_local_action',
'#link' => [
'title' => $this->t('Add'),
'url' => Url::fromUserInput('#'),
],
],
];
}
}

View File

@ -0,0 +1,97 @@
<?php
declare(strict_types=1);
namespace Drupal\block\Plugin\ConfigAction;
use Drupal\block\BlockInterface;
use Drupal\Core\Config\Action\Attribute\ConfigAction;
use Drupal\Core\Config\Action\ConfigActionException;
use Drupal\Core\Config\Action\ConfigActionPluginInterface;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Config\Entity\ConfigEntityStorageInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Places a block in either the admin or default theme.
*
* @internal
* This API is experimental.
*/
#[ConfigAction(
id: 'placeBlock',
admin_label: new TranslatableMarkup('Place a block'),
entity_types: ['block'],
deriver: PlaceBlockDeriver::class,
)]
final class PlaceBlock implements ConfigActionPluginInterface, ContainerFactoryPluginInterface {
public function __construct(
private readonly ConfigActionPluginInterface $createAction,
private readonly string $whichTheme,
private readonly ConfigFactoryInterface $configFactory,
private readonly ConfigEntityStorageInterface $blockStorage,
) {}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
return new static(
$container->get('plugin.manager.config_action')->createInstance('entity_create:createIfNotExists'),
$plugin_definition['which_theme'],
$container->get(ConfigFactoryInterface::class),
$container->get(EntityTypeManagerInterface::class)->getStorage('block'),
);
}
/**
* {@inheritdoc}
*/
public function apply(string $configName, mixed $value): void {
assert(is_array($value));
$theme = $this->configFactory->get('system.theme')->get($this->whichTheme);
$value['theme'] = $theme;
if (array_key_exists('region', $value) && is_array($value['region'])) {
// Since the recipe author might not know ahead of time what theme the
// block is in, they should supply a map whose keys are theme names and
// values are region names, so we know where to place this block. If the
// target theme is not in the map, they should supply the name of a
// fallback region. If all that fails, give up with an exception.
$value['region'] = $value['region'][$theme] ?? $value['default_region'] ?? throw new ConfigActionException("Cannot determine which region to place this block into, because no default region was provided.");
}
// Allow the recipe author to position the block in the region without
// needing to know exact weights.
if (array_key_exists('position', $value)) {
$blocks = $this->blockStorage->loadByProperties([
'theme' => $theme,
'region' => $value['region'],
]);
if ($blocks) {
// Sort the blocks by weight. Don't use
// \Drupal\block\Entity\Block::sort() here because it seems to be
// intended to sort blocks in the UI, where we really just want to get
// the weights right in this situation.
uasort($blocks, fn (BlockInterface $a, BlockInterface $b) => $a->getWeight() <=> $b->getWeight());
$value['weight'] = match ($value['position']) {
'first' => reset($blocks)->getWeight() - 1,
'last' => end($blocks)->getWeight() + 1,
};
}
}
// Remove values that are not valid properties of block entities.
unset($value['position'], $value['default_region']);
// Ensure a weight is set by default.
$value += ['weight' => 0];
$this->createAction->apply($configName, $value);
}
}

View File

@ -0,0 +1,32 @@
<?php
declare(strict_types=1);
namespace Drupal\block\Plugin\ConfigAction;
use Drupal\Component\Plugin\Derivative\DeriverBase;
/**
* Defines a deriver for the `placeBlock` config action.
*
* This creates two actions: `placeBlockInDefaultTheme`, and
* `placeBlockInAdminTheme`. They behave identically except for which theme
* they target.
*/
final class PlaceBlockDeriver extends DeriverBase {
/**
* {@inheritdoc}
*/
public function getDerivativeDefinitions($base_plugin_definition) {
$this->derivatives['placeBlockInAdminTheme'] = [
'which_theme' => 'admin',
] + $base_plugin_definition;
$this->derivatives['placeBlockInDefaultTheme'] = [
'which_theme' => 'default',
] + $base_plugin_definition;
return parent::getDerivativeDefinitions($base_plugin_definition);
}
}

View File

@ -0,0 +1,66 @@
<?php
namespace Drupal\block\Plugin\Derivative;
use Drupal\Component\Plugin\Derivative\DeriverBase;
use Drupal\Core\Extension\ThemeHandlerInterface;
use Drupal\Core\Plugin\Discovery\ContainerDeriverInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Provides dynamic tabs based on active themes.
*/
class ThemeLocalTask extends DeriverBase implements ContainerDeriverInterface {
/**
* The theme handler.
*
* @var \Drupal\Core\Extension\ThemeHandlerInterface
*/
protected $themeHandler;
/**
* Constructs a new ThemeLocalTask.
*
* @param \Drupal\Core\Extension\ThemeHandlerInterface $theme_handler
* The theme handler.
*/
public function __construct(ThemeHandlerInterface $theme_handler) {
$this->themeHandler = $theme_handler;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, $base_plugin_id) {
return new static(
$container->get('theme_handler')
);
}
/**
* {@inheritdoc}
*/
public function getDerivativeDefinitions($base_plugin_definition) {
$default_theme = $this->themeHandler->getDefault();
foreach ($this->themeHandler->listInfo() as $theme_name => $theme) {
if ($this->themeHandler->hasUi($theme_name)) {
$this->derivatives[$theme_name] = $base_plugin_definition;
$this->derivatives[$theme_name]['title'] = $theme->info['name'];
$this->derivatives[$theme_name]['route_parameters'] = ['theme' => $theme_name];
}
// Default task!
if ($default_theme == $theme_name) {
$this->derivatives[$theme_name]['route_name'] = $base_plugin_definition['parent_id'];
// Emulate default logic because without the base plugin id we can't
// change the base_route.
$this->derivatives[$theme_name]['weight'] = -10;
unset($this->derivatives[$theme_name]['route_parameters']);
}
}
return $this->derivatives;
}
}

View File

@ -0,0 +1,205 @@
<?php
namespace Drupal\block\Plugin\DisplayVariant;
use Drupal\block\BlockRepositoryInterface;
use Drupal\Core\Block\MainContentBlockPluginInterface;
use Drupal\Core\Block\TitleBlockPluginInterface;
use Drupal\Core\Block\MessagesBlockPluginInterface;
use Drupal\Core\Cache\CacheableMetadata;
use Drupal\Core\Display\Attribute\PageDisplayVariant;
use Drupal\Core\Display\PageVariantInterface;
use Drupal\Core\Entity\EntityViewBuilderInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\Core\Display\VariantBase;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Provides a page display variant that decorates the main content with blocks.
*
* To ensure essential information is displayed, each essential part of a page
* has a corresponding block plugin interface, so that BlockPageVariant can
* automatically provide a fallback in case no block for each of these
* interfaces is placed.
*
* @see \Drupal\Core\Block\MainContentBlockPluginInterface
* @see \Drupal\Core\Block\MessagesBlockPluginInterface
*/
#[PageDisplayVariant(
id: 'block_page',
admin_label: new TranslatableMarkup('Page with blocks')
)]
class BlockPageVariant extends VariantBase implements PageVariantInterface, ContainerFactoryPluginInterface {
/**
* The block repository.
*
* @var \Drupal\block\BlockRepositoryInterface
*/
protected $blockRepository;
/**
* The block view builder.
*
* @var \Drupal\Core\Entity\EntityViewBuilderInterface
*/
protected $blockViewBuilder;
/**
* The Block entity type list cache tags.
*
* @var string[]
*/
protected $blockListCacheTags;
/**
* The render array representing the main page content.
*
* @var array
*/
protected $mainContent = [];
/**
* The page title: a string (plain title) or a render array (formatted title).
*
* @var string|array
*/
protected $title = '';
/**
* Constructs a new BlockPageVariant.
*
* @param array $configuration
* A configuration array containing information about the plugin instance.
* @param string $plugin_id
* The plugin ID for the plugin instance.
* @param mixed $plugin_definition
* The plugin implementation definition.
* @param \Drupal\block\BlockRepositoryInterface $block_repository
* The block repository.
* @param \Drupal\Core\Entity\EntityViewBuilderInterface $block_view_builder
* The block view builder.
* @param string[] $block_list_cache_tags
* The Block entity type list cache tags.
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, BlockRepositoryInterface $block_repository, EntityViewBuilderInterface $block_view_builder, array $block_list_cache_tags) {
parent::__construct($configuration, $plugin_id, $plugin_definition);
$this->blockRepository = $block_repository;
$this->blockViewBuilder = $block_view_builder;
$this->blockListCacheTags = $block_list_cache_tags;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
return new static(
$configuration,
$plugin_id,
$plugin_definition,
$container->get('block.repository'),
$container->get('entity_type.manager')->getViewBuilder('block'),
$container->get('entity_type.manager')->getDefinition('block')->getListCacheTags()
);
}
/**
* {@inheritdoc}
*/
public function setMainContent(array $main_content) {
$this->mainContent = $main_content;
return $this;
}
/**
* {@inheritdoc}
*/
public function setTitle($title) {
$this->title = $title;
return $this;
}
/**
* {@inheritdoc}
*/
public function build() {
// Track whether blocks showing the main content and messages are displayed.
$main_content_block_displayed = FALSE;
$messages_block_displayed = FALSE;
$build = [
'#cache' => [
'tags' => $this->blockListCacheTags,
],
];
// Load all region content assigned via blocks.
$cacheable_metadata_list = [];
foreach ($this->blockRepository->getVisibleBlocksPerRegion($cacheable_metadata_list) as $region => $blocks) {
/** @var \Drupal\block\BlockInterface[] $blocks */
foreach ($blocks as $key => $block) {
$block_plugin = $block->getPlugin();
if ($block_plugin instanceof MainContentBlockPluginInterface) {
$block_plugin->setMainContent($this->mainContent);
$main_content_block_displayed = TRUE;
}
elseif ($block_plugin instanceof TitleBlockPluginInterface) {
$block_plugin->setTitle($this->title);
}
elseif ($block_plugin instanceof MessagesBlockPluginInterface) {
$messages_block_displayed = TRUE;
}
$build[$region][$key] = $this->blockViewBuilder->view($block);
// The main content block cannot be cached: it is a placeholder for the
// render array returned by the controller. It should be rendered as-is,
// with other placed blocks "decorating" it. Analogous reasoning for the
// title block.
if ($block_plugin instanceof MainContentBlockPluginInterface || $block_plugin instanceof TitleBlockPluginInterface) {
unset($build[$region][$key]['#cache']['keys']);
}
}
if (!empty($build[$region])) {
// \Drupal\block\BlockRepositoryInterface::getVisibleBlocksPerRegion()
// returns the blocks in sorted order.
$build[$region]['#sorted'] = TRUE;
}
}
// If no block that shows the main content is displayed, still show the main
// content. Otherwise the end user will see all displayed blocks, but not
// the main content they came for.
if (!$main_content_block_displayed) {
$build['content']['system_main'] = $this->mainContent;
}
// If no block displays status messages, still render them.
if (!$messages_block_displayed) {
$build['content']['messages'] = [
'#weight' => -1000,
'#type' => 'status_messages',
'#include_fallback' => TRUE,
];
}
// If any render arrays are manually placed, render arrays and blocks must
// be sorted.
if (!$main_content_block_displayed || !$messages_block_displayed) {
unset($build['content']['#sorted']);
}
// The access results' cacheability is currently added to the top level of
// the render array. This is done to prevent issues with empty regions being
// displayed.
// This would need to be changed to allow caching of block regions, as each
// region must then have the relevant cacheable metadata.
$merged_cacheable_metadata = CacheableMetadata::createFromRenderArray($build);
foreach ($cacheable_metadata_list as $cacheable_metadata) {
$merged_cacheable_metadata = $merged_cacheable_metadata->merge($cacheable_metadata);
}
$merged_cacheable_metadata->applyTo($build);
return $build;
}
}

View File

@ -0,0 +1,43 @@
<?php
namespace Drupal\block\Plugin\migrate\destination;
use Drupal\Core\Config\Schema\SchemaIncompleteException;
use Drupal\migrate\Attribute\MigrateDestination;
use Drupal\migrate\MigrateException;
use Drupal\migrate\Plugin\migrate\destination\EntityConfigBase;
use Drupal\migrate\Row;
/**
* Migrate destination for block entity.
*/
#[MigrateDestination('entity:block')]
class EntityBlock extends EntityConfigBase {
/**
* {@inheritdoc}
*/
protected function getEntityId(Row $row) {
// Try to find the block by its plugin ID and theme.
$properties = [
'plugin' => $row->getDestinationProperty('plugin'),
'theme' => $row->getDestinationProperty('theme'),
];
$blocks = array_keys($this->storage->loadByProperties($properties));
return reset($blocks);
}
/**
* {@inheritdoc}
*/
public function import(Row $row, array $old_destination_id_values = []) {
try {
$entity_ids = parent::import($row, $old_destination_id_values);
}
catch (SchemaIncompleteException $e) {
throw new MigrateException($e->getMessage());
}
return $entity_ids;
}
}

View File

@ -0,0 +1,115 @@
<?php
namespace Drupal\block\Plugin\migrate\process;
use Drupal\Core\Entity\EntityStorageInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\migrate\Attribute\MigrateProcess;
use Drupal\migrate\MigrateLookupInterface;
use Drupal\migrate\Plugin\MigrationInterface;
use Drupal\migrate\MigrateExecutableInterface;
use Drupal\migrate\ProcessPluginBase;
use Drupal\migrate\Row;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Determines the block plugin ID.
*/
#[MigrateProcess('block_plugin_id')]
class BlockPluginId extends ProcessPluginBase implements ContainerFactoryPluginInterface {
/**
* The migrate lookup service.
*
* @var \Drupal\migrate\MigrateLookupInterface
*/
protected $migrateLookup;
/**
* The block_content entity storage handler.
*
* @var \Drupal\Core\Entity\EntityStorageInterface|null
*/
protected $blockContentStorage;
/**
* Constructs a BlockPluginId object.
*
* @param array $configuration
* The plugin configuration.
* @param string $plugin_id
* The plugin ID.
* @param array $plugin_definition
* The plugin definition.
* @param \Drupal\Core\Entity\EntityStorageInterface|null $storage
* The block content storage object. NULL if the block_content module is
* not installed.
* @param \Drupal\migrate\MigrateLookupInterface $migrate_lookup
* The migrate lookup service.
*/
public function __construct(array $configuration, $plugin_id, array $plugin_definition, ?EntityStorageInterface $storage, MigrateLookupInterface $migrate_lookup) {
parent::__construct($configuration, $plugin_id, $plugin_definition);
$this->blockContentStorage = $storage;
$this->migrateLookup = $migrate_lookup;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition, ?MigrationInterface $migration = NULL) {
$entity_type_manager = $container->get('entity_type.manager');
return new static(
$configuration,
$plugin_id,
$plugin_definition,
$entity_type_manager->hasDefinition('block_content') ? $entity_type_manager->getStorage('block_content') : NULL,
$container->get('migrate.lookup')
);
}
/**
* {@inheritdoc}
*
* Set the block plugin id.
*/
public function transform($value, MigrateExecutableInterface $migrate_executable, Row $row, $destination_property) {
if (is_array($value)) {
[$module, $delta] = $value;
switch ($module) {
case 'aggregator':
[$type] = explode('-', $delta);
if ($type == 'feed') {
return 'aggregator_feed_block';
}
break;
case 'menu':
return "system_menu_block:$delta";
case 'block':
if ($this->blockContentStorage) {
$block_id = $row->getDestinationProperty('_block_module_plugin_id');
// Legacy generated migrations will not have the destination
// property '_block_module_plugin_id'.
if (!$block_id) {
$lookup_result = $this->migrateLookup->lookup(['d6_custom_block', 'd7_custom_block'], [$delta]);
if ($lookup_result) {
$block_id = $lookup_result[0]['id'];
}
}
if ($block_id) {
return 'block_content:' . $this->blockContentStorage->load($block_id)->uuid();
}
}
break;
default:
break;
}
}
else {
return $value;
}
}
}

View File

@ -0,0 +1,73 @@
<?php
namespace Drupal\block\Plugin\migrate\process;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\migrate\Attribute\MigrateProcess;
use Drupal\migrate\MigrateExecutableInterface;
use Drupal\migrate\Plugin\migrate\process\StaticMap;
use Drupal\migrate\Row;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Determines the region for a block.
*/
#[MigrateProcess('block_region')]
class BlockRegion extends StaticMap implements ContainerFactoryPluginInterface {
/**
* List of regions, keyed by theme.
*
* @var array[]
*/
protected $regions;
/**
* Constructs a BlockRegion plugin instance.
*
* @param array $configuration
* The plugin configuration.
* @param string $plugin_id
* The plugin ID.
* @param mixed $plugin_definition
* The plugin definition.
* @param array $regions
* Array of region maps, keyed by theme.
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, array $regions) {
parent::__construct($configuration, $plugin_id, $plugin_definition);
$this->regions = $regions;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
$regions = [];
foreach ($container->get('theme_handler')->listInfo() as $key => $theme) {
$regions[$key] = $theme->info['regions'];
}
return new static($configuration, $plugin_id, $plugin_definition, $regions);
}
/**
* {@inheritdoc}
*/
public function transform($value, MigrateExecutableInterface $migrate_executable, Row $row, $destination_property) {
// Set the destination region, based on the source region and theme as well
// as the current destination default theme.
[$source_theme, $destination_theme, $region] = $value;
// Theme is the same on both source and destination, so ensure that the
// region exists in the destination theme.
if (strtolower($source_theme) == strtolower($destination_theme)) {
if (isset($this->regions[$destination_theme][$region])) {
return $region;
}
}
// Fall back to static mapping.
return parent::transform($value, $migrate_executable, $row, $destination_property);
}
}

View File

@ -0,0 +1,104 @@
<?php
namespace Drupal\block\Plugin\migrate\process;
use Drupal\Core\Block\BlockManagerInterface;
use Drupal\Core\Block\BlockPluginInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\migrate\Attribute\MigrateProcess;
use Drupal\migrate\MigrateExecutableInterface;
use Drupal\migrate\ProcessPluginBase;
use Drupal\migrate\Row;
use Symfony\Component\DependencyInjection\ContainerInterface;
// cspell:ignore whois
/**
* Determines the block settings.
*/
#[MigrateProcess('block_settings')]
class BlockSettings extends ProcessPluginBase implements ContainerFactoryPluginInterface {
/**
* The block manager service.
*
* @var \Drupal\Core\Block\BlockManagerInterface
*/
protected readonly BlockManagerInterface $blockManager;
public function __construct(array $configuration, $plugin_id, $plugin_definition, ?BlockManagerInterface $blockManager = NULL) {
if (empty($blockManager)) {
@trigger_error('Calling ' . __METHOD__ . '() without the $blockManager parameter is deprecated in drupal:11.2.0 and must be provided in drupal:12.0.0. See https://www.drupal.org/node/3522023', E_USER_DEPRECATED);
$blockManager = \Drupal::service(BlockManagerInterface::class);
}
$this->blockManager = $blockManager;
parent::__construct($configuration, $plugin_id, $plugin_definition);
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition): static {
return new static(
$configuration,
$plugin_id,
$plugin_definition,
$container->get(BlockManagerInterface::class),
);
}
/**
* {@inheritdoc}
*
* Set the block configuration.
*/
public function transform($value, MigrateExecutableInterface $migrate_executable, Row $row, $destination_property) {
[$plugin, $delta, $old_settings, $title] = $value;
$settings = [];
$settings['label'] = $title;
if ($title && $title !== '<none>') {
$settings['label_display'] = BlockPluginInterface::BLOCK_LABEL_VISIBLE;
}
else {
$settings['label_display'] = '0';
}
switch ($plugin) {
case 'aggregator_feed_block':
[, $id] = explode('-', $delta);
$settings['block_count'] = $old_settings['aggregator']['item_count'];
$settings['feed'] = $id;
break;
case 'book_navigation':
$settings['block_mode'] = $old_settings['book']['block_mode'];
break;
case 'forum_active_block':
case 'forum_new_block':
$settings['block_count'] = $old_settings['forum']['block_num'];
break;
case 'statistics_popular_block':
$settings['top_day_num'] = $old_settings['statistics']['statistics_block_top_day_num'];
$settings['top_all_num'] = $old_settings['statistics']['statistics_block_top_all_num'];
$settings['top_last_num'] = $old_settings['statistics']['statistics_block_top_last_num'];
break;
case 'views_block:who_s_new-block_1':
$settings['items_per_page'] = $old_settings['user']['block_whois_new_count'];
break;
case 'views_block:who_s_online-who_s_online_block':
$settings['items_per_page'] = $old_settings['user']['max_list_count'];
break;
}
// Let the block plugin fill in any missing settings.
$settings = $this->blockManager->createInstance($plugin, $settings)
->getConfiguration();
return $settings;
}
}

View File

@ -0,0 +1,91 @@
<?php
namespace Drupal\block\Plugin\migrate\process;
use Drupal\Core\Config\Config;
use Drupal\migrate\Attribute\MigrateProcess;
use Drupal\migrate\MigrateExecutableInterface;
use Drupal\migrate\ProcessPluginBase;
use Drupal\migrate\Row;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Determines the theme to use for a block.
*/
#[MigrateProcess('block_theme')]
class BlockTheme extends ProcessPluginBase implements ContainerFactoryPluginInterface {
/**
* Contains the system.theme configuration object.
*/
protected Config $themeConfig;
/**
* List of themes available on the destination.
*
* @var string[]
*/
protected array $themes;
/**
* Constructs a BlockTheme object.
*
* @param array $configuration
* A configuration array containing information about the plugin instance.
* @param string $plugin_id
* The plugin ID for the plugin instance.
* @param mixed $plugin_definition
* The plugin implementation definition.
* @param \Drupal\Core\Config\Config $theme_config
* The system.theme configuration factory object.
* @param array $themes
* The list of themes available on the destination.
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, Config $theme_config, array $themes) {
parent::__construct($configuration, $plugin_id, $plugin_definition);
$this->themeConfig = $theme_config;
$this->themes = $themes;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
return new static(
$configuration,
$plugin_id,
$plugin_definition,
$container->get('config.factory')->get('system.theme'),
$container->get('theme_handler')->listInfo()
);
}
/**
* {@inheritdoc}
*/
public function transform($value, MigrateExecutableInterface $migrate_executable, Row $row, $destination_property) {
[$theme, $default_theme, $admin_theme] = $value;
// If the source theme exists on the destination, we're good.
if (isset($this->themes[$theme])) {
return $theme;
}
// If the source block is assigned to a region in the source default theme,
// then assign it to the destination default theme.
if (strtolower($theme) == strtolower($default_theme)) {
return $this->themeConfig->get('default');
}
// If the source block is assigned to a region in the source admin theme,
// then assign it to the destination admin theme.
if ($admin_theme && strtolower($theme) == strtolower($admin_theme)) {
return $this->themeConfig->get('admin');
}
// We couldn't map it to a D8 theme so just return the incoming theme.
return $theme;
}
}

View File

@ -0,0 +1,154 @@
<?php
namespace Drupal\block\Plugin\migrate\process;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\migrate\Attribute\MigrateProcess;
use Drupal\migrate\MigrateLookupInterface;
use Drupal\migrate\Plugin\MigrationInterface;
use Drupal\migrate\MigrateExecutableInterface;
use Drupal\migrate\MigrateSkipRowException;
use Drupal\migrate\ProcessPluginBase;
use Drupal\migrate\Row;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Determines the visibility for a block.
*/
#[MigrateProcess('block_visibility')]
class BlockVisibility extends ProcessPluginBase implements ContainerFactoryPluginInterface {
/**
* The module handler.
*
* @var \Drupal\Core\Extension\ModuleHandlerInterface
*/
protected $moduleHandler;
/**
* The migrate lookup service.
*
* @var \Drupal\migrate\MigrateLookupInterface
*/
protected $migrateLookup;
/**
* Whether or not to skip blocks that use PHP for visibility.
*
* Only applies if the PHP module is not enabled.
*
* @var bool
*/
protected $skipPHP = FALSE;
/**
* Constructs a BlockVisibility object.
*
* @param array $configuration
* The plugin configuration.
* @param string $plugin_id
* The plugin ID.
* @param mixed $plugin_definition
* The plugin definition.
* @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
* The module handler service.
* @param \Drupal\migrate\MigrateLookupInterface $migrate_lookup
* The migrate lookup service.
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, ModuleHandlerInterface $module_handler, MigrateLookupInterface $migrate_lookup) {
parent::__construct($configuration, $plugin_id, $plugin_definition);
$this->moduleHandler = $module_handler;
$this->migrateLookup = $migrate_lookup;
if (isset($configuration['skip_php'])) {
$this->skipPHP = $configuration['skip_php'];
}
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition, ?MigrationInterface $migration = NULL) {
return new static(
$configuration,
$plugin_id,
$plugin_definition,
$container->get('module_handler'),
$container->get('migrate.lookup')
);
}
/**
* {@inheritdoc}
*/
public function transform($value, MigrateExecutableInterface $migrate_executable, Row $row, $destination_property) {
[$old_visibility, $pages, $roles] = $value;
$visibility = [];
// If the block is assigned to specific roles, add the user_role condition.
if ($roles) {
$visibility['user_role'] = [
'id' => 'user_role',
'roles' => [],
'context_mapping' => [
'user' => '@user.current_user_context:current_user',
],
'negate' => FALSE,
];
// Legacy generated migrations will not have the destination property
// '_role_ids'.
$role_ids = $row->getDestinationProperty('_role_ids');
foreach ($roles as $key => $role_id) {
if (!$role_ids) {
$lookup = $this->migrateLookup->lookup(['d6_user_role', 'd7_user_role'], [$role_id]);
$lookup_result = $lookup[0]['id'];
}
else {
$lookup_result = $role_ids[$role_id] ?? NULL;
}
if ($lookup_result) {
$roles[$key] = $lookup_result;
}
}
$visibility['user_role']['roles'] = array_combine($roles, $roles);
}
if ($pages) {
// 2 == BLOCK_VISIBILITY_PHP in Drupal 6 and 7.
if ($old_visibility == 2) {
// If the PHP module is present, migrate the visibility code unaltered.
if ($this->moduleHandler->moduleExists('php')) {
$visibility['php'] = [
'id' => 'php',
// PHP code visibility could not be negated in Drupal 6 or 7.
'negate' => FALSE,
'php' => $pages,
];
}
// Skip the row if we're configured to. If not, we don't need to do
// anything else -- the block will simply have no PHP or request_path
// visibility configuration.
elseif ($this->skipPHP) {
throw new MigrateSkipRowException(sprintf("The block with bid '%d' from module '%s' will have no PHP or request_path visibility configuration.", $row->getSourceProperty('bid'), $row->getSourceProperty('module')));
}
}
else {
$paths = preg_split("(\r\n?|\n)", $pages);
foreach ($paths as $key => $path) {
$paths[$key] = $path === '<front>' ? $path : '/' . ltrim($path, '/');
}
$visibility['request_path'] = [
'id' => 'request_path',
'negate' => !$old_visibility,
'pages' => implode("\n", $paths),
];
}
}
return $visibility;
}
}

View File

@ -0,0 +1,103 @@
<?php
namespace Drupal\block\Plugin\migrate\process;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\migrate\Attribute\MigrateProcess;
use Drupal\migrate\MigrateLookupInterface;
use Drupal\migrate\Plugin\MigrationInterface;
use Drupal\migrate\MigrateExecutableInterface;
use Drupal\migrate\ProcessPluginBase;
use Drupal\migrate\Row;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Gets the destination roles ID for an array of source roles IDs.
*
* The roles_lookup plugin is used to get the destination roles for roles that
* are assigned to a block. It always uses the 'roles' value on the row as the
* source value.
*
* Examples
*
* @code
* process:
* roles:
* plugin: roles_lookup
* migration: d7_user_role
* @endcode
*
* This will get the destination role ID for each role in the 'roles' value on
* the source row.
*
* @see \Drupal\migrate\Plugin\MigrateProcessInterface
*/
#[MigrateProcess('roles_lookup')]
class RolesLookup extends ProcessPluginBase implements ContainerFactoryPluginInterface {
/**
* The migrate lookup service.
*
* @var \Drupal\migrate\MigrateLookupInterface
*/
protected $migrateLookup;
/**
* The migration for user role lookup.
*
* @var string
*/
protected $migration;
/**
* Constructs a BlockVisibility object.
*
* @param array $configuration
* The plugin configuration.
* @param string $plugin_id
* The plugin ID.
* @param mixed $plugin_definition
* The plugin definition.
* @param \Drupal\migrate\MigrateLookupInterface $migrate_lookup
* The migrate lookup service.
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, MigrateLookupInterface $migrate_lookup) {
parent::__construct($configuration, $plugin_id, $plugin_definition);
$this->migrateLookup = $migrate_lookup;
if (isset($configuration['migration'])) {
$this->migration = $configuration['migration'];
}
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition, ?MigrationInterface $migration = NULL) {
return new static(
$configuration,
$plugin_id,
$plugin_definition,
$container->get('migrate.lookup')
);
}
/**
* {@inheritdoc}
*/
public function transform($value, MigrateExecutableInterface $migrate_executable, Row $row, $destination_property) {
$roles = $row->get('roles');
$roles_result = [];
// If the block is assigned to specific roles, add the user_role condition.
if ($roles) {
foreach ($roles as $role_id) {
$lookup_result = $this->migrateLookup->lookup([$this->migration], [$role_id]);
if ($lookup_result) {
$roles_result[$role_id] = $lookup_result[0]['id'];
}
}
}
return $roles_result;
}
}

View File

@ -0,0 +1,189 @@
<?php
namespace Drupal\block\Plugin\migrate\source;
use Drupal\migrate\Row;
use Drupal\migrate_drupal\Plugin\migrate\source\DrupalSqlBase;
// cspell:ignore whois
/**
* Drupal 6/7 block source from database.
*
* For available configuration keys, refer to the parent classes.
*
* @see \Drupal\migrate\Plugin\migrate\source\SqlBase
* @see \Drupal\migrate\Plugin\migrate\source\SourcePluginBase
*
* @MigrateSource(
* id = "block",
* source_module = "block"
* )
*/
class Block extends DrupalSqlBase {
/**
* The default theme name.
*
* @var string
*/
protected $defaultTheme;
/**
* The admin theme name.
*
* @var string
*/
protected $adminTheme;
/**
* Table containing block configuration.
*
* @var string
*/
protected $blockTable;
/**
* Table mapping blocks to user roles.
*
* @var string
*/
protected $blockRoleTable;
/**
* Table listing user roles.
*
* @var string
*/
protected $userRoleTable;
/**
* {@inheritdoc}
*/
public function query() {
if ($this->getModuleSchemaVersion('system') >= 7000) {
$this->blockTable = 'block';
$this->blockRoleTable = 'block_role';
}
else {
$this->blockTable = 'blocks';
$this->blockRoleTable = 'blocks_roles';
}
// Drupal 6 & 7 both use the same name for the user roles table.
$this->userRoleTable = 'role';
return $this->select($this->blockTable, 'b')->fields('b');
}
/**
* {@inheritdoc}
*/
protected function initializeIterator() {
$this->defaultTheme = $this->variableGet('theme_default', 'Garland');
$this->adminTheme = $this->variableGet('admin_theme', NULL);
return parent::initializeIterator();
}
/**
* {@inheritdoc}
*/
public function fields() {
return [
'bid' => $this->t('The block numeric identifier.'),
'module' => $this->t('The module providing the block.'),
'delta' => $this->t("The block's delta."),
'theme' => $this->t('Which theme the block is placed in.'),
'status' => $this->t('Whether or not the block is enabled.'),
'weight' => $this->t('Weight of the block for ordering within regions.'),
'region' => $this->t('Region the block is placed in.'),
'visibility' => $this->t('Visibility expression.'),
'pages' => $this->t('Pages list.'),
'title' => $this->t('Block title.'),
'cache' => $this->t('Cache rule.'),
];
}
/**
* {@inheritdoc}
*/
public function getIds() {
$ids['module']['type'] = 'string';
$ids['delta']['type'] = 'string';
$ids['theme']['type'] = 'string';
return $ids;
}
/**
* {@inheritdoc}
*/
public function prepareRow(Row $row) {
$row->setSourceProperty('default_theme', $this->defaultTheme);
$row->setSourceProperty('admin_theme', $this->adminTheme);
$module = $row->getSourceProperty('module');
$delta = $row->getSourceProperty('delta');
$query = $this->select($this->blockRoleTable, 'br')
->fields('br', ['rid'])
->condition('module', $module)
->condition('delta', $delta);
$query->join($this->userRoleTable, 'ur', '[br].[rid] = [ur].[rid]');
$roles = $query->execute()
->fetchCol();
$row->setSourceProperty('roles', $roles);
$settings = [];
switch ($module) {
case 'aggregator':
[$type, $id] = explode('-', $delta);
if ($type == 'feed') {
$item_count = $this->select('aggregator_feed', 'af')
->fields('af', ['block'])
->condition('fid', $id)
->execute()
->fetchField();
}
else {
$item_count = $this->select('aggregator_category', 'ac')
->fields('ac', ['block'])
->condition('cid', $id)
->execute()
->fetchField();
}
$settings['aggregator']['item_count'] = $item_count;
break;
case 'book':
$settings['book']['block_mode'] = $this->variableGet('book_block_mode', 'all pages');
break;
case 'forum':
$settings['forum']['block_num'] = $this->variableGet('forum_block_num_' . $delta, 5);
break;
case 'statistics':
foreach (['statistics_block_top_day_num', 'statistics_block_top_all_num', 'statistics_block_top_last_num'] as $name) {
$settings['statistics'][$name] = $this->variableGet($name, 0);
}
break;
case 'user':
switch ($delta) {
case 2:
case 'new':
$settings['user']['block_whois_new_count'] = $this->variableGet('user_block_whois_new_count', 5);
break;
case 3:
case 'online':
$settings['user']['block_seconds_online'] = $this->variableGet('user_block_seconds_online', 900);
$settings['user']['max_list_count'] = $this->variableGet('user_block_max_list_count', 10);
break;
}
break;
}
$row->setSourceProperty('settings', $settings);
return parent::prepareRow($row);
}
}

View File

@ -0,0 +1,76 @@
<?php
namespace Drupal\block\Plugin\migrate\source\d6;
use Drupal\block\Plugin\migrate\source\Block;
use Drupal\migrate\Plugin\migrate\source\SourcePluginBase;
use Drupal\migrate\Row;
/**
* Drupal 6 i18n block data from database.
*
* For available configuration keys, refer to the parent classes.
*
* @see \Drupal\migrate\Plugin\migrate\source\SqlBase
* @see \Drupal\migrate\Plugin\migrate\source\SourcePluginBase
*
* @MigrateSource(
* id = "d6_block_translation",
* source_module = "i18nblocks"
* )
*/
class BlockTranslation extends Block {
/**
* {@inheritdoc}
*/
public function query() {
// Let the parent set the block table to use, but do not use the parent
// query. Instead build a query so can use an inner join to the selected
// block table.
parent::query();
$query = $this->select('i18n_blocks', 'i18n')
->fields('i18n')
->fields('b', ['bid', 'module', 'delta', 'theme', 'title']);
$query->innerJoin($this->blockTable, 'b', ('[b].[module] = [i18n].[module] AND [b].[delta] = [i18n].[delta]'));
return $query;
}
/**
* {@inheritdoc}
*/
public function fields() {
return [
'bid' => $this->t('The block numeric identifier.'),
'ibid' => $this->t('The i18n_blocks block numeric identifier.'),
'module' => $this->t('The module providing the block.'),
'delta' => $this->t("The block's delta."),
'type' => $this->t('Block type'),
'language' => $this->t('Language for this field.'),
'theme' => $this->t('Which theme the block is placed in.'),
'default_theme' => $this->t('The default theme.'),
'title' => $this->t('Block title.'),
];
}
/**
* {@inheritdoc}
*/
public function prepareRow(Row $row) {
$row->setSourceProperty('default_theme', $this->defaultTheme);
return SourcePluginBase::prepareRow($row);
}
/**
* {@inheritdoc}
*/
public function getIds() {
$ids = parent::getIds();
$ids['module']['alias'] = 'b';
$ids['delta']['alias'] = 'b';
$ids['theme']['alias'] = 'b';
$ids['language']['type'] = 'string';
return $ids;
}
}

View File

@ -0,0 +1,115 @@
<?php
namespace Drupal\block\Plugin\migrate\source\d7;
use Drupal\block\Plugin\migrate\source\Block;
// cspell:ignore objectid objectindex plid textgroup
/**
* Drupal 7 i18n block data from database.
*
* For available configuration keys, refer to the parent classes.
*
* @see \Drupal\migrate\Plugin\migrate\source\SqlBase
* @see \Drupal\migrate\Plugin\migrate\source\SourcePluginBase
*
* @MigrateSource(
* id = "d7_block_translation",
* source_module = "i18n_block"
* )
*/
class BlockTranslation extends Block {
/**
* {@inheritdoc}
*/
public function query() {
// Let the parent set the block table to use, but do not use the parent
// query. Instead build a query so can use an inner join to the selected
// block table.
parent::query();
$query = $this->select('i18n_string', 'i18n')
->fields('i18n')
->fields('b', [
'bid',
'module',
'delta',
'theme',
'status',
'weight',
'region',
'custom',
'visibility',
'pages',
'title',
'cache',
'i18n_mode',
])
->fields('lt', [
'lid',
'translation',
'language',
'plid',
'plural',
])
->condition('i18n_mode', 1);
$query->leftJoin($this->blockTable, 'b', ('[b].[delta] = [i18n].[objectid]'));
$query->innerJoin('locales_target', 'lt', '[lt].[lid] = [i18n].[lid]');
// The i18n_string module adds a status column to locale_target. It was
// originally 'status' in a later revision it was named 'i18n_status'.
if ($this->getDatabase()->schema()->fieldExists('locales_target', 'status')) {
$query->addField('lt', 'status', 'i18n_status');
}
if ($this->getDatabase()->schema()->fieldExists('locales_target', 'i18n_status')) {
$query->addField('lt', 'i18n_status', 'i18n_status');
}
return $query;
}
/**
* {@inheritdoc}
*/
public function fields() {
return [
'bid' => $this->t('The block numeric identifier.'),
'module' => $this->t('The module providing the block.'),
'delta' => $this->t("The block's delta."),
'theme' => $this->t('Which theme the block is placed in.'),
'status' => $this->t('Block enabled status'),
'weight' => $this->t('Block weight within region'),
'region' => $this->t('Theme region within which the block is set'),
'visibility' => $this->t('Visibility'),
'pages' => $this->t('Pages list.'),
'title' => $this->t('Block title.'),
'cache' => $this->t('Cache rule.'),
'i18n_mode' => $this->t('Multilingual mode'),
'lid' => $this->t('Language string ID'),
'textgroup' => $this->t('A module defined group of translations'),
'context' => $this->t('Full string ID for quick search: type:objectid:property.'),
'objectid' => $this->t('Object ID'),
'type' => $this->t('Object type for this string'),
'property' => $this->t('Object property for this string'),
'objectindex' => $this->t('Integer value of Object ID'),
'format' => $this->t('The {filter_format}.format of the string'),
'translation' => $this->t('Translation'),
'language' => $this->t('Language code'),
'plid' => $this->t('Parent lid'),
'plural' => $this->t('Plural index number'),
'i18n_status' => $this->t('Translation needs update'),
];
}
/**
* {@inheritdoc}
*/
public function getIds() {
$ids['delta']['type'] = 'string';
$ids['delta']['alias'] = 'b';
$ids['language']['type'] = 'string';
return $ids;
}
}

View File

@ -0,0 +1,29 @@
<?php
namespace Drupal\block\Theme;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\Core\Theme\ThemeNegotiatorInterface;
/**
* Negotiates the theme for the block admin demo page via the URL.
*/
class AdminDemoNegotiator implements ThemeNegotiatorInterface {
/**
* {@inheritdoc}
*/
public function applies(RouteMatchInterface $route_match) {
return $route_match->getRouteName() == 'block.admin_demo';
}
/**
* {@inheritdoc}
*/
public function determineActiveTheme(RouteMatchInterface $route_match) {
// We return exactly what was passed in, to guarantee that the page will
// always be displayed using the theme whose blocks are being configured.
return $route_match->getParameter('theme');
}
}

View File

@ -0,0 +1,40 @@
{#
/**
* @file
* Default theme implementation to display a block.
*
* Available variables:
* - plugin_id: The ID of the block implementation.
* - label: The configured label of the block if visible.
* - configuration: A list of the block's configuration values.
* - label: The configured label for the block.
* - label_display: The display settings for the label.
* - provider: The module or other provider that provided this block plugin.
* - Block plugin specific settings will also be stored here.
* - in_preview: Whether the plugin is being rendered in preview mode.
* - content: The content of this block.
* - attributes: array of HTML attributes populated by modules, intended to
* be added to the main container tag of this template.
* - id: A valid HTML ID and guaranteed unique.
* - title_attributes: Same as attributes, except applied to the main title
* tag that appears in the template.
* - title_prefix: Additional output populated by modules, intended to be
* displayed in front of the main title tag that appears in the template.
* - title_suffix: Additional output populated by modules, intended to be
* displayed after the main title tag that appears in the template.
*
* @see template_preprocess_block()
*
* @ingroup themeable
*/
#}
<div{{ attributes }}>
{{ title_prefix }}
{% if label %}
<h2{{ title_attributes }}>{{ label }}</h2>
{% endif %}
{{ title_suffix }}
{% block content %}
{{ content }}
{% endblock %}
</div>

View File

@ -0,0 +1,26 @@
<?php
/**
* @file
* Adds a menu block with a `depth` setting of 0.
*/
use Drupal\Core\Database\Database;
$connection = Database::getConnection();
$data = $connection->select('config')
->condition('name', 'block.block.olivero_account_menu')
->fields('config', ['data'])
->execute()
->fetchField();
$data = unserialize($data);
// Change the depth setting to 0, which the update hook should change to NULL.
// @see system_post_update_set_menu_block_depth_to_null_if_zero().
$data['settings']['depth'] = 0;
$connection->update('config')
->condition('name', 'block.block.olivero_account_menu')
->fields([
'data' => serialize($data),
])
->execute();

View File

@ -0,0 +1,7 @@
name: 'Block test'
type: module
description: 'Provides test blocks.'
package: Testing
version: VERSION
dependencies:
- drupal:block

View File

@ -0,0 +1,7 @@
block_test.test_multiple_forms:
path: '/test-multiple-forms'
defaults:
_controller: '\Drupal\block_test\Controller\TestMultipleFormController::testMultipleForms'
_title: 'Multiple forms'
requirements:
_access: 'TRUE'

View File

@ -0,0 +1,6 @@
services:
block_test.multiple_static_context:
class: Drupal\block_test\ContextProvider\MultipleStaticContext
arguments: ['@current_user', '@entity_type.manager']
tags:
- { name: 'context_provider' }

View File

@ -0,0 +1,17 @@
langcode: en
status: true
dependencies:
module:
- block_test
theme:
- stark
id: test_block
theme: stark
region: '-1'
weight: 0
plugin: test_html
settings:
label: 'Test HTML block'
label_display: hidden
provider: block_test
visibility: { }

View File

@ -0,0 +1,10 @@
block.settings.test_block_instantiation:
type: block_settings
label: 'Test block instantiation settings'
mapping:
display_message:
type: string
label: 'Message text'
condition.plugin.baloney_spam:
type: condition.plugin

View File

@ -0,0 +1,24 @@
<?php
declare(strict_types=1);
namespace Drupal\block_test;
use Drupal\Core\Render\Element\RenderCallbackInterface;
/**
* Implements a trusted preRender callback.
*/
class BlockRenderAlterContent implements RenderCallbackInterface {
/**
* Render API callback: Alters the content of a block.
*
* This function is assigned as a #pre_render callback.
*/
public static function preRender(array $build) {
$build['#prefix'] = 'Hiya!<br>';
return $build;
}
}

View File

@ -0,0 +1,73 @@
<?php
declare(strict_types=1);
namespace Drupal\block_test\ContextProvider;
use Drupal\Core\Cache\CacheableMetadata;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Plugin\Context\ContextProviderInterface;
use Drupal\Core\Plugin\Context\EntityContext;
use Drupal\Core\Session\AccountInterface;
/**
* Sets multiple contexts for a static value.
*/
class MultipleStaticContext implements ContextProviderInterface {
/**
* The current user.
*
* @var \Drupal\Core\Session\AccountInterface
*/
protected $account;
/**
* The user storage.
*
* @var \Drupal\user\UserStorageInterface
*/
protected $userStorage;
/**
* Constructs a new MultipleStaticContext.
*
* @param \Drupal\Core\Session\AccountInterface $account
* The current user.
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity type manager.
*/
public function __construct(AccountInterface $account, EntityTypeManagerInterface $entity_type_manager) {
$this->account = $account;
$this->userStorage = $entity_type_manager->getStorage('user');
}
/**
* {@inheritdoc}
*/
public function getRuntimeContexts(array $unqualified_context_ids) {
$current_user = $this->userStorage->load($this->account->id());
$context1 = EntityContext::fromEntity($current_user, 'User A');
$context2 = EntityContext::fromEntity($current_user, 'User B');
$cacheability = new CacheableMetadata();
$cacheability->setCacheContexts(['user']);
$context1->addCacheableDependency($cacheability);
$context2->addCacheableDependency($cacheability);
return [
'userA' => $context1,
'userB' => $context2,
];
}
/**
* {@inheritdoc}
*/
public function getAvailableContexts() {
return $this->getRuntimeContexts([]);
}
}

View File

@ -0,0 +1,42 @@
<?php
declare(strict_types=1);
namespace Drupal\block_test\Controller;
use Drupal\Core\Controller\ControllerBase;
use Drupal\Core\Form\FormState;
/**
* Controller for block_test module.
*/
class TestMultipleFormController extends ControllerBase {
public function testMultipleForms() {
$form_state = new FormState();
$build = [
'form1' => $this->formBuilder()->buildForm('\Drupal\block_test\Form\TestForm', $form_state),
'form2' => $this->formBuilder()->buildForm('\Drupal\block_test\Form\FavoriteAnimalTestForm', $form_state),
];
// Output all attached placeholders trough
// \Drupal\Core\Messenger\MessengerInterface::addMessage(), so we can
// see if there's only one in the tests.
$post_render_callable = function ($elements) {
$matches = [];
preg_match_all('<form\s(.*?)action="(.*?)"(.*)>', (string) $elements, $matches);
$action_values = $matches[2];
foreach ($action_values as $action_value) {
$this->messenger()->addStatus('Form action: ' . $action_value);
}
return $elements;
};
$build['#post_render'] = [$post_render_callable];
return $build;
}
}

View File

@ -0,0 +1,48 @@
<?php
declare(strict_types=1);
namespace Drupal\block_test\Form;
use Drupal\Core\Form\FormBase;
use Drupal\Core\Form\FormStateInterface;
/**
* Form that performs favorite animal test.
*
* @internal
*/
class FavoriteAnimalTestForm extends FormBase {
/**
* {@inheritdoc}
*/
public function getFormId() {
return 'block_test_form_favorite_animal_test';
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state) {
$form['favorite_animal'] = [
'#type' => 'textfield',
'#title' => $this->t('Your favorite animal.'),
];
$form['submit_animal'] = [
'#type' => 'submit',
'#value' => $this->t('Submit your chosen animal'),
];
return $form;
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
$this->messenger()->addStatus($this->t('Your favorite animal is: @favorite_animal', ['@favorite_animal' => $form['favorite_animal']['#value']]));
}
}

View File

@ -0,0 +1,57 @@
<?php
declare(strict_types=1);
namespace Drupal\block_test\Form;
use Drupal\Core\Form\FormBase;
use Drupal\Core\Form\FormStateInterface;
/**
* Form that performs base block form test.
*
* @internal
*/
class TestForm extends FormBase {
/**
* {@inheritdoc}
*/
public function getFormId() {
return 'block_test_form_test';
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state) {
$form['email'] = [
'#type' => 'email',
'#title' => $this->t('Your .com email address.'),
];
$form['show'] = [
'#type' => 'submit',
'#value' => $this->t('Submit'),
];
return $form;
}
/**
* {@inheritdoc}
*/
public function validateForm(array &$form, FormStateInterface $form_state) {
if (!str_contains($form_state->getValue('email'), '.com')) {
$form_state->setErrorByName('email', $this->t('This is not a .com email address.'));
}
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
$this->messenger()->addStatus($this->t('Your email address is @email', ['@email' => $form['email']['#value']]));
}
}

View File

@ -0,0 +1,73 @@
<?php
declare(strict_types=1);
namespace Drupal\block_test\Hook;
use Drupal\Core\Cache\Cache;
use Drupal\Core\Block\BlockPluginInterface;
use Drupal\Core\Hook\Attribute\Hook;
/**
* Hook implementations for block_test.
*/
class BlockTestHooks {
/**
* Implements hook_block_alter().
*/
#[Hook('block_alter')]
public function blockAlter(&$block_info): void {
if (\Drupal::state()->get('block_test_info_alter') && isset($block_info['test_block_instantiation'])) {
$block_info['test_block_instantiation']['category'] = 'Custom category';
}
}
/**
* Implements hook_block_view_BASE_BLOCK_ID_alter().
*/
#[Hook('block_view_test_cache_alter')]
public function blockViewTestCacheAlter(array &$build, BlockPluginInterface $block): void {
if (\Drupal::state()->get('block_test_view_alter_suffix') !== NULL) {
$build['#attributes']['foo'] = 'bar';
}
if (\Drupal::state()->get('block_test_view_alter_append_pre_render_prefix') !== NULL) {
$build['#pre_render'][] = '\Drupal\block_test\BlockRenderAlterContent::preRender';
}
}
/**
* Implements hook_block_view_BASE_BLOCK_ID_alter().
*
* @see \Drupal\Tests\block\Kernel\BlockViewBuilderTest::testBlockViewBuilderCacheTitleBlock()
*/
#[Hook('block_view_page_title_block_alter')]
public function blockViewPageTitleBlockAlter(array &$build, BlockPluginInterface $block): void {
$build['#cache']['tags'][] = 'custom_cache_tag';
}
/**
* Implements hook_block_build_BASE_BLOCK_ID_alter().
*/
#[Hook('block_build_test_cache_alter')]
public function blockBuildTestCacheAlter(array &$build, BlockPluginInterface $block): void {
// Test altering cache keys, contexts, tags and max-age.
if (\Drupal::state()->get('block_test_block_alter_cache_key') !== NULL) {
$build['#cache']['keys'][] = \Drupal::state()->get('block_test_block_alter_cache_key');
}
if (\Drupal::state()->get('block_test_block_alter_cache_context') !== NULL) {
$build['#cache']['contexts'][] = \Drupal::state()->get('block_test_block_alter_cache_context');
}
if (\Drupal::state()->get('block_test_block_alter_cache_tag') !== NULL) {
$build['#cache']['tags'] = Cache::mergeTags($build['#cache']['tags'], [\Drupal::state()->get('block_test_block_alter_cache_tag')]);
}
if (\Drupal::state()->get('block_test_block_alter_cache_max_age') !== NULL) {
$build['#cache']['max-age'] = \Drupal::state()->get('block_test_block_alter_cache_max_age');
}
// Test setting #create_placeholder.
if (\Drupal::state()->get('block_test_block_alter_create_placeholder') !== NULL) {
$build['#create_placeholder'] = \Drupal::state()->get('block_test_block_alter_create_placeholder');
}
}
}

View File

@ -0,0 +1,78 @@
<?php
declare(strict_types=1);
namespace Drupal\block_test\Plugin\Block;
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Block\Attribute\Block;
use Drupal\Core\Block\BlockBase;
use Drupal\Core\Cache\Cache;
use Drupal\Core\KeyValueStore\KeyValueStoreInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Provides a block to test access.
*/
#[Block(
id: "test_access",
admin_label: new TranslatableMarkup("Test block access"),
)]
class TestAccessBlock extends BlockBase implements ContainerFactoryPluginInterface {
/**
* Tests the test access block.
*
* @param array $configuration
* The plugin configuration, i.e. an array with configuration values keyed
* by configuration option name. The special key 'context' may be used to
* initialize the defined contexts by setting it to an array of context
* values keyed by context names.
* @param string $plugin_id
* The plugin ID for the plugin instance.
* @param mixed $plugin_definition
* The plugin implementation definition.
* @param \Drupal\Core\KeyValueStore\KeyValueStoreInterface $keyValue
* The key value store.
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, protected KeyValueStoreInterface $keyValue) {
parent::__construct($configuration, $plugin_id, $plugin_definition);
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
return new static(
$configuration,
$plugin_id,
$plugin_definition,
$container->get('keyvalue')->get('block_test')
);
}
/**
* {@inheritdoc}
*/
protected function blockAccess(AccountInterface $account) {
return $this->keyValue->get('access', FALSE) ? AccessResult::allowed()->setCacheMaxAge(0) : AccessResult::forbidden()->setCacheMaxAge(0);
}
/**
* {@inheritdoc}
*/
public function build() {
return ['#markup' => 'Hello test world'];
}
/**
* {@inheritdoc}
*/
public function getCacheMaxAge() {
return Cache::PERMANENT;
}
}

View File

@ -0,0 +1,67 @@
<?php
declare(strict_types=1);
namespace Drupal\block_test\Plugin\Block;
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Block\Attribute\Block;
use Drupal\Core\Block\BlockBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\StringTranslation\TranslatableMarkup;
/**
* Provides a basic block for testing block instantiation and configuration.
*/
#[Block(
id: "test_block_instantiation",
admin_label: new TranslatableMarkup("Display message")
)]
class TestBlockInstantiation extends BlockBase {
/**
* {@inheritdoc}
*/
public function defaultConfiguration() {
return [
'display_message' => 'no message set',
];
}
/**
* {@inheritdoc}
*/
protected function blockAccess(AccountInterface $account) {
return AccessResult::allowedIfHasPermission($account, 'access content');
}
/**
* {@inheritdoc}
*/
public function blockForm($form, FormStateInterface $form_state) {
$form['display_message'] = [
'#type' => 'textfield',
'#title' => $this->t('Display message'),
'#default_value' => $this->configuration['display_message'],
];
return $form;
}
/**
* {@inheritdoc}
*/
public function blockSubmit($form, FormStateInterface $form_state) {
$this->configuration['display_message'] = $form_state->getValue('display_message');
}
/**
* {@inheritdoc}
*/
public function build() {
return [
'#children' => $this->configuration['display_message'],
];
}
}

View File

@ -0,0 +1,47 @@
<?php
declare(strict_types=1);
namespace Drupal\block_test\Plugin\Block;
use Drupal\Core\Block\Attribute\Block;
use Drupal\Core\Block\BlockBase;
use Drupal\Core\StringTranslation\TranslatableMarkup;
/**
* Provides a block to test caching.
*/
#[Block(
id: "test_cache",
admin_label: new TranslatableMarkup("Test block caching")
)]
class TestCacheBlock extends BlockBase {
/**
* {@inheritdoc}
*/
public function build() {
$content = \Drupal::keyValue('block_test')->get('content');
$build = [];
if (!empty($content)) {
$build['#markup'] = $content;
}
return $build;
}
/**
* {@inheritdoc}
*/
public function getCacheContexts() {
return \Drupal::state()->get('block_test.cache_contexts', []);
}
/**
* {@inheritdoc}
*/
public function getCacheMaxAge() {
return \Drupal::state()->get('block_test.cache_max_age', parent::getCacheMaxAge());
}
}

View File

@ -0,0 +1,57 @@
<?php
declare(strict_types=1);
namespace Drupal\block_test\Plugin\Block;
use Drupal\Core\Block\Attribute\Block;
use Drupal\Core\Block\BlockBase;
use Drupal\Core\Plugin\Context\EntityContextDefinition;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\user\UserInterface;
/**
* Provides a context-aware block.
*/
#[Block(
id: "test_context_aware",
admin_label: new TranslatableMarkup("Test context-aware block"),
context_definitions: [
'user' => new EntityContextDefinition(
data_type: 'entity:user',
label: new TranslatableMarkup("User Context"),
required: FALSE,
constraints: [
"NotNull" => [],
]
),
]
)]
class TestContextAwareBlock extends BlockBase {
/**
* {@inheritdoc}
*/
public function build() {
/** @var \Drupal\user\UserInterface $user */
$user = $this->getContextValue('user');
return [
'#prefix' => '<div id="' . $this->getPluginId() . '--username">',
'#suffix' => '</div>',
'#markup' => $user ? $user->getAccountName() : 'No context mapping selected.',
];
}
/**
* {@inheritdoc}
*/
protected function blockAccess(AccountInterface $account) {
if ($this->getContextValue('user') instanceof UserInterface) {
$this->messenger()->addStatus('User context found.');
}
return parent::blockAccess($account);
}
}

View File

@ -0,0 +1,33 @@
<?php
declare(strict_types=1);
namespace Drupal\block_test\Plugin\Block;
use Drupal\Core\Block\Attribute\Block;
use Drupal\Core\Block\BlockBase;
use Drupal\Core\Plugin\Context\ContextDefinition;
use Drupal\Core\StringTranslation\TranslatableMarkup;
/**
* Provides a context-aware block that uses a not-passed, non-required context.
*/
#[Block(
id: "test_context_aware_no_valid_context_options",
admin_label: new TranslatableMarkup("Test context-aware block - no valid context options"),
context_definitions: [
'user' => new ContextDefinition(data_type: 'email', required: FALSE),
]
)]
class TestContextAwareNoValidContextOptionsBlock extends BlockBase {
/**
* {@inheritdoc}
*/
public function build() {
return [
'#markup' => 'Rendered block with no valid context options',
];
}
}

View File

@ -0,0 +1,33 @@
<?php
declare(strict_types=1);
namespace Drupal\block_test\Plugin\Block;
use Drupal\Core\Block\Attribute\Block;
use Drupal\Core\Block\BlockBase;
use Drupal\Core\Plugin\Context\EntityContextDefinition;
use Drupal\Core\StringTranslation\TranslatableMarkup;
/**
* Provides a context-aware block.
*/
#[Block(
id: "test_context_aware_unsatisfied",
admin_label: new TranslatableMarkup("Test context-aware unsatisfied block"),
context_definitions: [
'user' => new EntityContextDefinition('entity:foobar'),
]
)]
class TestContextAwareUnsatisfiedBlock extends BlockBase {
/**
* {@inheritdoc}
*/
public function build() {
return [
'#markup' => 'test',
];
}
}

View File

@ -0,0 +1,27 @@
<?php
declare(strict_types=1);
namespace Drupal\block_test\Plugin\Block;
use Drupal\Core\Block\Attribute\Block;
use Drupal\Core\Block\BlockBase;
use Drupal\Core\StringTranslation\TranslatableMarkup;
/**
* Provides a block that returns an empty array.
*/
#[Block(
id: "test_empty",
admin_label: new TranslatableMarkup("Test Empty block"),
)]
class TestEmptyBlock extends BlockBase {
/**
* {@inheritdoc}
*/
public function build() {
return [];
}
}

View File

@ -0,0 +1,27 @@
<?php
declare(strict_types=1);
namespace Drupal\block_test\Plugin\Block;
use Drupal\Core\Block\Attribute\Block;
use Drupal\Core\Block\BlockBase;
use Drupal\Core\StringTranslation\TranslatableMarkup;
/**
* Provides a block to test caching.
*/
#[Block(
id: "test_form_in_block",
admin_label: new TranslatableMarkup("Test form block caching"),
)]
class TestFormBlock extends BlockBase {
/**
* {@inheritdoc}
*/
public function build() {
return \Drupal::formBuilder()->getForm('Drupal\block_test\Form\TestForm');
}
}

View File

@ -0,0 +1,30 @@
<?php
declare(strict_types=1);
namespace Drupal\block_test\Plugin\Block;
use Drupal\Core\Block\Attribute\Block;
use Drupal\Core\Block\BlockBase;
use Drupal\Core\StringTranslation\TranslatableMarkup;
/**
* Provides a block to test HTML.
*/
#[Block(
id: "test_html",
admin_label: new TranslatableMarkup("Test HTML block"),
)]
class TestHtmlBlock extends BlockBase {
/**
* {@inheritdoc}
*/
public function build() {
return [
'#attributes' => \Drupal::keyvalue('block_test')->get('attributes'),
'#children' => \Drupal::keyValue('block_test')->get('content'),
];
}
}

View File

@ -0,0 +1,31 @@
<?php
declare(strict_types=1);
namespace Drupal\block_test\Plugin\Block;
use Drupal\block_test\PluginForm\EmptyBlockForm;
use Drupal\Core\Block\Attribute\Block;
use Drupal\Core\Block\BlockBase;
use Drupal\Core\StringTranslation\TranslatableMarkup;
/**
* Provides a block with multiple forms.
*/
#[Block(
id: "test_multiple_forms_block",
forms: [
'secondary' => EmptyBlockForm::class,
],
admin_label: new TranslatableMarkup("Multiple forms test block"),
)]
class TestMultipleFormsBlock extends BlockBase {
/**
* {@inheritdoc}
*/
public function build() {
return [];
}
}

View File

@ -0,0 +1,44 @@
<?php
declare(strict_types=1);
namespace Drupal\block_test\Plugin\Block;
use Drupal\Core\Block\Attribute\Block;
use Drupal\Core\Block\BlockBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\StringTranslation\TranslatableMarkup;
/**
* Provides a test settings validation block.
*/
#[Block(
id: "test_settings_validation",
admin_label: new TranslatableMarkup("Test settings validation block"),
)]
class TestSettingsValidationBlock extends BlockBase {
/**
* {@inheritdoc}
*/
public function blockForm($form, FormStateInterface $form_state) {
return ['digits' => ['#type' => 'textfield']] + $form;
}
/**
* {@inheritdoc}
*/
public function blockValidate($form, FormStateInterface $form_state) {
if (!ctype_digit($form_state->getValue('digits'))) {
$form_state->setErrorByName('digits', $this->t('Only digits are allowed'));
}
}
/**
* {@inheritdoc}
*/
public function build() {
return ['#markup' => 'foo'];
}
}

View File

@ -0,0 +1,34 @@
<?php
declare(strict_types=1);
namespace Drupal\block_test\Plugin\Condition;
use Drupal\Core\Condition\Attribute\Condition;
use Drupal\Core\Condition\ConditionPluginBase;
use Drupal\Core\StringTranslation\TranslatableMarkup;
/**
* Provides a 'baloney_spam' condition.
*/
#[Condition(
id: "baloney_spam",
label: new TranslatableMarkup("Baloney spam"),
)]
class BaloneySpam extends ConditionPluginBase {
/**
* {@inheritdoc}
*/
public function evaluate() {
return TRUE;
}
/**
* {@inheritdoc}
*/
public function summary() {
return 'Summary';
}
}

View File

@ -0,0 +1,34 @@
<?php
declare(strict_types=1);
namespace Drupal\block_test\Plugin\Condition;
use Drupal\Core\Condition\Attribute\Condition;
use Drupal\Core\Condition\ConditionPluginBase;
use Drupal\Core\StringTranslation\TranslatableMarkup;
/**
* Provides a 'missing_schema' condition.
*/
#[Condition(
id: "missing_schema",
label: new TranslatableMarkup("Missing schema"),
)]
class MissingSchema extends ConditionPluginBase {
/**
* {@inheritdoc}
*/
public function evaluate() {
return FALSE;
}
/**
* {@inheritdoc}
*/
public function summary() {
return 'Summary';
}
}

View File

@ -0,0 +1,34 @@
<?php
declare(strict_types=1);
namespace Drupal\block_test\PluginForm;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Plugin\PluginFormBase;
/**
* Provides a form for a block that is empty.
*/
class EmptyBlockForm extends PluginFormBase {
/**
* {@inheritdoc}
*/
public $plugin;
/**
* {@inheritdoc}
*/
public function buildConfigurationForm(array $form, FormStateInterface $form_state) {
return $form;
}
/**
* {@inheritdoc}
*/
public function submitConfigurationForm(array &$form, FormStateInterface $form_state) {
// Intentionally empty.
}
}

View File

@ -0,0 +1,7 @@
name: '<"Cat" & ''Mouse''>'
type: theme
base theme: stable9
description: 'Theme for testing special characters in block admin.'
regions:
content: Content
help: Help

View File

@ -0,0 +1,16 @@
name: 'Block test theme'
type: theme
base theme: stable9
description: 'Theme for testing the block system'
version: VERSION
regions:
sidebar_first: 'Left sidebar'
sidebar_second: 'Right sidebar'
content: Content
header: Header
footer: Footer
highlighted: Highlighted
help: Help
regions_hidden:
- sidebar_first
- sidebar_second

View File

@ -0,0 +1,8 @@
name: 'Block test views'
type: module
description: 'Provides a view and block to test block displays in views.'
package: Testing
version: VERSION
dependencies:
- drupal:block
- drupal:views

View File

@ -0,0 +1,56 @@
langcode: en
status: true
dependencies: { }
id: test_view_block
label: test_view_block
module: views
description: ''
tag: ''
base_table: views_test_data
base_field: id
display:
default:
display_plugin: default
id: default
display_title: Default
position: null
display_options:
access:
type: none
cache:
type: tag
query:
type: views_query
exposed_form:
type: basic
pager:
type: some
options:
items_per_page: 5
style:
type: default
row:
type: fields
fields:
name:
id: name
table: views_test_data
field: name
title: test_view_block
footer:
area_text_custom:
id: area_text_custom
table: views
field: area_text_custom
relationship: none
group_type: group
admin_label: ''
plugin_id: text_custom
empty: false
content: '[view:title]'
tokenize: false
block_1:
display_plugin: block
id: block_1
display_title: Block
position: null

View File

@ -0,0 +1,56 @@
langcode: en
status: true
dependencies:
module:
- user
id: test_view_block2
label: test_view_block2
module: views
description: ''
tag: ''
base_table: views_test_data
base_field: id
display:
default:
display_plugin: default
id: default
display_title: Default
position: null
display_options:
access:
type: perm
cache:
type: tag
query:
type: views_query
exposed_form:
type: basic
pager:
type: some
options:
items_per_page: 5
style:
type: default
row:
type: fields
fields:
name:
id: name
table: views_test_data
field: name
title: test_view_block2
block_1:
display_plugin: block
id: block_1
display_title: Block
position: null
block_2:
display_plugin: block
id: block_2
display_title: Block
position: null
block_3:
display_plugin: block
id: block_3
display_title: Block
position: null

View File

@ -0,0 +1,426 @@
langcode: en
status: true
dependencies:
module:
- node
- user
id: test_view_block_with_context
label: test_view_block_with_context
module: views
description: ''
tag: ''
base_table: node_field_data
base_field: nid
display:
default:
display_plugin: default
id: default
display_title: Default
position: 0
display_options:
access:
type: perm
options:
perm: 'access content'
cache:
type: tag
options: { }
query:
type: views_query
options:
disable_sql_rewrite: false
distinct: false
replica: false
query_comment: ''
query_tags: { }
exposed_form:
type: basic
options:
submit_button: Apply
reset_button: false
reset_button_label: Reset
exposed_sorts_label: 'Sort by'
expose_sort_order: true
sort_asc_label: Asc
sort_desc_label: Desc
pager:
type: some
options:
items_per_page: 5
offset: 0
style:
type: default
row:
type: fields
fields:
title:
id: title
table: node_field_data
field: title
relationship: none
group_type: group
admin_label: ''
label: ''
exclude: false
alter:
alter_text: true
text: 'Test view row: {{ title }}'
make_link: false
path: ''
absolute: false
external: false
replace_spaces: false
path_case: none
trim_whitespace: false
alt: ''
rel: ''
link_class: ''
prefix: ''
suffix: ''
target: ''
nl2br: false
max_length: 0
word_boundary: true
ellipsis: true
more_link: false
more_link_text: ''
more_link_path: ''
strip_tags: false
trim: false
preserve_tags: ''
html: false
element_type: ''
element_class: ''
element_label_type: ''
element_label_class: ''
element_label_colon: false
element_wrapper_type: ''
element_wrapper_class: ''
element_default_classes: true
empty: ''
hide_empty: false
empty_zero: false
hide_alter_empty: true
click_sort_column: value
type: string
settings:
link_to_entity: true
group_column: value
group_columns: { }
group_rows: true
delta_limit: 0
delta_offset: 0
delta_reversed: false
delta_first_last: false
multi_type: separator
separator: ', '
field_api_classes: false
plugin_id: field
filters:
status:
value: '1'
table: node_field_data
field: status
plugin_id: boolean
entity_type: node
entity_field: status
id: status
expose:
operator: ''
group: 1
sorts:
created:
id: created
table: node_field_data
field: created
order: DESC
entity_type: node
entity_field: created
plugin_id: date
relationship: none
group_type: group
admin_label: ''
exposed: false
expose:
label: ''
granularity: second
title: test_view_block_with_context
header: { }
footer: { }
empty:
area_text_custom:
id: area_text_custom
table: views
field: area_text_custom
relationship: none
group_type: group
admin_label: ''
empty: true
tokenize: false
content: 'Test view: No results found.'
plugin_id: text_custom
relationships: { }
arguments:
'null':
id: 'null'
table: views
field: 'null'
relationship: none
group_type: group
admin_label: ''
default_action: default
exception:
value: all
title_enable: false
title: All
title_enable: false
title: ''
default_argument_type: fixed
default_argument_options:
argument: foo
summary_options:
base_path: ''
count: true
items_per_page: 25
override: false
summary:
sort_order: asc
number_of_records: 0
format: default_summary
specify_validation: false
validate:
type: none
fail: 'not found'
validate_options: { }
must_not_be: false
plugin_id: 'null'
null_1:
id: null_1
table: views
field: 'null'
relationship: none
group_type: group
admin_label: ''
default_action: ignore
exception:
value: all
title_enable: false
title: All
title_enable: false
title: ''
default_argument_type: fixed
default_argument_options:
argument: ''
summary_options:
base_path: ''
count: true
items_per_page: 25
override: false
summary:
sort_order: asc
number_of_records: 0
format: default_summary
specify_validation: false
validate:
type: none
fail: 'not found'
validate_options: { }
must_not_be: false
plugin_id: 'null'
nid:
id: nid
table: node_field_data
field: nid
relationship: none
group_type: group
admin_label: ''
default_action: empty
exception:
value: all
title_enable: false
title: All
title_enable: false
title: ''
default_argument_type: fixed
default_argument_options:
argument: ''
summary_options:
base_path: ''
count: true
items_per_page: 25
override: false
summary:
sort_order: asc
number_of_records: 0
format: default_summary
specify_validation: true
validate:
type: 'entity:node'
fail: 'not found'
validate_options:
operation: view
multiple: 0
bundles: { }
access: false
break_phrase: false
not: false
entity_type: node
entity_field: nid
plugin_id: node_nid
display_extenders: { }
cache_metadata:
contexts:
- 'languages:language_content'
- 'languages:language_interface'
- url
- 'user.node_grants:view'
- user.permissions
cacheable: false
max-age: -1
tags: { }
block_1:
display_plugin: block
id: block_1
display_title: Block
position: 1
display_options:
display_extenders: { }
cache_metadata:
contexts:
- 'languages:language_content'
- 'languages:language_interface'
- url
- 'user.node_grants:view'
- user.permissions
cacheable: false
max-age: -1
tags: { }
block_2:
display_plugin: block
id: block_2
display_title: 'Block 2'
position: 2
display_options:
display_extenders: { }
arguments:
created:
id: created
table: node_field_data
field: created
relationship: none
group_type: group
admin_label: ''
default_action: ignore
exception:
value: all
title_enable: false
title: All
title_enable: false
title: ''
default_argument_type: fixed
default_argument_options:
argument: ''
summary_options:
base_path: ''
count: true
items_per_page: 25
override: false
summary:
sort_order: asc
number_of_records: 0
format: default_summary
specify_validation: true
validate:
type: numeric
fail: 'not found'
validate_options: { }
entity_type: node
entity_field: created
plugin_id: date
vid:
id: vid
table: node_field_data
field: vid
relationship: none
group_type: group
admin_label: ''
default_action: ignore
exception:
value: all
title_enable: false
title: All
title_enable: false
title: ''
default_argument_type: fixed
default_argument_options:
argument: ''
summary_options:
base_path: ''
count: true
items_per_page: 25
override: false
summary:
sort_order: asc
number_of_records: 0
format: default_summary
specify_validation: false
validate:
type: none
fail: 'not found'
validate_options: { }
break_phrase: false
not: false
entity_type: node
entity_field: vid
plugin_id: numeric
title:
id: title
table: node_field_data
field: title
relationship: none
group_type: group
admin_label: ''
default_action: ignore
exception:
value: all
title_enable: false
title: All
title_enable: false
title: ''
default_argument_type: fixed
default_argument_options:
argument: ''
summary_options:
base_path: ''
count: true
items_per_page: 25
override: false
summary:
sort_order: asc
number_of_records: 0
format: default_summary
specify_validation: false
validate:
type: none
fail: 'not found'
validate_options: { }
glossary: false
limit: 0
case: none
path_case: none
transform_dash: false
break_phrase: false
entity_type: node
entity_field: title
plugin_id: string
defaults:
arguments: false
cache_metadata:
max-age: -1
contexts:
- 'languages:language_content'
- 'languages:language_interface'
- url
- 'user.node_grants:view'
- user.permissions
tags: { }

View File

@ -0,0 +1,36 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\block\Functional;
use Drupal\block\Entity\Block;
/**
* Provides test assertions for testing block appearance.
*
* Can be used by test classes that extend \Drupal\Tests\BrowserTestBase.
*/
trait AssertBlockAppearsTrait {
/**
* Checks to see whether a block appears on the page.
*
* @param \Drupal\block\Entity\Block $block
* The block entity to find on the page.
*/
protected function assertBlockAppears(Block $block) {
$this->assertSession()->elementExists('xpath', "//div[@id = 'block-{$block->id()}']");
}
/**
* Checks to see whether a block does not appears on the page.
*
* @param \Drupal\block\Entity\Block $block
* The block entity to find on the page.
*/
protected function assertNoBlockAppears(Block $block) {
$this->assertSession()->elementNotExists('xpath', "//div[@id = 'block-{$block->id()}']");
}
}

View File

@ -0,0 +1,87 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\block\Functional;
use Drupal\Tests\BrowserTestBase;
/**
* Tests the block system with admin themes.
*
* @group block
*/
class BlockAdminThemeTest extends BrowserTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = ['block', 'contextual'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* Check for the accessibility of the admin theme on the block admin page.
*/
public function testAdminTheme(): void {
// Create administrative user.
$admin_user = $this->drupalCreateUser([
'administer blocks',
'administer themes',
]);
$this->drupalLogin($admin_user);
// Ensure that access to block admin page is denied when theme is not
// installed.
$this->drupalGet('admin/structure/block/list/olivero');
$this->assertSession()->statusCodeEquals(403);
// Install admin theme and confirm that tab is accessible.
\Drupal::service('theme_installer')->install(['olivero']);
$edit['admin_theme'] = 'olivero';
$this->drupalGet('admin/appearance');
$this->submitForm($edit, 'Save configuration');
$this->drupalGet('admin/structure/block/list/olivero');
$this->assertSession()->statusCodeEquals(200);
}
/**
* Ensure contextual links are disabled in Claro theme.
*/
public function testClaroAdminTheme(): void {
// Create administrative user.
$admin_user = $this->drupalCreateUser([
'access administration pages',
'administer themes',
'access contextual links',
'view the administration theme',
]);
$this->drupalLogin($admin_user);
// Install admin theme and confirm that tab is accessible.
\Drupal::service('theme_installer')->install(['claro']);
$edit['admin_theme'] = 'claro';
$this->drupalGet('admin/appearance');
$this->submitForm($edit, 'Save configuration');
// Define our block settings.
$settings = [
'theme' => 'claro',
'region' => 'header',
];
// Place a block.
$block = $this->drupalPlaceBlock('local_tasks_block', $settings);
// Open admin page.
$this->drupalGet('admin');
// Check if contextual link classes are unavailable.
$this->assertSession()->responseNotContains('<div data-contextual-id="block:block=' . $block->id() . ':langcode=en"></div>');
$this->assertSession()->responseNotContains('contextual-region');
}
}

View File

@ -0,0 +1,240 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\block\Functional;
use Drupal\Core\Cache\Cache;
use Drupal\Tests\BrowserTestBase;
/**
* Tests block caching.
*
* @group block
*/
class BlockCacheTest extends BrowserTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = ['block', 'block_test', 'test_page_test'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* A user with permission to administer blocks.
*
* @var object
*/
protected $adminUser;
/**
* An authenticated user to test block caching.
*
* @var object
*/
protected $normalUser;
/**
* Another authenticated user to test block caching.
*
* @var object
*/
protected $normalUserAlt;
/**
* The block used by this test.
*
* @var \Drupal\block\BlockInterface
*/
protected $block;
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
// Create an admin user, log in and enable test blocks.
$this->adminUser = $this->drupalCreateUser([
'administer blocks',
'access administration pages',
]);
$this->drupalLogin($this->adminUser);
// Create additional users to test caching modes.
$this->normalUser = $this->drupalCreateUser();
$this->normalUserAlt = $this->drupalCreateUser();
// Sync the roles, since drupalCreateUser() creates separate roles for
// the same permission sets.
$this->normalUserAlt->roles = $this->normalUser->getRoles();
$this->normalUserAlt->save();
// Enable our test block.
$this->block = $this->drupalPlaceBlock('test_cache');
}
/**
* Tests "user.roles" cache context.
*/
public function testCachePerRole(): void {
\Drupal::state()->set('block_test.cache_contexts', ['user.roles']);
// Enable our test block. Set some content for it to display.
$current_content = $this->randomMachineName();
\Drupal::keyValue('block_test')->set('content', $current_content);
$this->drupalLogin($this->normalUser);
$this->drupalGet('');
$this->assertSession()->pageTextContains($current_content);
// Change the content, but the cached copy should still be served.
$old_content = $current_content;
$current_content = $this->randomMachineName();
\Drupal::keyValue('block_test')->set('content', $current_content);
$this->drupalGet('');
$this->assertSession()->pageTextContains($old_content);
// Clear the cache and verify that the stale data is no longer there.
Cache::invalidateTags(['block_view']);
$this->drupalGet('');
$this->assertSession()->pageTextNotContains($old_content);
// Fresh block content is displayed after clearing the cache.
$this->assertSession()->pageTextContains($current_content);
// Test whether the cached data is served for the correct users.
$old_content = $current_content;
$current_content = $this->randomMachineName();
\Drupal::keyValue('block_test')->set('content', $current_content);
$this->drupalLogout();
$this->drupalGet('');
// Anonymous user does not see content cached per-role for normal user.
$this->assertSession()->pageTextNotContains($old_content);
// User with the same roles sees per-role cached content.
$this->drupalLogin($this->normalUserAlt);
$this->drupalGet('');
$this->assertSession()->pageTextContains($old_content);
// Admin user does not see content cached per-role for normal user.
$this->drupalLogin($this->adminUser);
$this->drupalGet('');
$this->assertSession()->pageTextNotContains($old_content);
// Block is served from the per-role cache.
$this->drupalLogin($this->normalUser);
$this->drupalGet('');
$this->assertSession()->pageTextContains($old_content);
}
/**
* Tests a cacheable block without any additional cache context.
*/
public function testCachePermissions(): void {
// user.permissions is a required context, so a user with different
// permissions will see a different version of the block.
\Drupal::state()->set('block_test.cache_contexts', []);
$current_content = $this->randomMachineName();
\Drupal::keyValue('block_test')->set('content', $current_content);
$this->drupalGet('');
$this->assertSession()->pageTextContains($current_content);
$old_content = $current_content;
$current_content = $this->randomMachineName();
\Drupal::keyValue('block_test')->set('content', $current_content);
// Block content served from cache.
$this->drupalGet('user');
$this->assertSession()->pageTextContains($old_content);
// Block content not served from cache.
$this->drupalLogout();
$this->drupalGet('user');
$this->assertSession()->pageTextContains($current_content);
}
/**
* Tests non-cacheable block.
*/
public function testNoCache(): void {
\Drupal::state()->set('block_test.cache_max_age', 0);
$current_content = $this->randomMachineName();
\Drupal::keyValue('block_test')->set('content', $current_content);
// If max_age = 0 has no effect, the next request would be cached.
$this->drupalGet('');
$this->assertSession()->pageTextContains($current_content);
// A cached copy should not be served.
$current_content = $this->randomMachineName();
\Drupal::keyValue('block_test')->set('content', $current_content);
$this->drupalGet('');
// Maximum age of zero prevents blocks from being cached.
$this->assertSession()->pageTextContains($current_content);
}
/**
* Tests "user" cache context.
*/
public function testCachePerUser(): void {
\Drupal::state()->set('block_test.cache_contexts', ['user']);
$current_content = $this->randomMachineName();
\Drupal::keyValue('block_test')->set('content', $current_content);
$this->drupalLogin($this->normalUser);
$this->drupalGet('');
$this->assertSession()->pageTextContains($current_content);
$old_content = $current_content;
$current_content = $this->randomMachineName();
\Drupal::keyValue('block_test')->set('content', $current_content);
// Block is served from per-user cache.
$this->drupalGet('');
$this->assertSession()->pageTextContains($old_content);
// Per-user block cache is not served for other users.
$this->drupalLogin($this->normalUserAlt);
$this->drupalGet('');
$this->assertSession()->pageTextContains($current_content);
// Per-user block cache is persistent.
$this->drupalLogin($this->normalUser);
$this->drupalGet('');
$this->assertSession()->pageTextContains($old_content);
}
/**
* Tests "url" cache context.
*/
public function testCachePerPage(): void {
\Drupal::state()->set('block_test.cache_contexts', ['url']);
$current_content = $this->randomMachineName();
\Drupal::keyValue('block_test')->set('content', $current_content);
$this->drupalGet('test-page');
$this->assertSession()->pageTextContains($current_content);
$old_content = $current_content;
$current_content = $this->randomMachineName();
\Drupal::keyValue('block_test')->set('content', $current_content);
$this->drupalGet('user');
$this->assertSession()->statusCodeEquals(200);
// Verify that block content cached for the test page does not show up
// for the user page.
$this->assertSession()->pageTextNotContains($old_content);
$this->drupalGet('test-page');
$this->assertSession()->statusCodeEquals(200);
// Verify that the block content is cached for the test page.
$this->assertSession()->pageTextContains($old_content);
}
}

View File

@ -0,0 +1,70 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\block\Functional;
use Drupal\Tests\BrowserTestBase;
/**
* Tests the block demo page with admin themes.
*
* @group block
*/
class BlockDemoTest extends BrowserTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = ['block'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* Check for the accessibility of the admin block demo page.
*/
public function testBlockDemo(): void {
// Create administrative user.
$admin_user = $this->drupalCreateUser([
'administer blocks',
'administer themes',
]);
$this->drupalLogin($admin_user);
// Confirm we have access to the block demo page for the default theme.
$config = $this->container->get('config.factory')->get('system.theme');
$default_theme = $config->get('default');
$this->drupalGet('admin/structure/block/demo/' . $default_theme);
$this->assertSession()->statusCodeEquals(200);
$this->assertSession()->linkByHrefExists('admin/structure/block');
$this->assertSession()->linkByHrefNotExists('admin/structure/block/list/' . $default_theme);
// All available themes in core.
$available_themes = [
'olivero',
'claro',
'stark',
];
// All available themes minute minus the default theme.
$themes = array_diff($available_themes, [$default_theme]);
foreach ($themes as $theme) {
// Install theme.
$this->container->get('theme_installer')->install([$theme]);
// Confirm access to the block demo page for the theme.
$this->drupalGet('admin/structure/block/demo/' . $theme);
$this->assertSession()->statusCodeEquals(200);
// Confirm existence of link for "Exit block region demonstration".
$this->assertSession()->linkByHrefExists('admin/structure/block/list/' . $theme);
}
// Confirm access to the block demo page is denied for an invalid theme.
$this->drupalGet('admin/structure/block/demo/invalid_theme');
$this->assertSession()->statusCodeEquals(403);
}
}

View File

@ -0,0 +1,77 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\block\Functional;
use Drupal\Component\Utility\Crypt;
use Drupal\Tests\BrowserTestBase;
/**
* Tests form in block caching.
*
* @group block
*/
class BlockFormInBlockTest extends BrowserTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = ['block', 'block_test', 'test_page_test'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
// Enable our test block.
$this->drupalPlaceBlock('test_form_in_block');
}
/**
* Tests to see if form in block's redirect isn't cached.
*/
public function testCachePerPage(): void {
$form_values = ['email' => 'test@example.com'];
// Go to "test-page" and test if the block is enabled.
$this->drupalGet('test-page');
$this->assertSession()->statusCodeEquals(200);
$this->assertSession()->pageTextContains('Your .com email address.');
// Make sure that we're currently still on /test-page after submitting the
// form.
$this->submitForm($form_values, 'Submit');
$this->assertSession()->addressEquals('test-page');
$this->assertSession()->pageTextContains('Your email address is test@example.com');
// Go to a different page and see if the block is enabled there as well.
$this->drupalGet('test-render-title');
$this->assertSession()->statusCodeEquals(200);
$this->assertSession()->pageTextContains('Your .com email address.');
// Make sure that submitting the form didn't redirect us to the first page
// we submitted the form from after submitting the form from
// /test-render-title.
$this->submitForm($form_values, 'Submit');
$this->assertSession()->addressEquals('test-render-title');
$this->assertSession()->pageTextContains('Your email address is test@example.com');
}
/**
* Tests the actual placeholders.
*/
public function testPlaceholders(): void {
$this->drupalGet('test-multiple-forms');
$placeholder = 'form_action_' . Crypt::hashBase64('Drupal\Core\Form\FormBuilder::prepareForm');
$this->assertSession()->pageTextContains('Form action: ' . $placeholder);
}
}

View File

@ -0,0 +1,81 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\block\Functional;
use Drupal\Tests\BrowserTestBase;
/**
* Tests that blocks are not added to hidden regions on theme installation.
*
* @group block
*/
class BlockHiddenRegionTest extends BrowserTestBase {
/**
* An administrative user to configure the test environment.
*
* @var \Drupal\user\Entity\User|false
*/
protected $adminUser;
/**
* {@inheritdoc}
*/
protected static $modules = ['block', 'block_test', 'search'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
// Create administrative user.
$this->adminUser = $this->drupalCreateUser([
'administer blocks',
'administer themes',
'search content',
]);
$this->drupalLogin($this->adminUser);
$this->drupalPlaceBlock('search_form_block');
$this->drupalPlaceBlock('local_tasks_block');
}
/**
* Tests that hidden regions do not inherit blocks when a theme is installed.
*/
public function testBlockNotInHiddenRegion(): void {
// Ensure that the search form block is displayed.
$this->drupalGet('');
$this->assertSession()->pageTextContains('Search');
// Install "block_test_theme" and set it as the default theme.
$theme = 'block_test_theme';
// We need to install a non-hidden theme so that there is more than one
// local task.
\Drupal::service('theme_installer')->install([$theme, 'stark']);
$this->config('system.theme')
->set('default', $theme)
->save();
// Installing a theme will cause the kernel terminate event to rebuild the
// router. Simulate that here.
\Drupal::service('router.builder')->rebuildIfNeeded();
// Ensure that "block_test_theme" is set as the default theme.
$this->drupalGet('admin/structure/block');
$this->assertSession()->pageTextContains('Block test theme');
// Ensure that the search form block is displayed.
$this->drupalGet('');
$this->assertSession()->pageTextContains('Search');
}
}

View File

@ -0,0 +1,58 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\block\Functional;
use Drupal\Tests\BrowserTestBase;
/**
* Tests for Block module regarding hook_entity_operations_alter().
*
* @group block
*/
class BlockHookOperationTest extends BrowserTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = ['block', 'entity_test'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$permissions = [
'administer blocks',
];
// Create and log in user.
$admin_user = $this->drupalCreateUser($permissions);
$this->drupalLogin($admin_user);
}
/**
* Tests the block list to see if the test_operation link is added.
*/
public function testBlockOperationAlter(): void {
// Add a test block, any block will do.
// Set the machine name so the test_operation link can be built later.
$block_id = $this->randomMachineName(16);
$this->drupalPlaceBlock('system_powered_by_block', ['id' => $block_id]);
// Get the Block listing.
$this->drupalGet('admin/structure/block');
$test_operation_link = 'admin/structure/block/manage/' . $block_id . '/test_operation';
// Test if the test_operation link is on the page.
$this->assertSession()->linkByHrefExists($test_operation_link);
}
}

View File

@ -0,0 +1,60 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\block\Functional;
use Drupal\Tests\BrowserTestBase;
/**
* Tests block HTML ID validity.
*
* @group block
*/
class BlockHtmlTest extends BrowserTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = ['block', 'block_test'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->drupalLogin($this->drupalCreateUser([
'administer blocks',
'access administration pages',
]));
// Enable the test_html block, to test HTML ID and attributes.
\Drupal::keyValue('block_test')->set('attributes', ['data-custom-attribute' => 'foo']);
\Drupal::keyValue('block_test')->set('content', $this->randomMachineName());
$this->drupalPlaceBlock('test_html', ['id' => 'test_html_block']);
// Enable a menu block, to test more complicated HTML.
$this->drupalPlaceBlock('system_menu_block:admin', ['id' => 'test_menu_block']);
}
/**
* Tests for valid HTML for a block.
*/
public function testHtml(): void {
$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->assertSession()->elementExists('xpath', '//div[@id="block-test-html-block" and @data-custom-attribute="foo"]');
// Ensure expected markup for a menu block.
$this->assertSession()->elementExists('xpath', '//nav[@id="block-test-menu-block"]/ul/li');
}
}

View File

@ -0,0 +1,42 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\block\Functional;
use Drupal\Tests\BrowserTestBase;
/**
* Tests block module's installation.
*
* @group block
*/
class BlockInstallTest extends BrowserTestBase {
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* Tests cache tag invalidation after installing the block module.
*/
public function testCacheTagInvalidationUponInstallation(): void {
// Warm the page cache.
$this->drupalGet('');
$this->assertSession()->pageTextNotContains('Powered by Drupal');
$this->assertSession()->responseHeaderNotContains('X-Drupal-Cache-Tags', 'config:block_list');
// Install the block module, and place the "Powered by Drupal" block.
$this->container->get('module_installer')->install(['block', 'shortcut']);
$this->rebuildContainer();
$this->drupalPlaceBlock('system_powered_by_block');
// Check the same page, block.module's hook_install() should have
// invalidated the 'rendered' cache tag to make blocks show up.
$this->drupalGet('');
$this->assertSession()->responseHeaderContains('X-Drupal-Cache-Tags', 'config:block_list');
$this->assertSession()->pageTextContains('Powered by Drupal');
}
}

View File

@ -0,0 +1,83 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\block\Functional;
use Drupal\Tests\BrowserTestBase;
use Drupal\block\Entity\Block;
/**
* Tests that blocks assigned to invalid regions are disabled with a warning.
*
* @group block
*/
class BlockInvalidRegionTest extends BrowserTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = ['block', 'block_test'];
/**
* {@inheritdoc}
*/
protected static $configSchemaCheckerExclusions = [
// This block is intentionally put in an invalid region, so it will violate
// config schema.
// @see ::testBlockInvalidRegion()
'block.block.invalid_region',
];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
// Create an admin user.
$admin_user = $this->drupalCreateUser([
'administer site configuration',
'access administration pages',
'administer blocks',
]);
$this->drupalLogin($admin_user);
}
/**
* Tests that blocks assigned to invalid regions work correctly.
*/
public function testBlockInInvalidRegion(): void {
// Enable a test block and place it in an invalid region.
$block = $this->drupalPlaceBlock('test_html', ['id' => 'invalid_region']);
\Drupal::configFactory()->getEditable('block.block.' . $block->id())->set('region', 'invalid_region')->save();
$block = Block::load($block->id());
$warning_message = 'The block ' . $block->id() . ' was assigned to the invalid region invalid_region and has been disabled.';
// Clearing the cache should disable the test block placed in the invalid
// region.
$this->drupalGet('admin/config/development/performance');
$this->submitForm([], 'Clear all caches');
$this->assertSession()->statusMessageContains($warning_message, 'warning');
// Clear the cache to check if the warning message is not triggered.
$this->drupalGet('admin/config/development/performance');
$this->submitForm([], 'Clear all caches');
$this->assertSession()->statusMessageNotContains($warning_message, 'warning');
// Place disabled test block in the invalid region of the default theme.
\Drupal::configFactory()->getEditable('block.block.' . $block->id())->set('region', 'invalid_region')->save();
$block = Block::load($block->id());
// Clear the cache to check if the warning message is not triggered.
$this->drupalGet('admin/config/development/performance');
$this->submitForm([], 'Clear all caches');
$this->assertSession()->statusMessageNotContains($warning_message, 'warning');
}
}

View File

@ -0,0 +1,85 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\block\Functional;
use Drupal\language\Entity\ConfigurableLanguage;
use Drupal\Tests\BrowserTestBase;
/**
* Tests display of menu blocks with multiple languages.
*
* @group block
*/
class BlockLanguageCacheTest extends BrowserTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = ['block', 'language', 'menu_ui'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* List of langcodes.
*
* @var array
*/
protected $langcodes = [];
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
// Create test languages.
$this->langcodes = [ConfigurableLanguage::load('en')];
for ($i = 1; $i < 3; ++$i) {
$language = ConfigurableLanguage::create([
'id' => 'l' . $i,
'label' => $this->randomString(),
]);
$language->save();
$this->langcodes[$i] = $language;
}
}
/**
* Creates a block in a language, check blocks page in all languages.
*/
public function testBlockLinks(): void {
// Create admin user to be able to access block admin.
$admin_user = $this->drupalCreateUser([
'administer blocks',
'access administration pages',
'administer menu',
]);
$this->drupalLogin($admin_user);
// Create the block cache for all languages.
foreach ($this->langcodes as $langcode) {
$this->drupalGet('admin/structure/block', ['language' => $langcode]);
$this->clickLink('Place block');
}
// Create a menu in the default language.
$edit['label'] = $this->randomMachineName();
$edit['id'] = mb_strtolower($edit['label']);
$this->drupalGet('admin/structure/menu/add');
$this->submitForm($edit, 'Save');
$this->assertSession()->pageTextContains('Menu ' . $edit['label'] . ' has been added.');
// Check that the block is listed for all languages.
foreach ($this->langcodes as $langcode) {
$this->drupalGet('admin/structure/block', ['language' => $langcode]);
$this->clickLink('Place block');
$this->assertSession()->pageTextContains($edit['label']);
}
}
}

View File

@ -0,0 +1,224 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\block\Functional;
use Drupal\Tests\BrowserTestBase;
use Drupal\block\Entity\Block;
/**
* Tests per-language block configuration.
*
* @group block
*/
class BlockLanguageTest extends BrowserTestBase {
/**
* An administrative user to configure the test environment.
*
* @var \Drupal\user\Entity\User|false
*/
protected $adminUser;
/**
* {@inheritdoc}
*/
protected static $modules = ['language', 'block', 'content_translation', 'node'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->adminUser = $this->drupalCreateUser([
'administer blocks',
'administer languages',
]);
$this->drupalLogin($this->adminUser);
// Add predefined language.
$edit = [
'predefined_langcode' => 'fr',
];
$this->drupalGet('admin/config/regional/language/add');
$this->submitForm($edit, 'Add language');
// Verify that language was added successfully.
$this->assertSession()->pageTextContains('French');
// Set path prefixes for both languages.
$this->config('language.negotiation')->set('url', [
'source' => 'path_prefix',
'prefixes' => [
'en' => 'en',
'fr' => 'fr',
],
])->save();
$this->drupalCreateContentType(['type' => 'page']);
$this->drupalCreateNode();
}
/**
* Tests the visibility settings for the blocks based on language.
*/
public function testLanguageBlockVisibility(): void {
// Check if the visibility setting is available.
$default_theme = $this->config('system.theme')->get('default');
$this->drupalGet('admin/structure/block/add/system_powered_by_block/' . $default_theme);
// Ensure that the language visibility field is visible without a type
// setting.
$this->assertSession()->fieldExists('visibility[language][langcodes][en]');
$this->assertSession()->fieldNotExists('visibility[language][context_mapping][language]');
// Enable a standard block and set the visibility setting for one language.
$edit = [
'visibility[language][langcodes][en]' => TRUE,
'id' => $this->randomMachineName(8),
'region' => 'sidebar_first',
];
$this->drupalGet('admin/structure/block/add/system_powered_by_block/' . $default_theme);
$this->submitForm($edit, 'Save block');
// Change the default language.
$edit = [
'site_default_language' => 'fr',
];
$this->drupalGet('admin/config/regional/language');
$this->submitForm($edit, 'Save configuration');
// Check that a page has a block.
$this->drupalGet('en');
$this->assertSession()->statusCodeEquals(200);
$this->assertSession()->pageTextContains('Powered by Drupal');
// Check that a page doesn't has a block for the current language anymore.
$this->drupalGet('fr');
$this->assertSession()->statusCodeEquals(200);
$this->assertSession()->pageTextNotContains('Powered by Drupal');
}
/**
* Tests if the visibility settings are removed if the language is deleted.
*/
public function testLanguageBlockVisibilityLanguageDelete(): void {
// Enable a standard block and set the visibility setting for one language.
$edit = [
'visibility' => [
'language' => [
'langcodes' => [
'fr' => 'fr',
],
'context_mapping' => ['language' => '@language.current_language_context:language_interface'],
],
],
];
$block = $this->drupalPlaceBlock('system_powered_by_block', $edit);
// Check that we have the language in config after saving the setting.
$visibility = $block->getVisibility();
$this->assertEquals('fr', $visibility['language']['langcodes']['fr'], 'Language is set in the block configuration.');
// Delete the language.
$this->drupalGet('admin/config/regional/language/delete/fr');
$this->submitForm([], 'Delete');
// Check that the language is no longer stored in the configuration after
// it is deleted.
$block = Block::load($block->id());
$visibility = $block->getVisibility();
$this->assertArrayNotHasKey('language', $visibility, 'Language is no longer not set in the block configuration after deleting the block.');
// Ensure that the block visibility for language is gone from the UI.
$this->drupalGet('admin/structure/block');
$this->clickLink('Configure');
$this->assertSession()->elementNotExists('xpath', '//details[@id="edit-visibility-language"]');
}
/**
* Tests block language visibility with different language types.
*/
public function testMultipleLanguageTypes(): void {
// Customize content language detection to be different from interface
// language detection.
$edit = [
// Interface language detection: only using session.
'language_interface[enabled][language-url]' => FALSE,
'language_interface[enabled][language-session]' => TRUE,
// Content language detection: only using URL.
'language_content[configurable]' => TRUE,
'language_content[enabled][language-url]' => TRUE,
'language_content[enabled][language-interface]' => FALSE,
];
$this->drupalGet('admin/config/regional/language/detection');
$this->submitForm($edit, 'Save settings');
// Check if the visibility setting is available with a type setting.
$default_theme = $this->config('system.theme')->get('default');
$this->drupalGet('admin/structure/block/add/system_powered_by_block/' . $default_theme);
$this->assertSession()->fieldExists('visibility[language][langcodes][en]');
$this->assertSession()->fieldExists('visibility[language][context_mapping][language]');
// Enable a standard block and set visibility to French only.
$block_id = $this->randomMachineName(8);
$edit = [
'visibility[language][context_mapping][language]' => '@language.current_language_context:language_interface',
'visibility[language][langcodes][fr]' => TRUE,
'id' => $block_id,
'region' => 'sidebar_first',
];
$this->drupalGet('admin/structure/block/add/system_powered_by_block/' . $default_theme);
$this->submitForm($edit, 'Save block');
// Interface negotiation depends on request arguments.
$this->drupalGet('node/1', ['query' => ['language' => 'en']]);
$this->assertSession()->statusCodeEquals(200);
$this->assertSession()->pageTextNotContains('Powered by Drupal');
$this->drupalGet('node/1', ['query' => ['language' => 'fr']]);
$this->assertSession()->pageTextContains('Powered by Drupal');
// Log in again in order to clear the interface language stored in the
// session.
$this->drupalLogout();
$this->drupalLogin($this->adminUser);
// Content language does not depend on session/request arguments.
// It will fall back on English (site default) and not display the block.
$this->drupalGet('en');
$this->assertSession()->statusCodeEquals(200);
$this->assertSession()->pageTextNotContains('Powered by Drupal');
$this->drupalGet('fr');
$this->assertSession()->statusCodeEquals(200);
$this->assertSession()->pageTextNotContains('Powered by Drupal');
// Change visibility to now depend on content language for this block.
$edit = [
'visibility[language][context_mapping][language]' => '@language.current_language_context:language_content',
];
$this->drupalGet('admin/structure/block/manage/' . $block_id);
$this->submitForm($edit, 'Save block');
// Content language negotiation does not depend on request arguments.
// It will fall back on English (site default) and not display the block.
$this->drupalGet('node/1', ['query' => ['language' => 'en']]);
$this->assertSession()->statusCodeEquals(200);
$this->assertSession()->pageTextNotContains('Powered by Drupal');
$this->drupalGet('node/1', ['query' => ['language' => 'fr']]);
$this->assertSession()->statusCodeEquals(200);
$this->assertSession()->pageTextNotContains('Powered by Drupal');
// Content language negotiation depends on path prefix.
$this->drupalGet('en');
$this->assertSession()->pageTextNotContains('Powered by Drupal');
$this->drupalGet('fr');
$this->assertSession()->pageTextContains('Powered by Drupal');
}
}

View File

@ -0,0 +1,92 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\block\Functional;
use Drupal\Component\Utility\Html;
use Drupal\Tests\BrowserTestBase;
/**
* Tests blocks are being rendered in order by weight.
*
* @group block
*/
class BlockRenderOrderTest extends BrowserTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = ['node', 'block'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
// Create a test user.
$end_user = $this->drupalCreateUser([
'access content',
]);
$this->drupalLogin($end_user);
}
/**
* Tests the render order of the blocks.
*/
public function testBlockRenderOrder(): void {
// Enable test blocks and place them in the same region.
$region = 'header';
$test_blocks = [
'stark_powered' => [
'weight' => -3,
'id' => 'stark_powered',
'label' => 'Test block A',
],
'stark_by' => [
'weight' => 3,
'id' => 'stark_by',
'label' => 'Test block C',
],
'stark_drupal' => [
'weight' => 3,
'id' => 'stark_drupal',
'label' => 'Test block B',
],
];
// Place the test blocks.
foreach ($test_blocks as $test_block) {
$this->drupalPlaceBlock('system_powered_by_block', [
'label' => $test_block['label'],
'region' => $region,
'weight' => $test_block['weight'],
'id' => $test_block['id'],
]);
}
$this->drupalGet('');
$test_content = $this->getSession()->getPage()->getContent();
$controller = $this->container->get('entity_type.manager')->getStorage('block');
foreach ($controller->loadMultiple() as $return_block) {
$id = $return_block->id();
if ($return_block_weight = $return_block->getWeight()) {
$this->assertSame((int) $test_blocks[$id]['weight'], $return_block_weight, 'Block weight is set as "' . $return_block_weight . '" for ' . $id . ' block.');
$position[$id] = strpos($test_content, Html::getClass('block-' . $test_blocks[$id]['id']));
}
}
// Verify that blocks with different weight are rendered in the correct
// order.
$this->assertLessThan($position['stark_by'], $position['stark_powered']);
// Verify that blocks with identical weight are rendered in alphabetical
// order.
$this->assertLessThan($position['stark_by'], $position['stark_drupal']);
}
}

Some files were not shown because too many files have changed in this diff Show More