Commit f630b510 authored by alexpott's avatar alexpott

Issue #1927608 by EclipseGc, tim.plunkett, effulgentsia: Remove the tight...

Issue #1927608 by EclipseGc, tim.plunkett, effulgentsia: Remove the tight coupling between Block Plugins and Block Entities.
parent f5ec8253
......@@ -547,7 +547,7 @@ function aggregator_filter_xss($value) {
* Implements hook_preprocess_HOOK() for block.tpl.php.
*/
function aggregator_preprocess_block(&$variables) {
if ($variables['block']->module == 'aggregator') {
if ($variables['configuration']['module'] == 'aggregator') {
$variables['attributes']['role'] = 'complementary';
}
}
......@@ -34,9 +34,9 @@ public function settings() {
}
/**
* Overrides \Drupal\block\BlockBase::blockAccess().
* Overrides \Drupal\block\BlockBase::access().
*/
public function blockAccess() {
public function access() {
// Only grant access to users with the 'access news feeds' permission.
return user_access('access news feeds');
}
......
......@@ -34,9 +34,9 @@ public function settings() {
}
/**
* Overrides \Drupal\block\BlockBase::blockAccess().
* Overrides \Drupal\block\BlockBase::access().
*/
public function blockAccess() {
public function access() {
// Only grant access to users with the 'access news feeds' permission.
return user_access('access news feeds');
}
......
......@@ -50,7 +50,7 @@ public function testBlockLinks() {
));
$this->drupalLogin($admin_user);
$block = $this->drupalPlaceBlock("aggregator_feed_block:{$feed->id()}", array('label' => 'feed-' . $feed->label()), array('block_count' => 2));
$block = $this->drupalPlaceBlock("aggregator_feed_block:{$feed->id()}", array('label' => 'feed-' . $feed->label(), 'block_count' => 2));
// Confirm that the block is now being displayed on pages.
$this->drupalGet('test-page');
......
......@@ -11,113 +11,90 @@
*/
/**
* Perform alterations to the content of a block.
* Alter the result of \Drupal\block\BlockBase::build().
*
* This hook allows you to modify any data returned by hook_block_view().
* 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.
*
* Note that instead of hook_block_view_alter(), which is called for all blocks,
* you can also use hook_block_view_ID_alter() to alter a specific block, or
* hook_block_view_NAME_alter() to alter a specific block instance.
* 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.tpl.php. See drupal_render() and theme() documentation respectively
* 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\block\BlockPluginInterface $block
* The block instance.
* The block plugin instance.
*
* @see hook_block_view_ID_alter()
* @see hook_block_view_NAME_alter()
* @see hook_block_view_BASE_BLOCK_ID_alter()
*/
function hook_block_view_alter(array &$build, \Drupal\block\Plugin\Core\Entity\Block $block) {
function hook_block_view_alter(array &$build, \Drupal\block\BlockPluginInterface $block) {
// Remove the contextual links on all blocks that provide them.
if (is_array($build) && isset($build['#contextual_links'])) {
if (isset($build['#contextual_links'])) {
unset($build['#contextual_links']);
}
// Add a theme wrapper function defined by the current module to all blocks
// provided by the "somemodule" module.
if (is_array($build) && $block instanceof SomeBlockClass) {
$build['#theme_wrappers'][] = 'mymodule_special_block';
}
}
/**
* Perform alterations to a specific block.
* Provide a block plugin specific block_view alteration.
*
* Modules can implement hook_block_view_ID_alter() to modify a specific block,
* rather than implementing hook_block_view_alter().
* 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\block\SystemPoweredByBlock block, this would
* be 'system_powered_by_block' as per that class's annotation. And for the
* \Drupal\system\Plugin\block\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\block\BlockPluginInterface $block
* The block instance.
*
* @todo Add a more specific example of a block ID, and illustrate how this is
* different from hook_block_view_NAME_alter().
* The block plugin instance.
*
* @see hook_block_view_alter()
* @see hook_block_view_NAME_alter()
*/
function hook_block_view_ID_alter(array &$build, \Drupal\block\BlockPluginInterface $block) {
// This code will only run for a specific block. For example, if ID
// in the function definition above is set to "someid", the code
// will only run on the "someid" block.
// Change the title of the "someid" block.
function hook_block_view_BASE_BLOCK_ID_alter(array &$build, \Drupal\block\BlockPluginInterface $block) {
// Change the title of the specific block.
$build['#title'] = t('New title of the block');
}
/**
* Perform alterations to a specific block instance.
*
* Modules can implement hook_block_view_NAME_alter() to modify a specific block
* instance, rather than implementing hook_block_view_alter().
* Control access to a block instance.
*
* @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\block\BlockPluginInterface $block
* The block instance.
*
* @todo NAME is ambiguous, and so is the example here. Use a more specific
* example to illustrate what the block instance name will look like, and
* also illustrate how it is different from hook_block_view_ID().
*
* @see hook_block_view_alter()
* @see hook_block_view_ID_alter()
*/
function hook_block_view_NAME_alter(array &$build, \Drupal\block\BlockPluginInterface $block) {
// This code will only run for a specific block instance. For example, if NAME
// in the function definition above is set to "someid", the code will only run
// on the "someid" block.
// Change the title of the "someid" block.
$build['#title'] = t('New title of the block');
}
/**
* Define access for a specific block instance.
*
* This hook is invoked by the access methods of the block plugin system and
* should be used to alter the block access rules defined by a module from
* another module.
* 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\Plugin\Core\Entity\Block $block
* The block instance.
*
* @return bool
* TRUE will allow access whereas FALSE will deny access to the block.
*
* @see \Drupal\block\BlockBase::access()
* @see \Drupal\block\BlockBase::blockAccess()
* @param string $operation
* The operation to be performed, e.g., 'view', 'create', 'delete', 'update'.
* @param \Drupal\user\Plugin\Core\Entity\User $account
* The user object to perform the access check operation on.
* @param string $langcode
* The language code to perform the access check operation on.
*
* @return bool|NULL
* FALSE denies access. TRUE allows access unless another module returns
* FALSE. If all modules return NULL, then default access rules from
* \Drupal\block\BlockAccessController::checkAccess() are used.
*
* @see \Drupal\Core\Entity\EntityAccessController::access()
* @see \Drupal\block\BlockAccessController::checkAccess()
*/
function hook_block_access(\Drupal\block\Plugin\Core\Entity\Block $block) {
function hook_block_access(\Drupal\block\Plugin\Core\Entity\Block $block, $operation, \Drupal\user\Plugin\Core\Entity\User $account, $langcode) {
// Example code that would prevent displaying the 'Powered by Drupal' block in
// a region different than the footer.
if ($block->get('plugin') == 'system_powered_by_block' && $block->get('region') != 'footer') {
if ($operation == 'view' && $block->get('plugin') == 'system_powered_by_block' && $block->get('region') != 'footer') {
return FALSE;
}
}
......
......@@ -331,10 +331,9 @@ function _block_get_renderable_region($list = array()) {
$build[$key] = array(
'#block' => $block,
'#weight' => $block->get('weight'),
'#theme_wrappers' => array('block'),
'#pre_render' => array('_block_get_renderable_block'),
'#cache' => array(
'keys' => array($id, $block->get('module')),
'keys' => array($id, $settings['module']),
'granularity' => $settings['cache'],
'bin' => 'block',
'tags' => array('content' => TRUE),
......@@ -535,22 +534,17 @@ function block_rebuild() {
function template_preprocess_block(&$variables) {
$block_counter = &drupal_static(__FUNCTION__, array());
$variables['block'] = (object) $variables['elements']['#block_config'];
// If the block title is configured to be hidden, set it to an empty string.
if (empty($variables['elements']['#block']->label_display)) {
$variables['block']->label_hidden = $variables['block']->label;
$variables['block']->label = '';
}
// Create the $content variable that templates expect.
$variables['content'] = $variables['elements']['#children'];
$variables['configuration'] = $variables['elements']['#configuration'];
$variables['plugin_id'] = $variables['elements']['#plugin_id'];
$variables['label'] = !empty($variables['configuration']['label_display']) ? $variables['configuration']['label'] : '';
$variables['content'] = $variables['elements']['content'];
$variables['attributes']['class'][] = drupal_html_class('block-' . $variables['block']->module);
$variables['attributes']['class'][] = drupal_html_class('block-' . $variables['configuration']['module']);
// Add default class for block content.
$variables['content_attributes']['class'][] = 'content';
$variables['theme_hook_suggestions'][] = 'block__' . $variables['block']->module;
$variables['theme_hook_suggestions'][] = 'block__' . $variables['configuration']['module'];
// 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
......@@ -562,7 +556,7 @@ function template_preprocess_block(&$variables) {
// We can safely explode on : because we know the Block plugin type manager
// enforces that delimiter for all derivatives.
$parts = explode(':', $variables['elements']['#block']->get('plugin'));
$parts = explode(':', $variables['plugin_id']);
$suggestion = 'block';
while ($part = array_shift($parts)) {
$variables['theme_hook_suggestions'][] = $suggestion .= '__' . strtr($part, '-', '_');
......
......@@ -7,7 +7,6 @@
use Drupal\custom_block\Plugin\Core\Entity\CustomBlockType;
use Drupal\custom_block\Plugin\Core\Entity\CustomBlock;
use Drupal\block\Plugin\Core\Entity\Block;
/**
* Implements hook_menu_local_tasks().
......
......@@ -18,26 +18,20 @@
* id = "custom_block",
* admin_label = @Translation("Custom block"),
* module = "custom_block",
* derivative = "Drupal\custom_block\Plugin\Derivative\CustomBlock",
* settings = {
* "status" = TRUE,
* "info" = "",
* "view_mode" = "full"
* }
* derivative = "Drupal\custom_block\Plugin\Derivative\CustomBlock"
* )
*/
class CustomBlockBlock extends BlockBase {
/**
* Overrides \Drupal\block\BlockBase::getConfig().
* Overrides \Drupal\block\BlockBase::settings().
*/
public function getConfig() {
$definition = $this->getDefinition();
$this->configuration = parent::getConfig();
$this->configuration['status'] = $definition['settings']['status'];
$this->configuration['info'] = $definition['settings']['info'];
$this->configuration['view_mode'] = $definition['settings']['view_mode'];
return $this->configuration;
public function settings() {
return array(
'status' => TRUE,
'info' => '',
'view_mode' => 'full',
);
}
/**
......
......@@ -43,9 +43,6 @@ public function getDerivativeDefinitions(array $base_plugin_definition) {
$custom_blocks = entity_load_multiple('custom_block');
foreach ($custom_blocks as $custom_block) {
$this->derivatives[$custom_block->uuid->value] = $base_plugin_definition;
$this->derivatives[$custom_block->uuid->value]['settings'] = array(
'info' => $custom_block->info->value,
) + $base_plugin_definition['settings'];
$this->derivatives[$custom_block->uuid->value]['admin_label'] = $custom_block->info->value;
}
return $this->derivatives;
......
......@@ -101,7 +101,7 @@ public function testBlockFields() {
// Place the block.
$instance = array(
'machine_name' => drupal_strtolower($edit['info']),
'label' => $edit['info'],
'settings[label]' => $edit['info'],
'region' => 'sidebar_first',
);
$this->drupalPost(NULL, $instance, t('Save block'));
......
......@@ -20,9 +20,78 @@ class BlockAccessController extends EntityAccessController {
* {@inheritdoc}
*/
protected function checkAccess(EntityInterface $entity, $operation, $langcode, User $account) {
if ($operation === 'view') {
return $entity->getPlugin()->access();
// Currently, only view access is implemented.
if ($operation != 'view') {
return FALSE;
}
// Deny access to disabled blocks.
if (!$entity->status()) {
return FALSE;
}
// If the plugin denies access, then deny access.
if (!$entity->getPlugin()->access()) {
return FALSE;
}
// Otherwise, check for other access restrictions.
global $user;
// User role access handling.
// If a block has no roles associated, it is displayed for every role.
// For blocks with roles associated, if none of the user's roles matches
// the settings from this block, access is denied.
$visibility = $entity->get('visibility');
if (!empty($visibility['role']['roles']) && !array_intersect(array_filter($visibility['role']['roles']), array_keys($user->roles))) {
// No match.
return FALSE;
}
// Page path handling.
// Limited visibility blocks must list at least one page.
if (!empty($visibility['path']['visibility']) && $visibility['path']['visibility'] == BLOCK_VISIBILITY_LISTED && empty($visibility['path']['pages'])) {
return FALSE;
}
// Match path if necessary.
if (!empty($visibility['path']['pages'])) {
// Assume there are no matches until one is found.
$page_match = FALSE;
// Convert path to lowercase. This allows comparison of the same path
// with different case. Ex: /Page, /page, /PAGE.
$pages = drupal_strtolower($visibility['path']['pages']);
if ($visibility['path']['visibility'] < BLOCK_VISIBILITY_PHP) {
// Compare the lowercase path alias (if any) and internal path.
$path = current_path();
$path_alias = drupal_strtolower(drupal_container()->get('path.alias_manager')->getPathAlias($path));
$page_match = drupal_match_path($path_alias, $pages) || (($path != $path_alias) && drupal_match_path($path, $pages));
// When $block->visibility has a value of 0
// (BLOCK_VISIBILITY_NOTLISTED), the block is displayed on all pages
// except those listed in $block->pages. When set to 1
// (BLOCK_VISIBILITY_LISTED), it is displayed only on those pages
// listed in $block->pages.
$page_match = !($visibility['path']['visibility'] xor $page_match);
}
elseif (module_exists('php')) {
$page_match = php_eval($visibility['path']['pages']);
}
// If there are page visibility restrictions and this page does not
// match, deny access.
if (!$page_match) {
return FALSE;
}
}
// Language visibility settings.
if (!empty($visibility['language']['langcodes']) && array_filter($visibility['language']['langcodes'])) {
if (empty($visibility['language']['langcodes'][language($visibility['language']['language_type'])->langcode])) {
return FALSE;
}
}
return TRUE;
}
}
......@@ -18,7 +18,163 @@ class BlockFormController extends EntityFormController {
* Overrides \Drupal\Core\Entity\EntityFormController::form().
*/
public function form(array $form, array &$form_state) {
return $this->entity->getPlugin()->form($form, $form_state);
$entity = $this->entity;
$form['#tree'] = TRUE;
$form['id'] = array(
'#type' => 'value',
'#value' => $entity->id(),
);
$form['settings'] = $entity->getPlugin()->form(array(), $form_state);
$form['machine_name'] = array(
'#type' => 'machine_name',
'#title' => t('Machine name'),
'#maxlength' => 64,
'#description' => t('A unique name to save this block configuration. Must be alpha-numeric and be underscore separated.'),
'#default_value' => $entity->id(),
'#machine_name' => array(
'exists' => 'block_load',
'replace_pattern' => '[^a-z0-9_.]+',
'source' => array('settings', 'label'),
),
'#required' => TRUE,
'#disabled' => !$entity->isNew(),
);
// Visibility settings.
$form['visibility'] = array(
'#type' => 'vertical_tabs',
'#title' => t('Visibility settings'),
'#attached' => array(
'js' => array(drupal_get_path('module', 'block') . '/block.js'),
),
'#tree' => TRUE,
'#weight' => 10,
'#parents' => array('visibility'),
);
// Per-path visibility.
$form['visibility']['path'] = array(
'#type' => 'details',
'#title' => t('Pages'),
'#collapsed' => TRUE,
'#group' => 'visibility',
'#weight' => 0,
);
// @todo remove this access check and inject it in some other way. In fact
// this entire visibility settings section probably needs a separate user
// interface in the near future.
$visibility = $entity->get('visibility');
$access = user_access('use PHP for settings');
if (!empty($visibility['path']['visibility']) && $visibility['path']['visibility'] == BLOCK_VISIBILITY_PHP && !$access) {
$form['visibility']['path']['visibility'] = array(
'#type' => 'value',
'#value' => BLOCK_VISIBILITY_PHP,
);
$form['visibility']['path']['pages'] = array(
'#type' => 'value',
'#value' => !empty($visibility['path']['pages']) ? $visibility['path']['pages'] : '',
);
}
else {
$options = array(
BLOCK_VISIBILITY_NOTLISTED => t('All pages except those listed'),
BLOCK_VISIBILITY_LISTED => t('Only the listed pages'),
);
$description = t("Specify pages by using their paths. Enter one path per line. The '*' character is a wildcard. Example paths are %user for the current user's page and %user-wildcard for every user page. %front is the front page.", array('%user' => 'user', '%user-wildcard' => 'user/*', '%front' => '<front>'));
if (module_exists('php') && $access) {
$options += array(BLOCK_VISIBILITY_PHP => t('Pages on which this PHP code returns <code>TRUE</code> (experts only)'));
$title = t('Pages or PHP code');
$description .= ' ' . t('If the PHP option is chosen, enter PHP code between %php. Note that executing incorrect PHP code can break your Drupal site.', array('%php' => '<?php ?>'));
}
else {
$title = t('Pages');
}
$form['visibility']['path']['visibility'] = array(
'#type' => 'radios',
'#title' => t('Show block on specific pages'),
'#options' => $options,
'#default_value' => !empty($visibility['path']['visibility']) ? $visibility['path']['visibility'] : BLOCK_VISIBILITY_NOTLISTED,
);
$form['visibility']['path']['pages'] = array(
'#type' => 'textarea',
'#title' => '<span class="element-invisible">' . $title . '</span>',
'#default_value' => !empty($visibility['path']['pages']) ? $visibility['path']['pages'] : '',
'#description' => $description,
);
}
// Configure the block visibility per language.
if (module_exists('language') && language_multilingual()) {
$configurable_language_types = language_types_get_configurable();
// Fetch languages.
$languages = language_list(LANGUAGE_ALL);
foreach ($languages as $language) {
// @todo $language->name is not wrapped with t(), it should be replaced
// by CMI translation implementation.
$langcodes_options[$language->langcode] = $language->name;
}
$form['visibility']['language'] = array(
'#type' => 'details',
'#title' => t('Languages'),
'#collapsed' => TRUE,
'#group' => 'visibility',
'#weight' => 5,
);
// If there are multiple configurable language types, let the user pick
// which one should be applied to this visibility setting. This way users
// can limit blocks by interface language or content language for exmaple.
$language_types = language_types_info();
$language_type_options = array();
foreach ($configurable_language_types as $type_key) {
$language_type_options[$type_key] = $language_types[$type_key]['name'];
}
$form['visibility']['language']['language_type'] = array(
'#type' => 'radios',
'#title' => t('Language type'),
'#options' => $language_type_options,
'#default_value' => !empty($visibility['language']['language_type']) ? $visibility['language']['language_type'] : $configurable_language_types[0],
'#access' => count($language_type_options) > 1,
);
$form['visibility']['language']['langcodes'] = array(
'#type' => 'checkboxes',
'#title' => t('Show this block only for specific languages'),
'#default_value' => !empty($visibility['language']['langcodes']) ? $visibility['language']['langcodes'] : array(),
'#options' => $langcodes_options,
'#description' => t('Show this block only for the selected language(s). If you select no languages, the block will be visibile in all languages.'),
);
}
// Per-role visibility.
$role_options = array_map('check_plain', user_role_names());
$form['visibility']['role'] = array(
'#type' => 'details',
'#title' => t('Roles'),
'#collapsed' => TRUE,
'#group' => 'visibility',
'#weight' => 10,
);
$form['visibility']['role']['roles'] = array(
'#type' => 'checkboxes',
'#title' => t('Show block for specific roles'),
'#default_value' => !empty($visibility['role']['roles']) ? $visibility['role']['roles'] : array(),
'#options' => $role_options,
'#description' => t('Show this block only for the selected role(s). If you select no roles, the block will be visible to all users.'),
);
// Region settings.
$form['region'] = array(
'#type' => 'select',
'#title' => t('Region'),
'#description' => t('Select the region where this block should be displayed.'),
'#default_value' => $entity->get('region'),
'#empty_value' => BLOCK_REGION_NONE,
'#options' => system_region_list($entity->get('theme'), REGIONS_VISIBLE),
);
return $form;
}
/**
......@@ -37,7 +193,21 @@ public function validate(array $form, array &$form_state) {
parent::validate($form, $form_state);
$entity = $this->entity;
$entity->getPlugin()->validate($form, $form_state);
if ($entity->isNew()) {
form_set_value($form['id'], $entity->get('theme') . '.' . $form_state['values']['machine_name'], $form_state);
}
if (!empty($form['machine_name']['#disabled'])) {
$config_id = explode('.', $form_state['values']['machine_name']);
$form_state['values']['machine_name'] = array_pop($config_id);
}
$form_state['values']['visibility']['role']['roles'] = array_filter($form_state['values']['visibility']['role']['roles']);
// The Block Entity form puts all block plugin form elements in the
// settings form element, so just pass that to the block for validation.
$settings = array(
'values' => &$form_state['values']['settings']
);
// Call the plugin validate handler.
$entity->getPlugin()->validate($form, $settings);
}
/**
......@@ -47,12 +217,20 @@ public function submit(array $form, array &$form_state) {
parent::submit($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.
$settings = array(
'values' => &$form_state['values']['settings']
);
// Call the plugin submit handler.
$entity->getPlugin()->submit($form, $form_state);
$entity->getPlugin()->submit($form, $settings);
// Save the settings of the plugin.
$entity->set('settings', $entity->getPlugin()->getConfig());
$entity->save();
drupal_set_message(t('The block configuration has been saved.'));
cache_invalidate_tags(array('content' => TRUE));
$form_state['redirect'] = 'admin/structure/block/list/block_plugin_ui:' . $entity->get('theme');
}
}
......@@ -206,6 +206,12 @@ public function submitForm(array &$form, array &$form_state) {
foreach ($entities as $entity_id => $entity) {
$entity->set('weight', $form_state['values']['blocks'][$entity_id]['weight']);
$entity->set('region', $form_state['values']['blocks'][$entity_id]['region']);
if ($entity->get('region') == BLOCK_REGION_NONE) {
$entity->disable();
}
else {
$entity->enable();
}
$entity->save();
}
drupal_set_message(t('The block settings have been updated.'));
......
<?php
/**
* @file
* Contains \Drupal\block\BlockPluginBag.
*/
namespace Drupal\block;
use Drupal\block\Plugin\Core\Entity\Block;
use Drupal\Component\Plugin\PluginBag;
use Drupal\Component\Plugin\PluginManagerInterface;
use Drupal\Component\Plugin\Exception\PluginException;
/**
* Provides a collection of block plugins.
*/
class BlockPluginBag extends PluginBag {
/**
* The manager used to instantiate the plugins.
*
* @var \Drupal\Component\Plugin\PluginManagerInterface
*/
protected $manager;
/**
* Constructs a BlockPluginBag object.
*
* @param \Drupal\Component\Plugin\PluginManagerInterface $manager
* The manager to be used for instantiating plugins.
* @param array $instance_ids
* The ids of the plugin instances with which we are dealing.
* @param \Drupal\block\Plugin\Core\Entity\Block $entity
* The Block entity that holds our configuration.
*/
public function __construct(PluginManagerInterface $manager, array $instance_ids, Block $entity) {
$this->manager