diff --git a/core/modules/block_content/block_content.install b/core/modules/block_content/block_content.install index e3da6bde881cddfc1e55d56150c2f6079a2c296f..fab15b4f88c2feb8d297525a5adb6659195ace6c 100644 --- a/core/modules/block_content/block_content.install +++ b/core/modules/block_content/block_content.install @@ -138,3 +138,19 @@ function block_content_update_8400() { $definition_update_manager->uninstallFieldStorageDefinition($content_translation_status); } } + +/** + * Add 'reusable' field to 'block_content' entities. + */ +function block_content_update_8600() { + $reusable = BaseFieldDefinition::create('boolean') + ->setLabel(t('Reusable')) + ->setDescription(t('A boolean indicating whether this block is reusable.')) + ->setTranslatable(FALSE) + ->setRevisionable(FALSE) + ->setDefaultValue(TRUE) + ->setInitialValue(TRUE); + + \Drupal::entityDefinitionUpdateManager() + ->installFieldStorageDefinition('reusable', 'block_content', 'block_content', $reusable); +} diff --git a/core/modules/block_content/block_content.module b/core/modules/block_content/block_content.module index 3adc979d9f08feb3b33d56f85ac87bbd5a46c5fb..98f0925ab96a9643dc3d9dfafc1a90de1e44d4b7 100644 --- a/core/modules/block_content/block_content.module +++ b/core/modules/block_content/block_content.module @@ -8,6 +8,9 @@ use Drupal\Core\Routing\RouteMatchInterface; use Drupal\field\Entity\FieldConfig; use Drupal\field\Entity\FieldStorageConfig; +use Drupal\Core\Database\Query\SelectInterface; +use Drupal\Core\Database\Query\AlterableInterface; +use Drupal\Core\Database\Query\ConditionInterface; /** * Implements hook_help(). @@ -105,3 +108,73 @@ function block_content_add_body_field($block_type_id, $label = 'Body') { return $field; } + +/** + * Implements hook_query_TAG_alter(). + * + * Alters any 'entity_reference' query where the entity type is + * 'block_content' and the query has the tag 'block_content_access'. + * + * These queries should only return reusable blocks unless a condition on + * 'reusable' is explicitly set. + * + * Block_content entities that are reusable should by default not be selectable + * as entity reference values. A module can still create an instance of + * \Drupal\Core\Entity\EntityReferenceSelection\SelectionInterface + * that will allow selection of non-reusable blocks by explicitly setting + * a condition on the 'reusable' field. + * + * @see \Drupal\block_content\BlockContentAccessControlHandler + */ +function block_content_query_entity_reference_alter(AlterableInterface $query) { + if ($query instanceof SelectInterface && $query->getMetaData('entity_type') === 'block_content' && $query->hasTag('block_content_access')) { + $data_table = \Drupal::entityTypeManager()->getDefinition('block_content')->getDataTable(); + if (array_key_exists($data_table, $query->getTables()) && !_block_content_has_reusable_condition($query->conditions(), $query->getTables())) { + $query->condition("$data_table.reusable", TRUE); + } + } +} + +/** + * Utility function to find nested conditions using the reusable field. + * + * @todo Replace this function with a call to the API in + * https://www.drupal.org/project/drupal/issues/2984930 + * + * @param array $condition + * The condition or condition group to check. + * @param array $tables + * The tables from the related select query. + * + * @see \Drupal\Core\Database\Query\SelectInterface::getTables + * + * @return bool + * Whether the conditions contain any condition using the reusable field. + */ +function _block_content_has_reusable_condition(array $condition, array $tables) { + // If this is a condition group call this function recursively for each nested + // condition until a condition is found that return TRUE. + if (isset($condition['#conjunction'])) { + foreach (array_filter($condition, 'is_array') as $nested_condition) { + if (_block_content_has_reusable_condition($nested_condition, $tables)) { + return TRUE; + } + } + return FALSE; + } + if (isset($condition['field'])) { + $field = $condition['field']; + if (is_object($field) && $field instanceof ConditionInterface) { + return _block_content_has_reusable_condition($field->conditions(), $tables); + } + $field_parts = explode('.', $field); + $data_table = \Drupal::entityTypeManager()->getDefinition('block_content')->getDataTable(); + foreach ($tables as $table) { + if ($table['table'] === $data_table && $field_parts[0] === $table['alias'] && $field_parts[1] === 'reusable') { + return TRUE; + } + } + + } + return FALSE; +} diff --git a/core/modules/block_content/block_content.post_update.php b/core/modules/block_content/block_content.post_update.php new file mode 100644 index 0000000000000000000000000000000000000000..6587658d49e2e8a15177e50eedda672f32c071a0 --- /dev/null +++ b/core/modules/block_content/block_content.post_update.php @@ -0,0 +1,46 @@ +getDefinition('block_content') + ->getDataTable(); + + \Drupal::classResolver(ConfigEntityUpdater::class)->update($sandbox, 'view', function ($view) use ($data_table) { + /** @var \Drupal\views\ViewEntityInterface $view */ + if ($view->get('base_table') != $data_table) { + return FALSE; + } + $save_view = FALSE; + $displays = $view->get('display'); + foreach ($displays as $display_name => &$display) { + // Update the default display and displays that have overridden filters. + if (!isset($display['display_options']['filters']['reusable']) && + ($display_name === 'default' || isset($display['display_options']['filters']))) { + $display['display_options']['filters']['reusable'] = [ + 'id' => 'reusable', + 'plugin_id' => 'boolean', + 'table' => $data_table, + 'field' => 'reusable', + 'value' => '1', + 'entity_type' => 'block_content', + 'entity_field' => 'reusable', + ]; + $save_view = TRUE; + } + } + if ($save_view) { + $view->set('display', $displays); + } + return $save_view; + }); +} diff --git a/core/modules/block_content/config/optional/views.view.block_content.yml b/core/modules/block_content/config/optional/views.view.block_content.yml index 1be5a0417c12d01b9e081da6b74b739cb90f9e81..2c008864f32e0a6cbdc448d93d18eaf4d86ea4c0 100644 --- a/core/modules/block_content/config/optional/views.view.block_content.yml +++ b/core/modules/block_content/config/optional/views.view.block_content.yml @@ -431,6 +431,44 @@ display: entity_type: block_content entity_field: type plugin_id: bundle + reusable: + id: reusable + table: block_content_field_data + field: reusable + relationship: none + group_type: group + admin_label: '' + operator: '=' + value: '1' + group: 1 + exposed: false + expose: + operator_id: '' + label: '' + description: '' + use_operator: false + operator: '' + identifier: '' + required: false + remember: false + multiple: false + remember_roles: + authenticated: authenticated + is_grouped: false + group_info: + label: '' + description: '' + identifier: '' + optional: true + widget: select + multiple: false + remember: false + default_group: All + default_group_multiple: { } + group_items: { } + entity_type: block_content + entity_field: reusable + plugin_id: boolean sorts: { } title: 'Custom block library' header: { } diff --git a/core/modules/block_content/src/Access/AccessGroupAnd.php b/core/modules/block_content/src/Access/AccessGroupAnd.php new file mode 100644 index 0000000000000000000000000000000000000000..7bb62bce0586111ffa28e4b463ca5372c47f6ae0 --- /dev/null +++ b/core/modules/block_content/src/Access/AccessGroupAnd.php @@ -0,0 +1,50 @@ +dependencies[] = $dependency; + return $this; + } + + /** + * {@inheritdoc} + */ + public function access($operation, AccountInterface $account = NULL, $return_as_object = FALSE) { + $access_result = AccessResult::neutral(); + foreach (array_slice($this->dependencies, 1) as $dependency) { + $access_result = $access_result->andIf($dependency->access($operation, $account, TRUE)); + } + return $return_as_object ? $access_result : $access_result->isAllowed(); + } + + /** + * {@inheritdoc} + */ + public function getDependencies() { + return $this->dependencies; + } + +} diff --git a/core/modules/block_content/src/Access/DependentAccessInterface.php b/core/modules/block_content/src/Access/DependentAccessInterface.php new file mode 100644 index 0000000000000000000000000000000000000000..bc6a6dcec694b58b0963a29de0cd83b9fc40a1d8 --- /dev/null +++ b/core/modules/block_content/src/Access/DependentAccessInterface.php @@ -0,0 +1,35 @@ +getAccessDependency()->access($op, $account, TRUE); + * @endcode + * + * @internal + */ +interface DependentAccessInterface { + + /** + * Gets the access dependency. + * + * @return \Drupal\Core\Access\AccessibleInterface|null + * The access dependency or NULL if none has been set. + */ + public function getAccessDependency(); + +} diff --git a/core/modules/block_content/src/Access/RefinableDependentAccessInterface.php b/core/modules/block_content/src/Access/RefinableDependentAccessInterface.php new file mode 100644 index 0000000000000000000000000000000000000000..469de52b8a2c66d1bdb344038614e3102f55f648 --- /dev/null +++ b/core/modules/block_content/src/Access/RefinableDependentAccessInterface.php @@ -0,0 +1,48 @@ +accessDependency = $access_dependency; + return $this; + } + + /** + * {@inheritdoc} + */ + public function getAccessDependency() { + return $this->accessDependency; + } + + /** + * {@inheritdoc} + */ + public function addAccessDependency(AccessibleInterface $access_dependency) { + if (empty($this->accessDependency)) { + $this->accessDependency = $access_dependency; + return $this; + } + if (!$this->accessDependency instanceof AccessGroupAnd) { + $accessGroup = new AccessGroupAnd(); + $this->accessDependency = $accessGroup->addDependency($this->accessDependency); + } + $this->accessDependency->addDependency($access_dependency); + return $this; + } + +} diff --git a/core/modules/block_content/src/BlockContentAccessControlHandler.php b/core/modules/block_content/src/BlockContentAccessControlHandler.php index 7079ef48495181081194a601eaab7c581643c176..17e61dce6b950966b32b75d4b3ac3f7aeae58123 100644 --- a/core/modules/block_content/src/BlockContentAccessControlHandler.php +++ b/core/modules/block_content/src/BlockContentAccessControlHandler.php @@ -2,27 +2,85 @@ namespace Drupal\block_content; +use Drupal\block_content\Access\DependentAccessInterface; +use Drupal\block_content\Event\BlockContentGetDependencyEvent; use Drupal\Core\Access\AccessResult; +use Drupal\Core\Entity\EntityHandlerInterface; use Drupal\Core\Entity\EntityInterface; use Drupal\Core\Entity\EntityAccessControlHandler; +use Drupal\Core\Entity\EntityTypeInterface; use Drupal\Core\Session\AccountInterface; +use Symfony\Component\DependencyInjection\ContainerInterface; +use Symfony\Component\EventDispatcher\EventDispatcherInterface; /** * Defines the access control handler for the custom block entity type. * * @see \Drupal\block_content\Entity\BlockContent */ -class BlockContentAccessControlHandler extends EntityAccessControlHandler { +class BlockContentAccessControlHandler extends EntityAccessControlHandler implements EntityHandlerInterface { + + /** + * The event dispatcher. + * + * @var \Symfony\Component\EventDispatcher\EventDispatcherInterface + */ + protected $eventDispatcher; + + /** + * BlockContentAccessControlHandler constructor. + * + * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type + * The entity type. + * @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $dispatcher + * The event dispatcher. + */ + public function __construct(EntityTypeInterface $entity_type, EventDispatcherInterface $dispatcher) { + parent::__construct($entity_type); + $this->eventDispatcher = $dispatcher; + } + + /** + * {@inheritdoc} + */ + public static function createInstance(ContainerInterface $container, EntityTypeInterface $entity_type) { + return new static( + $entity_type, + $container->get('event_dispatcher') + ); + } /** * {@inheritdoc} */ protected function checkAccess(EntityInterface $entity, $operation, AccountInterface $account) { if ($operation === 'view') { - return AccessResult::allowedIf($entity->isPublished())->addCacheableDependency($entity) + $access = AccessResult::allowedIf($entity->isPublished())->addCacheableDependency($entity) ->orIf(AccessResult::allowedIfHasPermission($account, 'administer blocks')); } - return parent::checkAccess($entity, $operation, $account); + else { + $access = parent::checkAccess($entity, $operation, $account); + } + $access->addCacheableDependency($entity); + /** @var \Drupal\block_content\BlockContentInterface $entity */ + if ($entity->isReusable() === FALSE) { + if (!$entity instanceof DependentAccessInterface) { + throw new \LogicException("Non-reusable block entities must implement \Drupal\block_content\Access\DependentAccessInterface for access control."); + } + $dependency = $entity->getAccessDependency(); + if (empty($dependency)) { + // If an access dependency has not been set let modules set one. + $event = new BlockContentGetDependencyEvent($entity); + $this->eventDispatcher->dispatch(BlockContentEvents::BLOCK_CONTENT_GET_DEPENDENCY, $event); + $dependency = $event->getAccessDependency(); + if (empty($dependency)) { + return AccessResult::forbidden("Non-reusable blocks must set an access dependency for access control."); + } + } + /** @var \Drupal\Core\Entity\EntityInterface $dependency */ + $access = $access->andIf($dependency->access($operation, $account, TRUE)); + } + return $access; } } diff --git a/core/modules/block_content/src/BlockContentEvents.php b/core/modules/block_content/src/BlockContentEvents.php new file mode 100644 index 0000000000000000000000000000000000000000..85931b7a726befb828010f8ab9d5f7f37cf1a9ca --- /dev/null +++ b/core/modules/block_content/src/BlockContentEvents.php @@ -0,0 +1,31 @@ +getStorage()->getQuery() + ->sort($this->entityType->getKey('id')); + $query->condition('reusable', TRUE); + + // Only add the pager if a limit is specified. + if ($this->limit) { + $query->pager($this->limit); + } + return $query->execute(); + } + } diff --git a/core/modules/block_content/src/BlockContentViewsData.php b/core/modules/block_content/src/BlockContentViewsData.php index 010ede0ea59ad468488e991bc0511c5fd2136b38..e9ff0eb4cd83d1e777aa3ee849a80c1d0823c3d8 100644 --- a/core/modules/block_content/src/BlockContentViewsData.php +++ b/core/modules/block_content/src/BlockContentViewsData.php @@ -23,6 +23,8 @@ public function getViewsData() { $data['block_content_field_data']['type']['field']['id'] = 'field'; + $data['block_content_field_data']['table']['wizard_id'] = 'block_content'; + $data['block_content']['block_content_listing_empty'] = [ 'title' => $this->t('Empty block library behavior'), 'help' => $this->t('Provides a link to add a new block.'), diff --git a/core/modules/block_content/src/Entity/BlockContent.php b/core/modules/block_content/src/Entity/BlockContent.php index 7696da091e0368d786152855aa594228664ed698..b9bc8a9d2dfef9d3eedfc43646072dc7061a667c 100644 --- a/core/modules/block_content/src/Entity/BlockContent.php +++ b/core/modules/block_content/src/Entity/BlockContent.php @@ -2,6 +2,7 @@ namespace Drupal\block_content\Entity; +use Drupal\block_content\Access\RefinableDependentAccessTrait; use Drupal\Core\Entity\EditorialContentEntityBase; use Drupal\Core\Entity\EntityStorageInterface; use Drupal\Core\Entity\EntityTypeInterface; @@ -77,6 +78,8 @@ */ class BlockContent extends EditorialContentEntityBase implements BlockContentInterface { + use RefinableDependentAccessTrait; + /** * The theme the block is being created in. * @@ -118,7 +121,9 @@ public function getTheme() { */ public function postSave(EntityStorageInterface $storage, $update = TRUE) { parent::postSave($storage, $update); - static::invalidateBlockPluginCache(); + if ($this->isReusable() || (isset($this->original) && $this->original->isReusable())) { + static::invalidateBlockPluginCache(); + } } /** @@ -126,7 +131,14 @@ public function postSave(EntityStorageInterface $storage, $update = TRUE) { */ public static function postDelete(EntityStorageInterface $storage, array $entities) { parent::postDelete($storage, $entities); - static::invalidateBlockPluginCache(); + /** @var \Drupal\block_content\BlockContentInterface $block */ + foreach ($entities as $block) { + if ($block->isReusable()) { + // If any deleted blocks are reusable clear the block cache. + static::invalidateBlockPluginCache(); + return; + } + } } /** @@ -200,6 +212,14 @@ public static function baseFieldDefinitions(EntityTypeInterface $entity_type) { ->setTranslatable(TRUE) ->setRevisionable(TRUE); + $fields['reusable'] = BaseFieldDefinition::create('boolean') + ->setLabel(t('Reusable')) + ->setDescription(t('A boolean indicating whether this block is reusable.')) + ->setTranslatable(FALSE) + ->setRevisionable(FALSE) + ->setDefaultValue(TRUE) + ->setInitialValue(TRUE); + return $fields; } @@ -282,6 +302,27 @@ public function setRevisionLogMessage($revision_log_message) { return $this; } + /** + * {@inheritdoc} + */ + public function isReusable() { + return $this->get('reusable')->first()->get('value')->getCastedValue(); + } + + /** + * {@inheritdoc} + */ + public function setReusable() { + return $this->set('reusable', TRUE); + } + + /** + * {@inheritdoc} + */ + public function setNonReusable() { + return $this->set('reusable', FALSE); + } + /** * Invalidates the block plugin cache after changes and deletions. */ diff --git a/core/modules/block_content/src/Event/BlockContentGetDependencyEvent.php b/core/modules/block_content/src/Event/BlockContentGetDependencyEvent.php new file mode 100644 index 0000000000000000000000000000000000000000..e705172aa523b97126626f5bd218b3bf49446014 --- /dev/null +++ b/core/modules/block_content/src/Event/BlockContentGetDependencyEvent.php @@ -0,0 +1,70 @@ +blockContent = $blockContent; + } + + /** + * Gets the block content entity. + * + * @return \Drupal\block_content\BlockContentInterface + * The block content entity. + */ + public function getBlockContentEntity() { + return $this->blockContent; + } + + /** + * Gets the access dependency. + * + * @return \Drupal\Core\Access\AccessibleInterface + * The access dependency. + */ + public function getAccessDependency() { + return $this->accessDependency; + } + + /** + * Sets the access dependency. + * + * @param \Drupal\Core\Access\AccessibleInterface $access_dependency + * The access dependency. + */ + public function setAccessDependency(AccessibleInterface $access_dependency) { + $this->accessDependency = $access_dependency; + } + +} diff --git a/core/modules/block_content/src/Plugin/Derivative/BlockContent.php b/core/modules/block_content/src/Plugin/Derivative/BlockContent.php index ac82a6c39caa0b55512983d3f5bf85552f5625e6..ba1ab989688a7d03ff32a7b01971875afd4db834 100644 --- a/core/modules/block_content/src/Plugin/Derivative/BlockContent.php +++ b/core/modules/block_content/src/Plugin/Derivative/BlockContent.php @@ -43,7 +43,7 @@ public static function create(ContainerInterface $container, $base_plugin_id) { * {@inheritdoc} */ public function getDerivativeDefinitions($base_plugin_definition) { - $block_contents = $this->blockContentStorage->loadMultiple(); + $block_contents = $this->blockContentStorage->loadByProperties(['reusable' => TRUE]); // Reset the discovered definitions. $this->derivatives = []; /** @var $block_content \Drupal\block_content\Entity\BlockContent */ diff --git a/core/modules/block_content/src/Plugin/views/wizard/BlockContent.php b/core/modules/block_content/src/Plugin/views/wizard/BlockContent.php new file mode 100644 index 0000000000000000000000000000000000000000..60725015374636a68d1d93879783043c990395fe --- /dev/null +++ b/core/modules/block_content/src/Plugin/views/wizard/BlockContent.php @@ -0,0 +1,35 @@ + 'reusable', + 'plugin_id' => 'boolean', + 'table' => $this->base_table, + 'field' => 'reusable', + 'value' => '1', + 'entity_type' => $this->entityTypeId, + 'entity_field' => 'reusable', + ]; + return $filters; + } + +} diff --git a/core/modules/block_content/tests/modules/block_content_test/src/Plugin/EntityReferenceSelection/TestSelection.php b/core/modules/block_content/tests/modules/block_content_test/src/Plugin/EntityReferenceSelection/TestSelection.php new file mode 100644 index 0000000000000000000000000000000000000000..1a46c486c230b0f239ee98809f822d12c29ab788 --- /dev/null +++ b/core/modules/block_content/tests/modules/block_content_test/src/Plugin/EntityReferenceSelection/TestSelection.php @@ -0,0 +1,80 @@ +conditionType = $condition_type; + $this->isReusable = $is_reusable; + } + + /** + * {@inheritdoc} + */ + protected function buildEntityQuery($match = NULL, $match_operator = 'CONTAINS') { + $query = parent::buildEntityQuery($match, $match_operator); + if ($this->conditionType) { + /** @var \Drupal\Core\Database\Query\ConditionInterface $add_condition */ + $add_condition = NULL; + switch ($this->conditionType) { + case 'base': + $add_condition = $query; + break; + + case 'group': + $group = $query->andConditionGroup() + ->exists('type'); + $add_condition = $group; + $query->condition($group); + break; + + case "nested_group": + $query->exists('type'); + $sub_group = $query->andConditionGroup() + ->exists('type'); + $add_condition = $sub_group; + $group = $query->andConditionGroup() + ->exists('type') + ->condition($sub_group); + $query->condition($group); + break; + } + if ($this->isReusable) { + $add_condition->condition('reusable', 1); + } + else { + $add_condition->condition('reusable', 0); + } + } + return $query; + } + +} diff --git a/core/modules/block_content/tests/modules/block_content_view_override/block_content_view_override.info.yml b/core/modules/block_content/tests/modules/block_content_view_override/block_content_view_override.info.yml new file mode 100644 index 0000000000000000000000000000000000000000..3ca2d1bc375d2b05d8be7503ee03ede71643dc9e --- /dev/null +++ b/core/modules/block_content/tests/modules/block_content_view_override/block_content_view_override.info.yml @@ -0,0 +1,9 @@ +name: "Custom Block module reusable tests" +type: module +description: "Support module for custom block reusable testing." +package: Testing +version: VERSION +core: 8.x +dependencies: + - drupal:block_content + - drupal:views diff --git a/core/modules/block_content/tests/modules/block_content_view_override/config/install/views.view.block_content.yml b/core/modules/block_content/tests/modules/block_content_view_override/config/install/views.view.block_content.yml new file mode 100644 index 0000000000000000000000000000000000000000..3c622a9abb42918cf149a0f42988dfd6bb08dd1f --- /dev/null +++ b/core/modules/block_content/tests/modules/block_content_view_override/config/install/views.view.block_content.yml @@ -0,0 +1,602 @@ +langcode: en +status: true +dependencies: + module: + - block_content + - user +id: block_content +label: 'Custom block library' +module: views +description: 'Find and manage custom blocks.' +tag: default +base_table: block_content_field_data +base_field: id +core: 8.x +display: + default: + display_plugin: default + id: default + display_title: Master + position: 0 + display_options: + access: + type: perm + options: + perm: 'administer blocks' + 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: mini + options: + items_per_page: 50 + offset: 0 + id: 0 + total_pages: null + tags: + previous: '‹ Previous' + next: 'Next ›' + expose: + items_per_page: false + items_per_page_label: 'Items per page' + items_per_page_options: '5, 10, 25, 50' + items_per_page_options_all: false + items_per_page_options_all_label: '- All -' + offset: false + offset_label: Offset + style: + type: table + options: + grouping: { } + row_class: '' + default_row_class: true + override: true + sticky: false + caption: '' + summary: '' + description: '' + columns: + info: info + type: type + changed: changed + operations: operations + info: + info: + sortable: true + default_sort_order: asc + align: '' + separator: '' + empty_column: false + responsive: '' + type: + sortable: true + default_sort_order: asc + align: '' + separator: '' + empty_column: false + responsive: '' + changed: + sortable: true + default_sort_order: desc + align: '' + separator: '' + empty_column: false + responsive: '' + operations: + sortable: false + default_sort_order: asc + align: '' + separator: '' + empty_column: false + responsive: '' + default: changed + empty_table: true + row: + type: fields + fields: + info: + id: info + table: block_content_field_data + field: info + relationship: none + group_type: group + admin_label: '' + label: 'Block description' + exclude: false + alter: + alter_text: false + text: '' + 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: true + 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 + entity_type: null + entity_field: info + plugin_id: field + type: + id: type + table: block_content_field_data + field: type + relationship: none + group_type: group + admin_label: '' + label: 'Block type' + exclude: false + alter: + alter_text: false + text: '' + 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: true + element_wrapper_type: '' + element_wrapper_class: '' + element_default_classes: true + empty: '' + hide_empty: false + empty_zero: false + hide_alter_empty: true + click_sort_column: target_id + type: entity_reference_label + settings: + link: false + group_column: target_id + 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 + entity_type: block_content + entity_field: type + plugin_id: field + changed: + id: changed + table: block_content_field_data + field: changed + relationship: none + group_type: group + admin_label: '' + label: Updated + exclude: false + alter: + alter_text: false + text: '' + 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: true + element_wrapper_type: '' + element_wrapper_class: '' + element_default_classes: true + empty: '' + hide_empty: false + empty_zero: false + hide_alter_empty: true + entity_type: block_content + entity_field: changed + type: timestamp + settings: + date_format: short + custom_date_format: '' + timezone: '' + plugin_id: field + operations: + id: operations + table: block_content + field: operations + relationship: none + group_type: group + admin_label: '' + label: Operations + exclude: false + alter: + alter_text: false + text: '' + 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: true + element_wrapper_type: '' + element_wrapper_class: '' + element_default_classes: true + empty: '' + hide_empty: false + empty_zero: false + hide_alter_empty: true + destination: true + entity_type: block_content + plugin_id: entity_operations + filters: + info: + id: info + table: block_content_field_data + field: info + relationship: none + group_type: group + admin_label: '' + operator: contains + value: '' + group: 1 + exposed: true + expose: + operator_id: info_op + label: 'Block description' + description: '' + use_operator: false + operator: info_op + identifier: info + required: false + remember: false + multiple: false + remember_roles: + authenticated: authenticated + anonymous: '0' + administrator: '0' + is_grouped: false + group_info: + label: '' + description: '' + identifier: '' + optional: true + widget: select + multiple: false + remember: false + default_group: All + default_group_multiple: { } + group_items: { } + entity_type: block_content + entity_field: info + plugin_id: string + type: + id: type + table: block_content_field_data + field: type + relationship: none + group_type: group + admin_label: '' + operator: in + value: { } + group: 1 + exposed: true + expose: + operator_id: type_op + label: 'Block type' + description: '' + use_operator: false + operator: type_op + identifier: type + required: false + remember: false + multiple: false + remember_roles: + authenticated: authenticated + anonymous: '0' + administrator: '0' + reduce: false + is_grouped: false + group_info: + label: '' + description: '' + identifier: '' + optional: true + widget: select + multiple: false + remember: false + default_group: All + default_group_multiple: { } + group_items: { } + entity_type: block_content + entity_field: type + plugin_id: bundle + sorts: { } + title: 'Custom block library' + 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: 'There are no custom blocks available.' + plugin_id: text_custom + block_content_listing_empty: + admin_label: '' + empty: true + field: block_content_listing_empty + group_type: group + id: block_content_listing_empty + label: '' + relationship: none + table: block_content + plugin_id: block_content_listing_empty + entity_type: block_content + relationships: { } + arguments: { } + display_extenders: { } + cache_metadata: + contexts: + - 'languages:language_content' + - 'languages:language_interface' + - url + - url.query_args + - user.permissions + max-age: 0 + tags: { } + page_1: + display_plugin: page + id: page_1 + display_title: Page + position: 1 + display_options: + display_extenders: { } + path: admin/structure/block/block-content + menu: + type: tab + title: 'Custom block library' + description: '' + parent: block.admin_display + weight: 0 + context: '0' + menu_name: admin + cache_metadata: + contexts: + - 'languages:language_content' + - 'languages:language_interface' + - url + - url.query_args + - user.permissions + max-age: 0 + tags: { } + page_2: + display_plugin: page + id: page_2 + display_title: 'Page 2' + position: 2 + display_options: + display_extenders: { } + path: extra-view-display + filters: + type: + id: type + table: block_content_field_data + field: type + relationship: none + group_type: group + admin_label: '' + operator: in + value: { } + group: 1 + exposed: true + expose: + operator_id: type_op + label: 'Block type' + description: '' + use_operator: false + operator: type_op + identifier: type + required: false + remember: false + multiple: false + remember_roles: + authenticated: authenticated + anonymous: '0' + administrator: '0' + reduce: false + is_grouped: false + group_info: + label: '' + description: '' + identifier: '' + optional: true + widget: select + multiple: false + remember: false + default_group: All + default_group_multiple: { } + group_items: { } + entity_type: block_content + entity_field: type + plugin_id: bundle + info: + id: info + table: block_content_field_data + field: info + relationship: none + group_type: group + admin_label: '' + operator: 'contains' + value: block2 + group: 1 + exposed: false + expose: + operator_id: '' + label: '' + description: '' + use_operator: false + operator: '' + identifier: '' + required: false + remember: false + multiple: false + remember_roles: + authenticated: authenticated + placeholder: '' + is_grouped: false + group_info: + label: '' + description: '' + identifier: '' + optional: true + widget: select + multiple: false + remember: false + default_group: All + default_group_multiple: { } + group_items: { } + entity_type: block_content + entity_field: info + plugin_id: string + defaults: + filters: false + filter_groups: false + filter_groups: + operator: AND + groups: + 1: AND + cache_metadata: + max-age: 0 + contexts: + - 'languages:language_content' + - 'languages:language_interface' + - url + - url.query_args + - user.permissions + tags: { } diff --git a/core/modules/block_content/tests/src/Functional/BlockContentListTest.php b/core/modules/block_content/tests/src/Functional/BlockContentListTest.php index 9a26f1c4077616b59aee55e8a21686a4a3f850c4..8919c05d00193c6354d84979aaa116725d35f1ce 100644 --- a/core/modules/block_content/tests/src/Functional/BlockContentListTest.php +++ b/core/modules/block_content/tests/src/Functional/BlockContentListTest.php @@ -2,6 +2,8 @@ namespace Drupal\Tests\block_content\Functional; +use Drupal\block_content\Entity\BlockContent; + /** * Tests the listing of custom blocks. * @@ -104,6 +106,19 @@ public function testListing() { // Confirm that the empty text is displayed. $this->assertText(t('There are no custom blocks yet.')); + + $block_content = BlockContent::create([ + 'info' => 'Non-reusable block', + 'type' => 'basic', + 'reusable' => FALSE, + ]); + $block_content->save(); + + $this->drupalGet('admin/structure/block/block-content'); + // Confirm that the empty text is displayed. + $this->assertSession()->pageTextContains('There are no custom blocks yet.'); + // Confirm the non-reusable block is not on the page. + $this->assertSession()->pageTextNotContains('Non-reusable block'); } } diff --git a/core/modules/block_content/tests/src/Functional/BlockContentListViewsTest.php b/core/modules/block_content/tests/src/Functional/BlockContentListViewsTest.php index 1c623be82eba3c9cf4af6525a00f07fa3971df37..f9cf29eb77bcc20ea4c47a4871c3f2d10d25ab9e 100644 --- a/core/modules/block_content/tests/src/Functional/BlockContentListViewsTest.php +++ b/core/modules/block_content/tests/src/Functional/BlockContentListViewsTest.php @@ -2,6 +2,8 @@ namespace Drupal\Tests\block_content\Functional; +use Drupal\block_content\Entity\BlockContent; + /** * Tests the Views-powered listing of custom blocks. * @@ -112,6 +114,19 @@ public function testListing() { // Confirm that the empty text is displayed. $this->assertText('There are no custom blocks available.'); $this->assertLink('custom block'); + + $block_content = BlockContent::create([ + 'info' => 'Non-reusable block', + 'type' => 'basic', + 'reusable' => FALSE, + ]); + $block_content->save(); + + $this->drupalGet('admin/structure/block/block-content'); + // Confirm that the empty text is displayed. + $this->assertSession()->pageTextContains('There are no custom blocks available.'); + // Confirm the non-reusable block is not on the page. + $this->assertSession()->pageTextNotContains('Non-reusable block'); } } diff --git a/core/modules/block_content/tests/src/Functional/Rest/BlockContentResourceTestBase.php b/core/modules/block_content/tests/src/Functional/Rest/BlockContentResourceTestBase.php index c77585eb292dca4650953bf8c9a59a39a61f7917..4a3ac11f4c5a71e448a48421057f1c9ef567bedc 100644 --- a/core/modules/block_content/tests/src/Functional/Rest/BlockContentResourceTestBase.php +++ b/core/modules/block_content/tests/src/Functional/Rest/BlockContentResourceTestBase.php @@ -92,6 +92,11 @@ protected function getExpectedNormalizedEntity() { 'value' => 'en', ], ], + 'reusable' => [ + [ + 'value' => TRUE, + ], + ], 'type' => [ [ 'target_id' => 'basic', diff --git a/core/modules/block_content/tests/src/Functional/Update/BlockContentReusableUpdateTest.php b/core/modules/block_content/tests/src/Functional/Update/BlockContentReusableUpdateTest.php new file mode 100644 index 0000000000000000000000000000000000000000..4e8d99398b68060c647deb7f12878be160cd3984 --- /dev/null +++ b/core/modules/block_content/tests/src/Functional/Update/BlockContentReusableUpdateTest.php @@ -0,0 +1,155 @@ +databaseDumpFiles = [ + __DIR__ . '/../../../../../system/tests/fixtures/update/drupal-8.4.0.bare.standard.php.gz', + ]; + } + + /** + * Tests adding 'reusable' entity base field to the block content entity type. + * + * @see block_content_update_8600 + * @see block_content_post_update_add_views_reusable_filter + */ + public function testReusableFieldAddition() { + $assert_session = $this->assertSession(); + $entity_definition_update_manager = \Drupal::entityDefinitionUpdateManager(); + + // Delete custom block library view. + $this->config('views.view.block_content')->delete(); + // Install the test module with the 'block_content' view with an extra + // display with overridden filters. This extra display should also have a + // filter added for 'reusable' field so that it does not expose non-reusable + // fields. This display also has a filter that only shows blocks that + // contain 'block2' in the 'info' field. + $this->container->get('module_installer')->install(['block_content_view_override']); + + // Ensure that 'reusable' field is not present before updates. + $this->assertEmpty($entity_definition_update_manager->getFieldStorageDefinition('reusable', 'block_content')); + + // Ensure that 'reusable' filter is not present before updates. + $view_config = \Drupal::configFactory()->get('views.view.block_content'); + $this->assertFalse($view_config->isNew()); + $this->assertEmpty($view_config->get('display.default.display_options.filters.reusable')); + $this->assertEmpty($view_config->get('display.page_2.display_options.filters.reusable')); + // Run updates. + $this->runUpdates(); + + // Ensure that 'reusable' filter is present after updates. + \Drupal::configFactory()->clearStaticCache(); + $view_config = \Drupal::configFactory()->get('views.view.block_content'); + $this->assertNotEmpty($view_config->get('display.default.display_options.filters.reusable')); + $this->assertNotEmpty($view_config->get('display.page_2.display_options.filters.reusable')); + + // Check that the field exists and is configured correctly. + $reusable_field = $entity_definition_update_manager->getFieldStorageDefinition('reusable', 'block_content'); + $this->assertEquals('Reusable', $reusable_field->getLabel()); + $this->assertEquals('A boolean indicating whether this block is reusable.', $reusable_field->getDescription()); + $this->assertEquals(FALSE, $reusable_field->isRevisionable()); + $this->assertEquals(FALSE, $reusable_field->isTranslatable()); + + $after_block1 = BlockContent::create([ + 'info' => 'After update block1', + 'type' => 'basic_block', + ]); + $after_block1->save(); + // Add second block that will be shown with the 'info' filter on the + // additional view display. + $after_block2 = BlockContent::create([ + 'info' => 'After update block2', + 'type' => 'basic_block', + ]); + $after_block2->save(); + + $this->assertTrue($after_block1->isReusable()); + $this->assertTrue($after_block2->isReusable()); + + $admin_user = $this->drupalCreateUser(['administer blocks']); + $this->drupalLogin($admin_user); + + $block_non_reusable = BlockContent::create([ + 'info' => 'block1 non reusable', + 'type' => 'basic_block', + 'reusable' => FALSE, + ]); + $block_non_reusable->save(); + // Add second block that would be shown with the 'info' filter on the + // additional view display if the 'reusable' filter was not added. + $block2_non_reusable = BlockContent::create([ + 'info' => 'block2 non reusable', + 'type' => 'basic_block', + 'reusable' => FALSE, + ]); + $block2_non_reusable->save(); + $this->assertFalse($block_non_reusable->isReusable()); + $this->assertFalse($block2_non_reusable->isReusable()); + + // Ensure the Custom Block view shows the reusable blocks only. + $this->drupalGet('admin/structure/block/block-content'); + $assert_session->statusCodeEquals('200'); + $assert_session->responseContains('view-id-block_content'); + $assert_session->pageTextContains($after_block1->label()); + $assert_session->pageTextContains($after_block2->label()); + $assert_session->pageTextNotContains($block_non_reusable->label()); + $assert_session->pageTextNotContains($block2_non_reusable->label()); + + // Ensure the view's other display also only shows reusable blocks and still + // filters on the 'info' field. + $this->drupalGet('extra-view-display'); + $assert_session->statusCodeEquals('200'); + $assert_session->responseContains('view-id-block_content'); + $assert_session->pageTextNotContains($after_block1->label()); + $assert_session->pageTextContains($after_block2->label()); + $assert_session->pageTextNotContains($block_non_reusable->label()); + $assert_session->pageTextNotContains($block2_non_reusable->label()); + + // Ensure the Custom Block listing without Views installed shows the only + // reusable blocks. + $this->drupalGet('admin/structure/block/block-content'); + $this->container->get('module_installer')->uninstall(['views_ui', 'views']); + $this->drupalGet('admin/structure/block/block-content'); + $assert_session->statusCodeEquals('200'); + $assert_session->responseNotContains('view-id-block_content'); + $assert_session->pageTextContains($after_block1->label()); + $assert_session->pageTextContains($after_block2->label()); + $assert_session->pageTextNotContains($block_non_reusable->label()); + $assert_session->pageTextNotContains($block2_non_reusable->label()); + + $this->drupalGet('block/' . $after_block1->id()); + $assert_session->statusCodeEquals('200'); + + // Ensure the non-reusable block is not accessible in the form. + $this->drupalGet('block/' . $block_non_reusable->id()); + $assert_session->statusCodeEquals('403'); + + $this->drupalLogout(); + + $this->drupalLogin($this->createUser([ + 'access user profiles', + 'administer blocks', + ])); + $this->drupalGet('block/' . $after_block1->id()); + $assert_session->statusCodeEquals('200'); + + $this->drupalGet('block/' . $block_non_reusable->id()); + $assert_session->statusCodeEquals('403'); + } + +} diff --git a/core/modules/block_content/tests/src/Functional/Views/BlockContentWizardTest.php b/core/modules/block_content/tests/src/Functional/Views/BlockContentWizardTest.php new file mode 100644 index 0000000000000000000000000000000000000000..2c59e5c50fe84ec0c7888e02f0d84e0a7d273821 --- /dev/null +++ b/core/modules/block_content/tests/src/Functional/Views/BlockContentWizardTest.php @@ -0,0 +1,52 @@ +drupalLogin($this->drupalCreateUser(['administer views'])); + $this->createBlockContentType('Basic block'); + } + + /** + * Tests creating a 'block_content' entity view. + */ + public function testViewAddBlockContent() { + $view = []; + $view['label'] = $this->randomMachineName(16); + $view['id'] = strtolower($this->randomMachineName(16)); + $view['description'] = $this->randomMachineName(16); + $view['page[create]'] = FALSE; + $view['show[wizard_key]'] = 'block_content'; + $this->drupalPostForm('admin/structure/views/add', $view, t('Save and edit')); + + $view_storage_controller = $this->container->get('entity_type.manager')->getStorage('view'); + /** @var \Drupal\views\Entity\View $view */ + $view = $view_storage_controller->load($view['id']); + + $display_options = $view->getDisplay('default')['display_options']; + + $this->assertEquals('block_content', $display_options['filters']['reusable']['entity_type']); + $this->assertEquals('reusable', $display_options['filters']['reusable']['entity_field']); + $this->assertEquals('boolean', $display_options['filters']['reusable']['plugin_id']); + $this->assertEquals('1', $display_options['filters']['reusable']['value']); + } + +} diff --git a/core/modules/block_content/tests/src/Kernel/BlockContentAccessHandlerTest.php b/core/modules/block_content/tests/src/Kernel/BlockContentAccessHandlerTest.php new file mode 100644 index 0000000000000000000000000000000000000000..64b524d80b89273049ca86fed288b9b3d84bef62 --- /dev/null +++ b/core/modules/block_content/tests/src/Kernel/BlockContentAccessHandlerTest.php @@ -0,0 +1,300 @@ +installSchema('system', ['sequence']); + $this->installSchema('system', ['sequences']); + $this->installSchema('user', ['users_data']); + $this->installEntitySchema('user'); + $this->installEntitySchema('block_content'); + + // Create a block content type. + $block_content_type = BlockContentType::create([ + 'id' => 'square', + 'label' => 'A square block type', + 'description' => "Provides a block type that is square.", + ]); + $block_content_type->save(); + + $this->blockEntity = BlockContent::create([ + 'info' => 'The Block', + 'type' => 'square', + ]); + $this->blockEntity->save(); + + // Create user 1 test does not have all permissions. + User::create([ + 'name' => 'admin', + ])->save(); + + $this->role = Role::create([ + 'id' => 'roly', + 'label' => 'roly poly', + ]); + $this->role->save(); + $this->accessControlHandler = new BlockContentAccessControlHandler(\Drupal::entityTypeManager()->getDefinition('block_content'), \Drupal::service('event_dispatcher')); + } + + /** + * @covers ::checkAccess + * + * @dataProvider providerTestAccess + */ + public function testAccess($operation, $published, $reusable, $permissions, $parent_access, $expected_access) { + $published ? $this->blockEntity->setPublished() : $this->blockEntity->setUnpublished(); + $reusable ? $this->blockEntity->setReusable() : $this->blockEntity->setNonReusable(); + + $user = User::create([ + 'name' => 'Someone', + 'mail' => 'hi@example.com', + ]); + + if ($permissions) { + foreach ($permissions as $permission) { + $this->role->grantPermission($permission); + } + $this->role->save(); + } + $user->addRole($this->role->id()); + $user->save(); + + if ($parent_access) { + $parent_entity = $this->prophesize(AccessibleInterface::class); + $expected_parent_result = NULL; + switch ($parent_access) { + case 'allowed': + $expected_parent_result = AccessResult::allowed(); + break; + + case 'neutral': + $expected_parent_result = AccessResult::neutral(); + break; + + case 'forbidden': + $expected_parent_result = AccessResult::forbidden(); + break; + } + $parent_entity->access($operation, $user, TRUE) + ->willReturn($expected_parent_result) + ->shouldBeCalled(); + + $this->blockEntity->setAccessDependency($parent_entity->reveal()); + + } + $this->blockEntity->save(); + + $result = $this->accessControlHandler->access($this->blockEntity, $operation, $user, TRUE); + switch ($expected_access) { + case 'allowed': + $this->assertTrue($result->isAllowed()); + break; + + case 'forbidden': + $this->assertTrue($result->isForbidden()); + break; + + case 'neutral': + $this->assertTrue($result->isNeutral()); + break; + + default: + $this->fail('Unexpected access type'); + } + } + + /** + * Dataprovider for testAccess(). + */ + public function providerTestAccess() { + $cases = [ + 'view:published:reusable' => [ + 'view', + TRUE, + TRUE, + [], + NULL, + 'allowed', + ], + 'view:unpublished:reusable' => [ + 'view', + FALSE, + TRUE, + [], + NULL, + 'neutral', + ], + 'view:unpublished:reusable:admin' => [ + 'view', + FALSE, + TRUE, + ['administer blocks'], + NULL, + 'allowed', + ], + 'view:published:reusable:admin' => [ + 'view', + TRUE, + TRUE, + ['administer blocks'], + NULL, + 'allowed', + ], + 'view:published:non_reusable' => [ + 'view', + TRUE, + FALSE, + [], + NULL, + 'forbidden', + ], + 'view:published:non_reusable:parent_allowed' => [ + 'view', + TRUE, + FALSE, + [], + 'allowed', + 'allowed', + ], + 'view:published:non_reusable:parent_neutral' => [ + 'view', + TRUE, + FALSE, + [], + 'neutral', + 'neutral', + ], + 'view:published:non_reusable:parent_forbidden' => [ + 'view', + TRUE, + FALSE, + [], + 'forbidden', + 'forbidden', + ], + ]; + foreach (['update', 'delete'] as $operation) { + $cases += [ + $operation . ':published:reusable' => [ + $operation, + TRUE, + TRUE, + [], + NULL, + 'neutral', + ], + $operation . ':unpublished:reusable' => [ + $operation, + FALSE, + TRUE, + [], + NULL, + 'neutral', + ], + $operation . ':unpublished:reusable:admin' => [ + $operation, + FALSE, + TRUE, + ['administer blocks'], + NULL, + 'allowed', + ], + $operation . ':published:reusable:admin' => [ + $operation, + TRUE, + TRUE, + ['administer blocks'], + NULL, + 'allowed', + ], + $operation . ':published:non_reusable' => [ + $operation, + TRUE, + FALSE, + [], + NULL, + 'forbidden', + ], + $operation . ':published:non_reusable:parent_allowed' => [ + $operation, + TRUE, + FALSE, + [], + 'allowed', + 'neutral', + ], + $operation . ':published:non_reusable:parent_neutral' => [ + $operation, + TRUE, + FALSE, + [], + 'neutral', + 'neutral', + ], + $operation . ':published:non_reusable:parent_forbidden' => [ + $operation, + TRUE, + FALSE, + [], + 'forbidden', + 'forbidden', + ], + ]; + return $cases; + } + } + +} diff --git a/core/modules/block_content/tests/src/Kernel/BlockContentDeriverTest.php b/core/modules/block_content/tests/src/Kernel/BlockContentDeriverTest.php new file mode 100644 index 0000000000000000000000000000000000000000..d08d86f25fa8a86f23bbbafd0733d33bd75c0e24 --- /dev/null +++ b/core/modules/block_content/tests/src/Kernel/BlockContentDeriverTest.php @@ -0,0 +1,66 @@ +installSchema('system', ['sequence']); + $this->installEntitySchema('user'); + $this->installEntitySchema('block_content'); + } + + /** + * Tests that only reusable blocks are derived. + */ + public function testReusableBlocksOnlyAreDerived() { + // Create a block content type. + $block_content_type = BlockContentType::create([ + 'id' => 'spiffy', + 'label' => 'Mucho spiffy', + 'description' => "Provides a block type that increases your site's spiffiness by up to 11%", + ]); + $block_content_type->save(); + // And a block content entity. + $block_content = BlockContent::create([ + 'info' => 'Spiffy prototype', + 'type' => 'spiffy', + ]); + $block_content->save(); + + // Ensure the reusable block content is provided as a derivative block + // plugin. + /** @var \Drupal\Core\Block\BlockManagerInterface $block_manager */ + $block_manager = $this->container->get('plugin.manager.block'); + $plugin_id = 'block_content' . PluginBase::DERIVATIVE_SEPARATOR . $block_content->uuid(); + $this->assertTrue($block_manager->hasDefinition($plugin_id)); + + // Set the block not to be reusable. + $block_content->setNonReusable(); + $block_content->save(); + + // Ensure the non-reusable block content is not provided a derivative block + // plugin. + $this->assertFalse($block_manager->hasDefinition($plugin_id)); + } + +} diff --git a/core/modules/block_content/tests/src/Kernel/BlockContentEntityReferenceSelectionTest.php b/core/modules/block_content/tests/src/Kernel/BlockContentEntityReferenceSelectionTest.php new file mode 100644 index 0000000000000000000000000000000000000000..e593336fa3a1eff8526203a199679efb5bca1b56 --- /dev/null +++ b/core/modules/block_content/tests/src/Kernel/BlockContentEntityReferenceSelectionTest.php @@ -0,0 +1,189 @@ +installSchema('system', ['sequence']); + $this->installSchema('system', ['sequences']); + $this->installEntitySchema('user'); + $this->installEntitySchema('block_content'); + + // Create a block content type. + $block_content_type = BlockContentType::create([ + 'id' => 'spiffy', + 'label' => 'Mucho spiffy', + 'description' => "Provides a block type that increases your site's spiffiness by up to 11%", + ]); + $block_content_type->save(); + $this->entityTypeManager = $this->container->get('entity_type.manager'); + + // And reusable block content entities. + $this->blockReusable = BlockContent::create([ + 'info' => 'Reusable Block', + 'type' => 'spiffy', + ]); + $this->blockReusable->save(); + $this->blockNonReusable = BlockContent::create([ + 'info' => 'Non-reusable Block', + 'type' => 'spiffy', + 'reusable' => FALSE, + ]); + $this->blockNonReusable->save(); + + $configuration = [ + 'target_type' => 'block_content', + 'target_bundles' => ['spiffy' => 'spiffy'], + 'sort' => ['field' => '_none'], + ]; + $this->selectionHandler = new TestSelection($configuration, '', '', $this->container->get('entity.manager'), $this->container->get('module_handler'), \Drupal::currentUser()); + + // Setup the 3 expectation cases. + $this->expectations = [ + 'both_blocks' => [ + 'spiffy' => [ + $this->blockReusable->id() => $this->blockReusable->label(), + $this->blockNonReusable->id() => $this->blockNonReusable->label(), + ], + ], + 'block_reusable' => ['spiffy' => [$this->blockReusable->id() => $this->blockReusable->label()]], + 'block_non_reusable' => ['spiffy' => [$this->blockNonReusable->id() => $this->blockNonReusable->label()]], + ]; + } + + /** + * Tests to make sure queries without the expected tags are not altered. + * + * @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException + * @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException + */ + public function testQueriesNotAltered() { + // Ensure that queries without all the tags are not altered. + $query = $this->entityTypeManager->getStorage('block_content')->getQuery(); + $this->assertCount(2, $query->execute()); + + $query = $this->entityTypeManager->getStorage('block_content')->getQuery(); + $query->addTag('block_content_access'); + $this->assertCount(2, $query->execute()); + + $query = $this->entityTypeManager->getStorage('block_content')->getQuery(); + $query->addTag('entity_query_block_content'); + $this->assertCount(2, $query->execute()); + } + + /** + * Test with no conditions set. + * + * @throws \Drupal\Core\Entity\EntityStorageException + */ + public function testNoConditions() { + $this->assertEquals( + $this->expectations['block_reusable'], + $this->selectionHandler->getReferenceableEntities() + ); + + $this->blockNonReusable->setReusable(); + $this->blockNonReusable->save(); + + // Ensure that the block is now returned as a referenceable entity. + $this->assertEquals( + $this->expectations['both_blocks'], + $this->selectionHandler->getReferenceableEntities() + ); + } + + /** + * Tests setting 'reusable' condition on different levels. + * + * @dataProvider fieldConditionProvider + * + * @throws \Exception + */ + public function testFieldConditions($condition_type, $is_reusable) { + $this->selectionHandler->setTestMode($condition_type, $is_reusable); + $this->assertEquals( + $is_reusable ? $this->expectations['block_reusable'] : $this->expectations['block_non_reusable'], + $this->selectionHandler->getReferenceableEntities() + ); + } + + /** + * Provides possible fields and condition types. + */ + public function fieldConditionProvider() { + $cases = []; + foreach (['base', 'group', 'nested_group'] as $condition_type) { + foreach ([TRUE, FALSE] as $reusable) { + $cases["$condition_type:" . ($reusable ? 'reusable' : 'non-reusable')] = [ + $condition_type, + $reusable, + ]; + } + } + return $cases; + } + +} diff --git a/core/modules/block_content/tests/src/Unit/Access/AccessGroupAndTest.php b/core/modules/block_content/tests/src/Unit/Access/AccessGroupAndTest.php new file mode 100644 index 0000000000000000000000000000000000000000..8915674862383c3a80398cdc9a2f5477ca826624 --- /dev/null +++ b/core/modules/block_content/tests/src/Unit/Access/AccessGroupAndTest.php @@ -0,0 +1,55 @@ +account = $this->prophesize(AccountInterface::class)->reveal(); + } + + /** + * @covers \Drupal\block_content\Access\AccessGroupAnd + */ + public function testGroups() { + $allowedAccessible = $this->createAccessibleDouble(AccessResult::allowed()); + $forbiddenAccessible = $this->createAccessibleDouble(AccessResult::forbidden()); + $neutralAccessible = $this->createAccessibleDouble(AccessResult::neutral()); + + // Ensure that groups with no dependencies return a neutral access result. + $this->assertTrue((new AccessGroupAnd())->access('view', $this->account, TRUE)->isNeutral()); + + $andNeutral = new AccessGroupAnd(); + $andNeutral->addDependency($allowedAccessible)->addDependency($neutralAccessible); + $this->assertTrue($andNeutral->access('view', $this->account, TRUE)->isNeutral()); + + $andForbidden = $andNeutral; + $andForbidden->addDependency($forbiddenAccessible); + $this->assertTrue($andForbidden->access('view', $this->account, TRUE)->isForbidden()); + + // Ensure that groups added to other groups works. + $andGroupsForbidden = new AccessGroupAnd(); + $andGroupsForbidden->addDependency($andNeutral)->addDependency($andForbidden); + $this->assertTrue($andGroupsForbidden->access('view', $this->account, TRUE)->isForbidden()); + // Ensure you can add a non-group accessible object. + $andGroupsForbidden->addDependency($allowedAccessible); + $this->assertTrue($andGroupsForbidden->access('view', $this->account, TRUE)->isForbidden()); + } + +} diff --git a/core/modules/block_content/tests/src/Unit/Access/AccessibleTestingTrait.php b/core/modules/block_content/tests/src/Unit/Access/AccessibleTestingTrait.php new file mode 100644 index 0000000000000000000000000000000000000000..8aab22782b05a9f1ed7a2af99ba5db6e862216f8 --- /dev/null +++ b/core/modules/block_content/tests/src/Unit/Access/AccessibleTestingTrait.php @@ -0,0 +1,36 @@ +prophesize(AccessibleInterface::class); + $accessible->access('view', $this->account, TRUE) + ->willReturn($accessResult); + return $accessible->reveal(); + } + +} diff --git a/core/modules/block_content/tests/src/Unit/Access/DependentAccessTest.php b/core/modules/block_content/tests/src/Unit/Access/DependentAccessTest.php new file mode 100644 index 0000000000000000000000000000000000000000..2be368601d7e0d637328567dc421de3a12ea11f4 --- /dev/null +++ b/core/modules/block_content/tests/src/Unit/Access/DependentAccessTest.php @@ -0,0 +1,160 @@ +account = $this->prophesize(AccountInterface::class)->reveal(); + $this->forbidden = $this->createAccessibleDouble(AccessResult::forbidden('Because I said so')); + $this->neutral = $this->createAccessibleDouble(AccessResult::neutral('I have no opinion')); + } + + /** + * Test that the previous dependency is replaced when using set. + * + * @covers ::setAccessDependency + * + * @dataProvider providerTestSetFirst + */ + public function testSetAccessDependency($use_set_first) { + $testRefinable = new RefinableDependentAccessTraitTestClass(); + + if ($use_set_first) { + $testRefinable->setAccessDependency($this->forbidden); + } + else { + $testRefinable->addAccessDependency($this->forbidden); + } + $accessResult = $testRefinable->getAccessDependency()->access('view', $this->account, TRUE); + $this->assertTrue($accessResult->isForbidden()); + $this->assertEquals('Because I said so', $accessResult->getReason()); + + // Calling setAccessDependency() replaces the existing dependency. + $testRefinable->setAccessDependency($this->neutral); + $dependency = $testRefinable->getAccessDependency(); + $this->assertFalse($dependency instanceof AccessGroupAnd); + $accessResult = $dependency->access('view', $this->account, TRUE); + $this->assertTrue($accessResult->isNeutral()); + $this->assertEquals('I have no opinion', $accessResult->getReason()); + } + + /** + * Tests merging a new dependency with existing non-group access dependency. + * + * @dataProvider providerTestSetFirst + */ + public function testMergeNonGroup($use_set_first) { + $testRefinable = new RefinableDependentAccessTraitTestClass(); + if ($use_set_first) { + $testRefinable->setAccessDependency($this->forbidden); + } + else { + $testRefinable->addAccessDependency($this->forbidden); + } + + $accessResult = $testRefinable->getAccessDependency()->access('view', $this->account, TRUE); + $this->assertTrue($accessResult->isForbidden()); + $this->assertEquals('Because I said so', $accessResult->getReason()); + + $testRefinable->addAccessDependency($this->neutral); + /** @var \Drupal\block_content\Access\AccessGroupAnd $dependency */ + $dependency = $testRefinable->getAccessDependency(); + // Ensure the new dependency create a new AND group when merged. + $this->assertTrue($dependency instanceof AccessGroupAnd); + $dependencies = $dependency->getDependencies(); + $accessResultForbidden = $dependencies[0]->access('view', $this->account, TRUE); + $this->assertTrue($accessResultForbidden->isForbidden()); + $this->assertEquals('Because I said so', $accessResultForbidden->getReason()); + $accessResultNeutral = $dependencies[1]->access('view', $this->account, TRUE); + $this->assertTrue($accessResultNeutral->isNeutral()); + $this->assertEquals('I have no opinion', $accessResultNeutral->getReason()); + + } + + /** + * Tests merging a new dependency with an existing access group dependency. + * + * @dataProvider providerTestSetFirst + */ + public function testMergeGroup($use_set_first) { + $andGroup = new AccessGroupAnd(); + $andGroup->addDependency($this->forbidden); + $testRefinable = new RefinableDependentAccessTraitTestClass(); + if ($use_set_first) { + $testRefinable->setAccessDependency($andGroup); + } + else { + $testRefinable->addAccessDependency($andGroup); + } + + $testRefinable->addAccessDependency($this->neutral); + /** @var \Drupal\block_content\Access\AccessGroupAnd $dependency */ + $dependency = $testRefinable->getAccessDependency(); + + // Ensure the new dependency is merged with the existing group. + $this->assertTrue($dependency instanceof AccessGroupAnd); + $dependencies = $dependency->getDependencies(); + $accessResultForbidden = $dependencies[0]->access('view', $this->account, TRUE); + $this->assertTrue($accessResultForbidden->isForbidden()); + $this->assertEquals('Because I said so', $accessResultForbidden->getReason()); + $accessResultNeutral = $dependencies[1]->access('view', $this->account, TRUE); + $this->assertTrue($accessResultNeutral->isNeutral()); + $this->assertEquals('I have no opinion', $accessResultNeutral->getReason()); + } + + /** + * Dataprovider for all test methods. + * + * Provides test cases for calling setAccessDependency() or + * mergeAccessDependency() first. A call to either should behave the same on a + * new RefinableDependentAccessInterface object. + */ + public function providerTestSetFirst() { + return [ + [TRUE], + [FALSE], + ]; + } + +} + +/** + * Test class that implements RefinableDependentAccessInterface. + */ +class RefinableDependentAccessTraitTestClass implements RefinableDependentAccessInterface { + + use RefinableDependentAccessTrait; + +}