Split out from #2957425: Allow the inline creation of non-reusable Custom Blocks in the layout builder

Problem/Motivation

When you create a Custom Block entity, a Block Plugin definition is derived and is available anywhere Block Plugin definitions are listed. This makes a lot of sense when Custom Blocks are meant to be re-used, for example you may want a "Logo" Custom Block on many pages in different regions.

However when using Custom Blocks within the Layout Builder the current functionality has many problems:
Performance
When using something like Panels or Layout Builder, Custom Blocks are often only meant to be used once. If you're building out hundreds of pages you could end up with thousands of Custom Blocks and lead to performance issues. This blocks would shown on every place you are able to pick a block including the layout builder and the current block place listing.

Proposed resolution

Add a new base field to Custom Blocks that determines if they are re-usable. If a Custom Block is not re-usable, it should not have a derived Block Plugin definition and should not be visible in the default "Custom Block library" View listings.

Non-reusable blocks access will be determined by:

  1. Access to the block itself, existing logic and
  2. Access to a new access dependency which the modules using non-reusable blocks are responsible for setting.

Modules using non-reusable blocks should when possible call $block->setAccessDependency() before access is called on the block. The dependency is not saved with the block content entity but called when interacting with the block object.

In the case when $block->setAccessDependency() has not been called before $block->access(), probably because another module is accessing access, the BlockContentAccessControlHandler will fire a new BlockContentGetDependencyEvent that will allow modules to set access the access dependency.

Non-reusable block will be used by the Layout Builder to create inline blocks in #2957425: Allow the inline creation of non-reusable Custom Blocks in the layout builder.

Remaining tasks

Add testing for changes to \Drupal\block_content\BlockContentAccessControlHandler::checkAccess

User interface changes

None this issue will not exposed the creation of the non-reusable blocks.
Non-reusable blocks will be used in #2957425: Allow the inline creation of non-reusable Custom Blocks in the layout builder

API changes

BlockContentInterface will have 3 new methods to deal with the new reusable base field.

  1. isReusable
  2. setReusable
  3. setNonReusable

Data model changes

New reusable field on block_content entity type. The default value would be TRUE so that all existing blocks would function the same as they currently do.

CommentFileSizeAuthor
#116 2976334-upgrade-tests-115.patch621 bytespenyaskito
#107 2976334-107.patch11.24 KBtedbow
#107 interdiff-104-107.txt1.74 KBtedbow
#104 2976334-104.patch9.78 KBtedbow
#104 interdiff-102-104.txt1.54 KBtedbow
#102 2976334-102.patch9.62 KBtedbow
#91 2976334-91.patch72.39 KBtedbow
#91 interdiff-89-91.txt15.26 KBtedbow
#89 2976334-89.patch76.58 KBtedbow
#89 interdiff-87-89.txt99.32 KBtedbow
#87 2976334-87.patch75.52 KBtedbow
#87 interdiff-85-87.txt5.68 KBtedbow
#85 2976334-85.patch75.64 KBtedbow
#85 interdiff-84-85.txt11.17 KBtedbow
#84 2976334-82.patch75.21 KBtedbow
#84 interdiff-78-82.txt4.28 KBtedbow
#78 2976334-76.patch75.21 KBtedbow
#78 interdiff-72-76.txt16.65 KBtedbow
#75 2976334-75.patch75.16 KBtedbow
#75 interdiff-72-75.patch16.61 KBtedbow
#72 access_group_idea-do-not-test.patch7.01 KBtedbow
#72 interdiff-70-72.txt7.73 KBtedbow
#72 2976334-72.patch59.57 KBtedbow
#70 2976334-70.patch59.02 KBtedbow
#70 interdiff-68-70.txt12.03 KBtedbow
#68 interdiff-66-68.txt28.14 KBtedbow
#68 2976334-68.patch58.33 KBtedbow
#66 2976334-65.patch56.76 KBtedbow
#64 2976334-64.patch54.19 KBtedbow
#64 interdiff-62-64.txt5.73 KBtedbow
#62 2976334-62.patch51.03 KBtedbow
#62 interdiff-60-62.txt6.07 KBtedbow
#60 2957425-layout_builder_only-do-not-test.patch57.53 KBtedbow
#60 2957425-162_plus_2976334-59.patch104.33 KBtedbow
#59 2957425-59-back_to_reusable.patch46.8 KBtedbow
#33 2976334-33.patch45.66 KBtedbow
#33 interdiff-29-33.txt1.11 KBtedbow
#29 2976334-27.patch45.71 KBtedbow
#29 interdiff-27-29.txt2.27 KBtedbow
#27 2976334-27.patch45.7 KBtedbow
#27 interdiff-25-27.txt7.9 KBtedbow
#25 2976334-25.patch41.53 KBtedbow
#25 interdiff-23-25.txt0 bytestedbow
#23 2976334-23.patch41.51 KBtedbow
#23 interdiff-22-23.txt25.05 KBtedbow
#22 2976334-22.patch31 KBtedbow
#22 interdiff-19-22.txt11.81 KBtedbow
#19 2976334-19.patch26.27 KBtedbow
#19 interdiff-18-19.txt10.04 KBtedbow
#18 2976334-18.patch21.75 KBtedbow
#18 interdiff-14-18.txt2.18 KBtedbow
#14 2976334-14.patch21.68 KBtedbow
#14 interdiff-10-14.patch780 bytestedbow
#10 2976334-10.patch21.69 KBtedbow
#10 interdiff-6-10.txt4.9 KBtedbow
#6 2976334-6.patch21.7 KBtedbow
#6 interdiff-2-6.txt10 KBtedbow
#2 2976334-2.patch20.68 KBtedbow
Support from Acquia helps fund testing for Drupal Acquia logo

Comments

tedbow created an issue. See original summary.

tedbow’s picture

Assigned: tedbow » Unassigned
Status: Active » Needs review
FileSize
20.68 KB
tedbow’s picture

Status: Needs review » Needs work

The last submitted patch, 2: 2976334-2.patch, failed testing. View results
- codesniffer_fixes.patch Interdiff of automated coding standards fixes only.

phenaproxima’s picture

  1. +++ b/core/lib/Drupal/Core/Access/AccessDependentInterface.php
    @@ -0,0 +1,26 @@
    +  /**
    +   * Sets the access dependee.
    +   *
    +   * @param \Drupal\Core\Access\AccessibleInterface $access_dependee
    +   *   The access dependee.
    +   */
    +  public function setAccessDependee(AccessibleInterface $access_dependee);
    

    "Dependee" is a strange word, can we clarify what it means in this context? Right now, it's not clear what is depending on what.

  2. +++ b/core/lib/Drupal/Core/Access/AccessDependentInterface.php
    @@ -0,0 +1,26 @@
    +  /**
    +   * Gets the access dependee.
    +   *
    +   * @return \Drupal\Core\Access\AccessibleInterface
    +   *   The access dependee.
    +   */
    +  public function getAccessDependee();
    

    What if no dependee has been defined?

  3. +++ b/core/lib/Drupal/Core/Access/AccessDependentTrait.php
    @@ -0,0 +1,32 @@
    +  /**
    +   * The object that is depended on.
    

    Also known as a "dependency", which might be a better word here. :)

  4. +++ b/core/modules/block_content/block_content.install
    @@ -138,3 +138,47 @@ function block_content_update_8400() {
    +  $reusable = BaseFieldDefinition::create('boolean')
    +    ->setLabel(t('Reusable'))
    +    ->setDescription(t('A boolean indicating whether this block is reusable.'))
    +    ->setDefaultValue(TRUE)
    +    ->setInitialValue(TRUE);
    

    Rather than completely copy the field definition, it might be preferable to directly invoke BlockContent::baseFieldDefinitions() and use what comes back from that.

  5. +++ b/core/modules/block_content/block_content.install
    @@ -138,3 +138,47 @@ function block_content_update_8400() {
    +  $view = $config_factory->getEditable('views.view.block_content');
    

    We should check if $view->isNew() and return if it is, because that would mean that the block_content view had been deleted by the site owner. Also, we don't need to keep $conig_factory in a local variable.

  6. +++ b/core/modules/block_content/block_content.install
    @@ -138,3 +138,47 @@ function block_content_update_8400() {
    +      $view->set("$base.id", 'reusable');
    +      $view->set("$base.plugin_id", 'boolean');
    +      $view->set("$base.table", $base_table);
    +      $view->set("$base.field", "reusable");
    +      $view->set("$base.value", "1");
    +      $view->set("$base.entity_type", "block_content");
    +      $view->set("$base.entity_field", "reusable");
    

    I believe all of these calls can be chained.

  7. +++ b/core/modules/block_content/block_content.module
    @@ -105,3 +108,62 @@ function block_content_add_body_field($block_type_id, $label = 'Body') {
    + * Alter any query with all tags:
    

    Should be "Alters"

  8. +++ b/core/modules/block_content/block_content.module
    @@ -105,3 +108,62 @@ function block_content_add_body_field($block_type_id, $label = 'Body') {
    +  if ($query instanceof SelectInterface && $query->hasAllTags('entity_query_block_content', 'entity_reference') && array_key_exists('block_content_field_data', $query->getTables())) {
    

    I'm not a huge fan of the fact that this hard-codes the name of the data table. Can we get the name of the table from the entity type definition instead?

    Also, under what circumstances would the query not be an instance of SelectInterface?

  9. +++ b/core/modules/block_content/block_content.module
    @@ -105,3 +108,62 @@ function block_content_add_body_field($block_type_id, $label = 'Body') {
    +      // If no reusable condition set to one.
    

    Set to TRUE, not one.

  10. +++ b/core/modules/block_content/block_content.module
    @@ -105,3 +108,62 @@ function block_content_add_body_field($block_type_id, $label = 'Body') {
    + * Utility function find nested conditions using the reusable field.
    

    "...function to find..."

  11. +++ b/core/modules/block_content/block_content.module
    @@ -105,3 +108,62 @@ function block_content_add_body_field($block_type_id, $label = 'Body') {
    + * @param mixed $condition
    + *   The conditions to check.
    

    Won't the condition either be an array or ConditionInterface? We can probably improve this type hint.

  12. +++ b/core/modules/block_content/block_content.module
    @@ -105,3 +108,62 @@ function block_content_add_body_field($block_type_id, $label = 'Body') {
    +  $field = isset($condition['field']) ? $condition['field'] : NULL;
    

    Under what circumstances will $condition['field'] be NULL?

  13. +++ b/core/modules/block_content/block_content.module
    @@ -105,3 +108,62 @@ function block_content_add_body_field($block_type_id, $label = 'Body') {
    +  if (is_object($field) && $field instanceof Condition) {
    

    I think we should use ConditionInterface instead.

  14. +++ b/core/modules/block_content/block_content.module
    @@ -105,3 +108,62 @@ function block_content_add_body_field($block_type_id, $label = 'Body') {
    +    return _block_content_has_reusable_condition($field->conditions());
    

    We should probably check $condition first before recursing downwards, so that we can return earlier.

  15. +++ b/core/modules/block_content/block_content.module
    @@ -105,3 +108,62 @@ function block_content_add_body_field($block_type_id, $label = 'Body') {
    +      if (is_int($key) && is_array($nested_condition)) {
    

    The is_int() makes me nervous, because it's coupling itself to deep implementation details of the Condition class. Not 100% sure how to work around that, though.

  16. +++ b/core/modules/block_content/src/BlockContentAccessControlHandler.php
    @@ -19,10 +20,24 @@ class BlockContentAccessControlHandler extends EntityAccessControlHandler {
    +      if (!$entity instanceof AccessDependentInterface) {
    +        throw new \Exception("Non-reusable block entities must implement \Drupal\Core\Access\AccessDependentInterface for access control.");
    +      }
    

    I'm not sure why we'd have this check. Can't we just add AccessDependentInterface to BlockContentInterface, so that all custom blocks implement it? Also, I think this should be \LogicException.

  17. +++ b/core/modules/block_content/src/Entity/BlockContent.php
    @@ -200,6 +204,11 @@ public static function baseFieldDefinitions(EntityTypeInterface $entity_type) {
    +    $fields['reusable'] = BaseFieldDefinition::create('boolean')
    +      ->setLabel(t('Reusable'))
    +      ->setDescription(t('A boolean indicating whether this block is reusable.'))
    +      ->setDefaultValue(TRUE);
    

    Why is this different from the field definition in the update hook?

  18. +++ b/core/modules/block_content/src/Entity/BlockContent.php
    @@ -282,6 +291,21 @@ public function setRevisionLogMessage($revision_log_message) {
    +    $this->set('reusable', $reusable);
    +    return $this;
    

    I think $this->set() already returns $this, so the second line is redundant.

tedbow’s picture

Status: Needs work » Needs review
FileSize
10 KB
21.7 KB

@phenaproxima thanks for the review!

1. Changing to "access dependency". Added a longer description in AccessDependentInterface.
2. Changing the @return description to mention it might be NULL.
3. changed.
4. Looking at other modules that add new fields and block_content module when it adds fields it seems that the field definitions are always duplicated.
For example block_content_update_8003 adds new field this way.
5. fixed
6. fixed
7. fixed
8. Good call, using \Drupal::entityTypeManager()->getDefinition('block_content')->getDataTable(); to get the data table.

Also, under what circumstances would the query not be an instance of SelectInterface?

I am not sure but since you can apply any tags to any query then this could an update query or any object that implements \Drupal\Core\Database\Query\AlterableInterface so trying to be safe here.
9. fixed
10. fixed
11, 13, 14, 15 I want to improve _block_content_has_reusable_condition but I want to add test cases to BlockContentEntityReferenceSelectionTest where the selection has a condition on reusable that is a conjunction or nested condition.
12. When $condition['#conjunction'] is set.
16. I think it is possible to switch out the class that handles the entity. But perhaps we always return AccessForbidden in this case and just log the warning?
Also now that think about it maybe the plugin deriver added in #2957425: Allow the inline creation of non-reusable Custom Blocks in the layout builder should check that the class for the entity implements AccessDependentInterface and log a warning if does not and not make derivatives since they won't work anyways. This is probably an edge case but could happen.
17. fixed
18. fixed.

Status: Needs review » Needs work

The last submitted patch, 6: 2976334-6.patch, failed testing. View results
- codesniffer_fixes.patch Interdiff of automated coding standards fixes only.

johnwebdev’s picture

Great work @tedbow!

  1. +++ b/core/modules/block_content/block_content.install
    @@ -138,3 +138,47 @@ function block_content_update_8400() {
    +function block_content_update_8600() {
    ...
    +function block_content_update_8601() {
    

    Why do we need two separate update hooks?

  2. +++ b/core/modules/block_content/block_content.install
    @@ -138,3 +138,47 @@ function block_content_update_8400() {
    +  if ($view->get('base_table') !== $base_table or $view->isNew()) {
    

    or => ||

  3. +++ b/core/modules/block_content/src/BlockContentAccessControlHandler.php
    @@ -19,10 +20,24 @@ class BlockContentAccessControlHandler extends EntityAccessControlHandler {
    +      if (empty($dependee)) {
    +        return AccessResult::forbidden("Non-reusable blocks must set an access dependee for access control.")->addCacheableDependency($dependee);
    

    Would it make sense to make this optional? i.e. I have a block that should not be re-usable (... or derivable) but is not used by a dependee. I'm not entirely sure when (and if) that would be applicable though.

  4. +++ b/core/modules/block_content/src/Plugin/Derivative/BlockContent.php
    @@ -43,7 +43,7 @@ public static function create(ContainerInterface $container, $base_plugin_id) {
    +    $block_contents = $this->blockContentStorage->loadByProperties(['reusable' => TRUE]);
    

    I think it was asked for a profiling of this change in the earlier issue.

johnwebdev’s picture

Issue tags: +Needs change record
tedbow’s picture

Status: Needs work » Needs review
FileSize
4.9 KB
21.69 KB

@johndevman thanks for the review!
re #8
1. I am not sure about this. I thought that we keep update hooks separate that do 2 different things even if we add them at them at the same time.
This would also keep the descriptions separate when seen when running updates via drush or drupal console. Not sure on this.
2. 😁Well I guess I make a new mistake every way. Fixed
3. Making this required means that any existing custom code that deals BlockContent entities and call ::access() will always get an access forbidden for non-reusable blocks unless it is updated. I think we have to be very careful because we are messing with a stable module.

Here is edge case where I think we want the AccessForbidden by default.

  1. A site has "tags" field on a Video Block Type
  2. Site builder place these blocks on the site and tags it with related terms
  3. On node page they have custom code(maybe in custom block "Related block videos") that finds all the blocks that are tagged with a tag used on the current node , picks a random 1 and renders it.
  4. They install Layout Builder after #2957425: Allow the inline creation of non-reusable Custom Blocks in the layout builder🤞
  5. A user adds a inline (non-reusable) Video block in the Layout Builder for access controlled node. They adds tags for the video

If we don't forbidden access unless !empty($dependee) then non-resuable will rendered in this "Related block videos" section.
The custom code could be update to only return $reusable === TRUE block entities but I think we want to do everything we possibly can to not exposed this now access controlled blocks. Without any existing code having to be updated.
4. Yes @eclipseGC wants to make sure if we start to have thousands of non-reusable blocks then we don't start to have the kind of performance problems we would have now if we created thousands of blocks.

From 4) I see that I am still using "dependee" here changing to "dependency".

Also in slack @phenaproxima mentioned

I would suggest that AccessDependentInterface actually extend AccessibleInterface, so that you can have a chain of access dependencies, which are then computed recursively.

I thought this was good idea and did this in #6. But when I was making the combined patch for #2957425: Allow the inline creation of non-reusable Custom Blocks in the layout builder I realized this actually cause a problem because the new block plugin InlineBlockContent. But \Drupal\Core\Block\BlockPluginInterface::access is not from AccessibleInterface so this causes a problem. Since other interfaces have the concept of access but don't implement AccessibleInterface and in this will need AccessDependentInterface this won't work. Changing back.

Status: Needs review » Needs work

The last submitted patch, 10: 2976334-10.patch, failed testing. View results
- codesniffer_fixes.patch Interdiff of automated coding standards fixes only.

johnwebdev’s picture

#10

Here is edge case where I think we want the AccessForbidden by default.

So the problem with that edge case is that we break that either way, if the block is denied access there will be no block rendered at all. Though I agree, it's probably better to NOT render it, than rendering something that SHOULD not be rendered.

tedbow’s picture

#12
That is good point. but yes I it much worse, a security issue, to render something that user doesn't have access to.

If I had added here

finds all the blocks that are tagged with a tag used on the current node , picks a random 1 and renders it.

picks a random 1 that the user has access to that would have been more correct. Because of course even though the block_content module itself has limited access control any other module could implement hook_ENTITY_TYPE_access so we should always check access.

In that case we wouldn't break this example because it would pick one the user had access to which would filter out the non-reusable blocks, as it would filter out any other blocks the user didn't have access to for any other reason.

tedbow’s picture

Status: Needs work » Needs review
FileSize
780 bytes
21.68 KB
+++ b/core/modules/block_content/tests/src/Kernel/BlockContentEntityReferenceSelectionTest.php
@@ -0,0 +1,113 @@
+use Drupal\Tests\token\Kernel\KernelTestBase;

Whoops was using a class for a contrib project in my project 😟.

Fixed. The full console output of the test failure told me the actually error.

Also now excluding the /modules folder in phpstorm which should make this harder to happen again.

The last submitted patch, 14: interdiff-10-14.patch, failed testing. View results

samuel.mortenson’s picture

+++ b/core/modules/block_content/block_content.module
@@ -105,3 +108,63 @@ function block_content_add_body_field($block_type_id, $label = 'Body') {
+function block_content_query_block_content_access_alter(AlterableInterface $query) {
+  $data_table = \Drupal::entityTypeManager()->getDefinition('block_content')->getDataTable();
+  if ($query instanceof SelectInterface && $query->hasAllTags('entity_query_block_content', 'entity_reference') && array_key_exists($data_table, $query->getTables())) {
+    $reusable_conditions = array_filter($query->conditions(), '_block_content_has_reusable_condition');
+    if (empty($reusable_conditions)) {
+      // If no reusable condition create a condition set to TRUE.
+      $query->condition("$data_table.reusable", TRUE);
+    }
+  }
+}

Is this hook implementation necessary? Entity queries check access by default and the access control handler for block content has been modified to deny access unless an access dependency is present.

tedbow’s picture

Yes this is necessary.

Entity queries check access by default and the access control handler for block content has been modified to deny access unless an access dependency is present.

I don't think this is true.

\Drupal\Core\Entity\Plugin\EntityReferenceSelection\DefaultSelection::buildEntityQuery() explicitly adds the access tag.
in

// Add entity-access tag.
    $query->addTag($target_type . '_access');

But then does not call $entity->access() so the access control handler is not called.

It is up to the module providing the entity type to actually control access like node_query_node_access_alter() does.

That is why without this hook implementation \Drupal\Tests\block_content\Kernel\BlockContentEntityReferenceSelectionTest::testReferenceableEntities will fail and return the non-reusable block.

+++ b/core/modules/block_content/block_content.module
@@ -105,3 +108,63 @@ function block_content_add_body_field($block_type_id, $label = 'Body') {
+function block_content_query_block_content_access_alter(AlterableInterface $query) {

I changed this to block_content_query_entity_reference_alter() because it is not really access that we want to alter but rather what should be able to be re-used.

The user may actually have access to a non-reusable block in a sense that they have access to the parent entity but that does not mean we want them to be able reference them in a entity reference field.

Because the blocks are non-reusable we should not let them be reused by being referenced in an entity reference value. It just doesn't make sense.

This won't even work because entity reference is not going to call setDependency() to so the access would alway be false.

What actually happens without this hook.

  • Set up entity reference field that reference custom blocks
  • Select a non-reusable block(this actually works regardless of whether you have access to the layout entity)
  • Save entity with reference field
  • You can't see the custom block b/c no access
  • edit entity with reference
  • get access error on existing entity reference field value

If a contrib module wanted to make a EntityReferenceSelection plugin that let you reference non-reusable blocks then they could do this by explicitly setting condition on reusable in their query.

tedbow’s picture

I meant to update a patch with #17

Here it is
Also change the location of $data_table = \Drupal::entityTypeManager()->getDefinition('block_content')->getDataTable(); to after the initial checks for performance reasons. Now this will not be called for every 'entity_reference' query.

tedbow’s picture

This patch:

  • Cleans up _block_content_has_reusable_condition()

    Among other things re @phenaproxima comment #5

    Won't the condition either be an array or ConditionInterface? We can probably improve this type hint.

    This will always be an array now.

    The is_int() makes me nervous, because it's coupling itself to deep implementation details of the Condition class. Not 100% sure how to work around that, though.

    Not doing this anymore.

  • Expands test coverage \Drupal\Tests\block_content\Kernel\BlockContentEntityReferenceSelectionTest::testReferenceableEntities() by creating \Drupal\block_content_test\Plugin\EntityReferenceSelection\TestSelection which has a test module and sets condition on reusable in various ways to make sure the expected referenceable entities are returned.
phenaproxima’s picture

Status: Needs review » Needs work
Issue tags: +Needs upgrade path tests

Just about everything I found is a doc comment thing. I think this makes a lot of sense, and it's clean! However, we are going to need a test of the update path.

  1. +++ b/core/lib/Drupal/Core/Access/AccessDependentInterface.php
    @@ -0,0 +1,41 @@
    + * @code
    + * $accessible->getAccessDependee()->access($op, $account, TRUE);
    + * @endcode
    

    getAccessDependee() no longer exists.

  2. +++ b/core/lib/Drupal/Core/Access/AccessDependentInterface.php
    @@ -0,0 +1,41 @@
    +interface AccessDependentInterface {
    

    Should this extend AccessibleInterface?

  3. +++ b/core/lib/Drupal/Core/Access/AccessDependentInterface.php
    @@ -0,0 +1,41 @@
    +   * @param \Drupal\Core\Access\AccessibleInterface $access_dependency
    +   *   The access dependency.
    

    Can we rephrase the description: "The object upon which access depends"?

  4. +++ b/core/lib/Drupal/Core/Access/AccessDependentInterface.php
    @@ -0,0 +1,41 @@
    +  public function setAccessDependency(AccessibleInterface $access_dependency);
    

    Should this method return $this?

  5. +++ b/core/modules/block_content/block_content.install
    @@ -138,3 +138,47 @@ function block_content_update_8400() {
    +  // Make sure the view's 'base_table' is correct and the View is not new. The
    +  // default view could have been deleted and possibly the view ID could be used
    +  // for a different view.
    

    Can we rephrase the second sentence for clarity -- something like "The default view may have been replaced by a completely different one with the same ID."

  6. +++ b/core/modules/block_content/block_content.module
    @@ -105,3 +108,69 @@ function block_content_add_body_field($block_type_id, $label = 'Body') {
    +    if (array_key_exists($data_table, $query->getTables())) {
    +      if (!_block_content_has_reusable_condition($query->conditions())) {
    

    These can be combined: if (array_key_exists(...) && !_block_content_has_reusable_condition(...)

  7. +++ b/core/modules/block_content/block_content.module
    @@ -105,3 +108,69 @@ function block_content_add_body_field($block_type_id, $label = 'Body') {
    +    foreach ($condition as $nested_condition) {
    +      if (is_array($nested_condition)) {
    

    We can remove a level of nesting if we change this to: foreach (array_filter($condition, 'is_array')) as $nested_condition)

  8. +++ b/core/modules/block_content/block_content.module
    @@ -105,3 +108,69 @@ function block_content_add_body_field($block_type_id, $label = 'Body') {
    +    return strpos($field_parts[0], $data_table) === 0 && $field_parts[1] === 'reusable';
    

    Can we add a comment explaining why strpos() is called?

  9. +++ b/core/modules/block_content/src/BlockContentAccessControlHandler.php
    @@ -19,10 +20,24 @@ class BlockContentAccessControlHandler extends EntityAccessControlHandler {
    +        return AccessResult::forbidden("Non-reusable blocks must set an access dependency for access control.")->addCacheableDependency($dependency);
    

    Why are we adding $dependency as a cacheable dependency if it's empty?

  10. +++ b/core/modules/block_content/src/Entity/BlockContent.php
    @@ -75,7 +77,9 @@
    +class BlockContent extends EditorialContentEntityBase implements BlockContentInterface, AccessDependentInterface {
    

    Can we get away with having BlockContentInterface extend AccessDependentInterface? Or would that be a BC break? (I think such a change might be covered under the 1:1 class/interface rule...?)

  11. +++ b/core/modules/block_content/tests/src/Kernel/BlockContentDeriverTest.php
    @@ -0,0 +1,66 @@
    +/**
    + * Tests block content plugin deriver.
    + *
    + * @group block_content
    + */
    +class BlockContentDeriverTest extends KernelTestBase {
    

    O_O Did we not already have coverage for this?!

  12. +++ b/core/modules/block_content/tests/src/Kernel/BlockContentDeriverTest.php
    @@ -0,0 +1,66 @@
    +  /**
    +   * Tests that reusable blocks only are derived.
    +   */
    

    For clarity: "Tests that only reusable blocks are derived".

  13. +++ b/core/modules/block_content/tests/src/Kernel/BlockContentDeriverTest.php
    @@ -0,0 +1,66 @@
    +      'description' => "Provides a block type that increases your site's spiffiness by up to 11%",
    

    Yeah, but this one goes up to 11. ;)

johnwebdev’s picture

  • Can we expand on test coverage for the views and BlockContentListBuilder to make sure they don't expose non-reusable blocks?
  • Re #10 and #8 we still need to do that profiling
  • I think we need tests for the update hooks. i.e BlockContentUpdateTest
tedbow’s picture

Status: Needs work » Needs review
FileSize
11.81 KB
31 KB

re #20 @phenaproxima thanks for the review again!

1. fixed
2. We can't if we use it for InlineBlockContent in #2957425: Allow the inline creation of non-reusable Custom Blocks in the layout builder because it conflicts \Drupal\Core\Block\BlockPluginInterface::access()with (see #10).
3. fixed
4. fixed
5. fixed
6. fixed
7. fixed
8. Added a comment. It is because there could a suffix like "block_content_field_data_2" on nested conditions. I don't think it is enough to test for "reusable" field name because this could on table that is joined also.
9. fixed. Changed to add as a cacheable dependency if it is not empty and ::access() is called.
10. Thanks for asking this! I thought we couldn't but I re-read the policy and found

Interfaces that are not tagged with either @api or @internal can be safely used as type hints. No methods will be changed or removed from these interface in a breaking way.
However, we reserve the ability to add methods to these interfaces in minor releases to support new features.

So yes we can! Any class implementing this interface is suppose to extend BlockContent so that the new method won't be problem(explained in the policy).
11. Nope, @johndevman added this in the first patch in #2957425: Allow the inline creation of non-reusable Custom Blocks in the layout builder where this patch started. but also there wasn't really any logic in this deriver before.
12. Fixed
13. Small nit here the "%" makes the joke not work, but will leave, 😜

@johndevman thanks for taking another look

  • Can we expand on test coverage for the views and BlockContentListBuilder to make sure they don't expose non-reusable blocks?

    Good point BlockContentListBuilder was actually exposing non-reusable blocks! Fixed and added test coverage.

  • Re #10 and #8 we still need to do that profiling

    Thanks for bring this up again! I made #2978102: Profile 1000s of custom non-reusable blocks for performance so we don't forget.

    But also this got me thinking about performance.
    In \Drupal\block_content\Entity\BlockContent::postSave() and ::postDelete() we call ::invalidateBlockPluginCache()

    This is because up until now every Custom Block entity corresponds to a Block Plugin. But now reusable blocks don't have an individual plugin.

    So we don't always need to do this.

    In ::postDelete() will only need to clear the cache if there is a reusable block.

    In ::postSave() we need to clear cache if $this->isReusable() || (isset($this->original) && $this->original->isReusable()).
    Though this patch doesn't allow changing blocks from reusable to non-reusable custom code or contrib might.

  • I think we need tests for the update hooks. i.e BlockContentUpdateTest

    Yep, I need to research how to do this.

tedbow’s picture

  • This patch adds BlockContentReusableUpdateTest
    thanks @tim.plunkett and @phenaproxima for helping me understand what I need to do for the update test.
    It tests
    The 'reusable' field is configured correctly after the update
    The custom block library view page is updated with the reusable filter
    That an extra display on this view will also be updated with the filter.

    Also added

    ->setTranslatable(FALSE)
    ->setRevisionable(FALSE)
    

    when creating the reusable field.

  • #2978102: Profile 1000s of custom non-reusable blocks for performance now has been test by myself, @johndevman, and @johnzzon
    We all found that making 10000 reusable blocks make the site not load but making 20000 non-reusable blocks works fine.
    This makes sense \Drupal\block_content\Plugin\Derivative\BlockContent is not picking up these 20000 blocks and having 20000 content entities is no problem for Drupal.

    Also #2957425: Allow the inline creation of non-reusable Custom Blocks in the layout builder which need this current issue should also not have a problem because on 1 new plugin derivative will be create for each custom block *type* not entity.

    #2978102: Profile 1000s of custom non-reusable blocks for performance I won't close now because it anyone else want to test that issue it would be great. But don't think it should be blocker now that the performance has been tested.

    Could @johnzzon get commit credit here since testing that issue really enables this 1? (@johndevman obviously should already get commit credit for his helpful reviews here and but also he started this patch off in #2957425 where this code started!)

Status: Needs review » Needs work

The last submitted patch, 23: 2976334-23.patch, failed testing. View results
- codesniffer_fixes.patch Interdiff of automated coding standards fixes only.

tedbow’s picture

Status: Needs work » Needs review
FileSize
0 bytes
41.53 KB

Whoops forgot to add @group to the new test file which is for sure a totally new thing that I for sure don't forget to do almost every time I add a new test file 😜

tedbow’s picture

Started the change record: https://www.drupal.org/node/2978419

The only thing that concerns when explaining the change(not finished yet) is:

The existing Custom block library view that is included in the Custom Block module has been updated to only show reusable blocks. For existing sites this View is also update automatically to filter on only reusable blocks. For any other Views that have been created to show Custom block entities they will have to be manually updated to filter out non-reusable blocks. If these Views are not updated and the View renders Custom Block fields then the entity field will be rendered regardless of the user access to non-reusable blocks

Does this matter? Isn't this the case for all entity fields except nodes which are filtered out?

On thing we could do is to implement hook_views_query_alter and never return non-reusable blocks. This might make sense because you can never call setAccessDependency() on the block you can never really get access.

This would the absolute safest and would remove the need for block_content_update_8601 which updates the existing View.

UPDATE
I played around with Views a little and reminded myself that, yes, this how Views handles entity field rendering and it only checks access on nodes in core I think when filtering.

So since this is expected behavior I say we do 2 things

  1. Update block_content_update_8601() to update all Views with the new resuable == TRUE filter. This will make sure all existing Custom Views don't select non-reusable blocks because that could expose fields that are now access sensitive.
  2. Make sure that all new Custom Block views that are created with the Wizard have a default resuable == TRUE filter.
tedbow’s picture

This patch does the suggestion I made #26

  1. As per @jibran's suggestion in #2957425-147: Allow the inline creation of non-reusable Custom Blocks in the layout builder I changed block_content_update_8601 to block_content_post_update_add_views_reusable_filter
  2. Then changed block_content_post_update_add_views_reusable_filter to update all Custom block views with the new reusable == TRUE filter.
  3. Added \Drupal\block_content\Plugin\views\wizard\BlockContent which adds reusable == TRUE filter to all new Custom block views

Status: Needs review » Needs work

The last submitted patch, 27: 2976334-27.patch, failed testing. View results
- codesniffer_fixes.patch Interdiff of automated coding standards fixes only.

tedbow’s picture

Status: Needs work » Needs review
Issue tags: -Needs change record
FileSize
2.27 KB
45.71 KB
  1. Fixed namespace that caused failure in #27
  2. Fixed codesniffer problems problems from #28
  3. Also update the change record for changes in #27. Could use reviews!

Status: Needs review » Needs work

The last submitted patch, 29: 2976334-27.patch, failed testing. View results
- codesniffer_fixes.patch Interdiff of automated coding standards fixes only.

tedbow credited johnzzon.

tedbow’s picture

Just notice the "Credit others" field here. Can I update this?

Adding @johnzzon because help on #2978102: Profile 1000s of custom non-reusable blocks for performance

tedbow’s picture

Status: Needs work » Needs review
FileSize
1.11 KB
45.66 KB
+++ b/core/modules/block_content/tests/src/Functional/Views/BlockContentWizardTest.php
@@ -0,0 +1,52 @@
+namespace Drupal\Tests\block_content\Functional\Views\Wizard;

Namespace was still wrong! 😅

johnwebdev’s picture

Looks great to me, +1 to RTBC this.

tim.plunkett’s picture

I think this needs a retitle, as this issue seems decoupled from LB (with that work occurring in #2957425: Allow the inline creation of non-reusable Custom Blocks in the layout builder).

tedbow’s picture

Title: Allow non-reusable blocks for placement in the Layout Builder » Allow Custom blocks to be set as non-reusable adding access restriction based on where it was used.

Changing title

larowlan’s picture

  1. +++ b/core/lib/Drupal/Core/Access/AccessDependentInterface.php
    @@ -0,0 +1,43 @@
    +  public function setAccessDependency(AccessibleInterface $access_dependency);
    ...
    +  public function getAccessDependency();
    

    What if there is more than one dependency?

  2. +++ b/core/modules/block_content/block_content.module
    @@ -105,3 +108,65 @@ function block_content_add_body_field($block_type_id, $label = 'Body') {
    +function block_content_query_entity_reference_alter(AlterableInterface $query) {
    

    There is hook_entity_query_TAG_alter which would be a more focussed hook?

  3. +++ b/core/modules/block_content/block_content.module
    @@ -105,3 +108,65 @@ function block_content_add_body_field($block_type_id, $label = 'Body') {
    +function _block_content_has_reusable_condition(array $condition) {
    

    wow, this sucks, entity query sure doesn't support introspection does it

    feels like we should add such an api (follow up)

  4. +++ b/core/modules/block_content/block_content.post_update.php
    @@ -0,0 +1,39 @@
    +  foreach ($config_factory->listAll('views.view.') as $view_config_name) {
    

    We have a config entity updater utility class now, this should use that

  5. +++ b/core/modules/block_content/tests/modules/block_content_view_override/config/install/views.view.block_content.yml
    @@ -4,6 +4,8 @@ dependencies:
    +_core:
    +  default_config_hash: gkRJCqHr3uSO8ALHLatX-7YKfX0lWEgkC5qMBtCf_Sg
    

    do we normally have these in core yml files?

  6. +++ b/core/modules/block_content/tests/src/Kernel/BlockContentEntityReferenceSelectionTest.php
    @@ -0,0 +1,166 @@
    +      'description' => "Provides a block type that increases your site's spiffiness by up to 11%",
    

    scientists agree

So, whilst I get the intent of the patch, I have a question.

Are we sure we're not forcing a square peg into a round hole.

If we need a limited use content entity that is for layout plugins only, why don't we just create one and avoid bolting this functionality onto an existing entity type. Content entities are cheap and easy to create in Drupal 8.

Doing so would avoid the need for the access dependency thing and all of the entity query/views stuff.

We already have a way to say 'don't show this block in the normal block UI', because the field plugin deriver does that. Those field blocks are available to layout builder, but not to the normal 'block layout' page. That sounds exactly like what we want here. A new content entity, with a block deriver, but limited only to layout builder usage.

I know that is a dramatic change of direction - but it feels better to purpose build something and have complete control over it than to bolt something onto existing code.

Thoughts?

tedbow’s picture

@larowlan thanks for the review!

No patch right now just comments.

1. Yes I had it as multiple dependencies in this issue or one of the related on earlier but someone convinced be not do it b/c we were currently using it.

I guess if we make this specifically for the Block Content use case we should probably keep it in the module.

If it is more general we should change to supporting multiple.

2. I didn't find that but will look.

3. Yeah if we just care about the top level it would be easier.

Regarding make a new content entity type....

I think the main reason to stick with Custom Blocks is because it would be what site builders expect. I talked to about 4-5 site builders and asked them what they would expect if they could make makes inline in the new Layout Builder and they all said they would expect to be able to use there existing Block Types. I would encourage other to check and see.

If it was new content entity type then if you had a Video, Map, and Image block and you wanted the functionality in both the block UI and layout builder you would have to keep those bundle fields in sync across both entity types.

In #2974864: [Meta] Determine best method of allowing inline content creation Layout Builder we talk about the idea making an entity type that would automatically mirror the fields I think that would be even more of pain, especially for any existing contrib and custom code.

Also I think if we can avoid the situation like the Drupal 7 contrib space where we had boxes, beans, and fieldable panel panes I think that would be good.

Fieldable Panels Panes already has on there project page:

There will be no port of Fieldable Panels Panes to Drupal 8, custom block types provide all of the functionality that is necessary.

Bean has:

As Drupal 8 supports fields on its block entities, this module is not needed for new sites.

So in addition to the site that have already customized their block types for use with Block UI many sites are have already probably put in a good level of effort building out their custom block types.

I think it would be a big win if they "just worked" with the layout builder.

I did start to look at the new entity type way #2974860: Allow inline content creation in layout builder through a single purpose entity type
but after talking with people and several layout initiative meetings it seemed like reusing Content Blocks had big advantages.

Content entities are cheap and easy to create in Drupal 8.

I think they are cheap programmatically but not site builder-wise as far as having many content entity types to manage.

johnwebdev’s picture

Having multiple dependencies interface used on BlockContent is misleading, because it would only ever use one.

Maybe we could rename it better to explain the composition we're actually after?

tedbow’s picture

@larowlan I reopened #2974864-8: [Meta] Determine best method of allowing inline content creation Layout Builder and added some comments about I why think reusing the existing entity type is better.

That issue discussed a few possible alternatives.

tedbow’s picture

Title: Allow Custom blocks to be set as non-reusable adding access restriction based on where it was used. » Allow Custom blocks to have parent set adding access restriction based on parent
Issue summary: View changes
FileSize
52.52 KB
46.93 KB
  1. This patch changes to add Parent entity to Custom Blocks rather than reusable flag
  2. Access is determined in \Drupal\block_content\BlockContentAccessControlHandler::checkAccess() by checking access to parent similar to the previous logic that required the AccessDependentInterface. But because block itself keeps track of it's parent the code checking access to the block doesn't have to call ::setAccessDependee().
  3. I have add has_parent filter to views that uses a boolean_string filter on parent_entity_type(id may be removed if parent deleted). This is added to all existing custom block views and the new wizard adds them to new views too(of course can be removed).
  4. I have not but was thinking about removing the parent_entity_type and parent_entity_id field from Views altogether in \Drupal\block_content\BlockContentViewsData::getViewsData() because these don't seem very useful. We could add a @todo to make views relation ship to the parent entity with these fields as follow up.

    The reason have not done this is there may be use if you leave them.
    For example you use the EVA module.

.

Status: Needs review » Needs work

The last submitted patch, 41: 2976334-41.patch, failed testing. View results
- codesniffer_fixes.patch Interdiff of automated coding standards fixes only.

phenaproxima’s picture

EDIT: Redacted.

tedbow’s picture

Status: Needs work » Needs review
FileSize
2.76 KB
46.83 KB
  1. Remove local debug line I left in
  2. Updated block_content_update_8600() to use NULL for default values
  3. fixed problems from https://dispatcher.drupalci.org/job/drupal_patches/62290/artifact/jenkin...
larowlan’s picture

  1. +++ b/core/modules/block_content/block_content.install
    @@ -138,3 +138,29 @@ function block_content_update_8400() {
    +    ->setLabel(t('Parent entity type'))
    

    oh for DER in core :)

  2. +++ b/core/modules/block_content/block_content.module
    @@ -105,3 +108,67 @@ function block_content_add_body_field($block_type_id, $label = 'Body') {
    + * These queries should only return with no parent blocks unless a condition on
    + * parent is explicitly set.
    

    english is off here

  3. +++ b/core/modules/block_content/block_content.module
    @@ -105,3 +108,67 @@ function block_content_add_body_field($block_type_id, $label = 'Body') {
    + * create a instance of
    

    an instance?

  4. +++ b/core/modules/block_content/block_content.module
    @@ -105,3 +108,67 @@ function block_content_add_body_field($block_type_id, $label = 'Body') {
    +function _block_content_has_parent_entity_condition(array $condition) {
    

    feels like this should be something generic we could use

  5. +++ b/core/modules/block_content/block_content.post_update.php
    @@ -0,0 +1,39 @@
    +  foreach ($config_factory->listAll('views.view.') as $view_config_name) {
    +    $view = $config_factory->getEditable($view_config_name);
    +    if ($view->get('base_table') != $data_table) {
    

    we have the config updater service for this

  6. +++ b/core/modules/block_content/src/BlockContentAccessControlHandler.php
    @@ -19,10 +19,23 @@ class BlockContentAccessControlHandler extends EntityAccessControlHandler {
    +        $access = $access->andIf($parent_entity->access($operation, $account, TRUE))->addCacheableDependency($entity);
    

    should we ->addCacheableDependency($parent_entity) too?

    if so, missing test coverage for that?

  7. +++ b/core/modules/block_content/src/Entity/BlockContent.php
    @@ -200,6 +210,24 @@ public static function baseFieldDefinitions(EntityTypeInterface $entity_type) {
    +    // @todo Is there a core issue to add
    +    //   https://www.drupal.org/project/dynamic_entity_reference
    

    ha - jinx!

  8. +++ b/core/modules/block_content/src/Entity/BlockContent.php
    @@ -290,4 +318,40 @@ protected static function invalidateBlockPluginCache() {
    +      return \Drupal::entityTypeManager()->getStorage($this->get('parent_entity_type')->value)->load($this->get('parent_entity_id')->value);
    

    should we catch plugin not found exception here in case parent_entity_type is mangled/references a deleted entity type?

  9. +++ b/core/modules/block_content/src/Entity/BlockContent.php
    @@ -290,4 +318,40 @@ protected static function invalidateBlockPluginCache() {
    +    // If either parent field value is set then the block is considered to have
    +    // a parent.
    +    return !empty($this->get('parent_entity_type')->value) || !empty($this->get('parent_entity_id')->value);
    

    shouldn't we need both for it to be valid?

  10. +++ b/core/modules/block_content/tests/src/Functional/Update/BlockContentParentEntityUpdateTest.php
    @@ -0,0 +1,168 @@
    +    $this->runUpdates();
    

    we should assert that the field and filter aren't present before we run the updates, to prevent people inadvertently updating the fixture and rendering this test redundant

  11. +++ b/core/modules/block_content/tests/src/Functional/Update/BlockContentParentEntityUpdateTest.php
    @@ -0,0 +1,168 @@
    +    file_put_contents('/Users/ted.bowman/Sites/www/test.html', $this->getSession()->getPage()->getOuterHtml());
    

    debug?

tedbow’s picture

@larowlan thanks for the review!

1. Is there an issue for issue for bringing DER into core? I checked https://www.drupal.org/project/ideas but I didn't see it.

2.Fixed

3. fixed and cleared up sentence

4. Should we add a @todo and follow up issue to add to \Drupal\Core\Database\Query\ConditionInterface with a implementation in \Drupal\Core\Database\Query\QueryConditionTrait

with something like:
public function hasConditionOnField($table_name, $field_name)

5. Changed to use new config updater service.
Left a comment in the handbook that we should update the doc page to demo this service: https://www.drupal.org/node/2535454/discuss

6. This section was wrong:
I removed both calls to ->addCacheableDependency($entity) inside the if ($entity->hasParentEntity()) { block.

I changed:
return AccessResult::forbidden('Parent entity not available.')->addCacheableDependency($entity);
$access = $access->andIf(AccessResult::forbidden('Parent entity not available.'));(returned later)

Using \Drupal\Core\Access\AccessResult::andIf() ensures whatever cachable metadata was added earlier in the code will be passed on.

For instance ->addCacheableDependency($entity) is called but also AccessResult::allowedIfHasPermission($account, 'administer blocks')) maybe called which adds cache context 'user.permissions'.

I removed the call to ->addCacheableDependency($entity) from here

$access = $access->andIf($parent_entity->access($operation, $account, TRUE))->addCacheableDependency($entity);
also. I had meant to had $parent_entity but that is the responsibility of the call to the parent entity access to handle this so it is not necessary.

8.. I am not sure what to do here.

I don't think we want to catch the exception and then not return a parent. Because then code might assume this block is parent free. I think any code should call ::hasParent() first then assume there should be parent even if ::getParentEntity() doesn't return one, like BlockContentAccessControlHandler does. My worry is that if just ::getParentEntity() is called and for some reason it doesn't return a parent because we caught the exception then it will be assumed it should not have a parent.

9.
Yes both should be valid but I think I was being over cautious.

I have entangled the logic of #2957425: Allow the inline creation of non-reusable Custom Blocks in the layout builder around deleting parent entities which I need to fix.

Basically when a layout parent entity is deleted in some cases I need to mark content block to be delete. I was setting parent_entity_id to NULL but leaving the parent_entity_type. Otherwise for parent entity types that don't use a datatable there is no way to tell if a blocks parent has been deleted except going through every single content with the parent type.

In some cases because of parent revisions there could be hundreds of content blocks for a single parent in the layout use case. So we can't delete all the content block entities on hook_entity_delete for the parent.

I will fix this is a follow up patch.

10. Fixed.

11. Yep I removed that in #44.

tedbow’s picture

UPDATE: I tried a simpler approach below in #49 which might make more sense

Ok this moves the logic of deleting blocks with parents from inside layout_builder in #2957425: Allow the inline creation of non-reusable Custom Blocks in the layout builder into block_content.

Basically the logic assumes that when a parent entity is deleted that the block entities with this parent should be deleted. This the case for layout_builder and it seems logical if it really is a parent.

Here is how it works

  1. On hook_entity_delete() if the parent entity or block_content entities are not using a datatable then all blocks with entity being deleted as the parent are update to remove parent_entity_id. This will be used later to delete these blocks on cron.
  2. In the case that where the entity types are using datatables we don't need to do anything on hook_entity_delete() because there is another way to determine which block_content entities should be deleted. This allows for the use case of having hundreds block_content entities to have the same parent. For instance nodes using the layout builder for overrides with hundreds of revisions using different block_content entities.
  3. On cron we get the blocks with deleted parents and delete them....
  4. For parent entity types using a datatable with select all block_content entities where the parent entity no longer exists(determined via "notExists" subquery)
  5. For parent entity types NOT using a datatable with select block_content entities where the parent_entity_id is NULL

Status: Needs review » Needs work

The last submitted patch, 47: 2957425-47.patch, failed testing. View results
- codesniffer_fixes.patch Interdiff of automated coding standards fixes only.

tedbow’s picture

Status: Needs work » Needs review
FileSize
14.99 KB
60.25 KB
  1. Looking back at #47 the logic is very complicated in \Drupal\block_content\BlockContentWithParentDeleter dealing with parents being deleted differently depending on whether the entity type uses a datatable for storage.

    This was to get around using a usage or deletion table.

    This patch simplifies that logic by adding the table block_content_delete which stores the ids of blocks that should be deleted when the parent is deleted.

    The logic is must simpler. It also should work if there would be hundreds of blocks where the parent entity is config entity type. Right now in Layout Builder I can't see that case because the only config entity type that would be parent entities would be 'entity_view_display' entities but maybe it could happen in another use case.

  2. This means when have to add check if the parent is deleted in BlockContentAccessControlHandler because
    // If the blocks parent has been deleted then access is forbidden. This
    // must be checked before loading the parent entity because if the
    // parent entity type allows arbitrary IDs then the entity type ID could
    // be reused after the original parent entity was deleted.
    
  3. This will have the same failure in BlockContentParentEntityUpdateTest because we are using the API to delete the View entity and fields have not been added yet.
  4. @todo add a test for logic in BlockContentAccessControlHandler

Status: Needs review » Needs work

The last submitted patch, 49: 2976334-49-deleted_table.patch, failed testing. View results

johnwebdev’s picture

Can't we just add a property to manage the state of the entity if it should be deleted or not, like File entity (file_managed) does?

tedbow’s picture

Assigned: Unassigned » tedbow

@johndevman I thought about that but if we had base field parent_status(status is taken) but then we would still have the update the block entities and call $block->save() which could be a problem if there are hundreds(thousands) of blocks using the deleted entity as a parent.

UPDATE: I looked into this and I think actually we could get away with not calling $block->save(). If we do this in storage handler like \Drupal\block_content\BlockContentStorage::setParentStatus I think this would be ok because it would be swappable. This allow use to update hundreds of records with 1 update query.

I looked at other storage handlers in core and this is done to update a base field in \Drupal\taxonomy\TermStorage::resetWeights()

I think this is similar use case.

I am working on patch to do this.

tedbow’s picture

Assigned: tedbow » Unassigned
Status: Needs work » Needs review
FileSize
20.48 KB
58.57 KB

Ok this patch

  1. Adds parent_status field to BlockContent.
  2. Removes block_content_delete table.
  3. I added BlockContentStorage with 2 methods update parent_status on parent delete and delete blocks with this setting.
  4. Removes BlockContentWithParentDeleter because now BlockContentStorage can just be used directly
  5. Changes BlockContentAccessControlHandler to deny access if $entity->get('parent_status')->value === BlockContentInterface::PARENT_DELETED
  6. Fix the test fail in BlockContentParentEntityUpdateTest
  7. Update BlockContentDeriverTest to make sure the plugin shows up after $block_content->removeParentEntity()
tedbow’s picture

This patch

  1. Codesniffer fixes
  2. BlockContentParentEntityUpdateTest updates to check configuration of parent_status field
  3. Fixed _block_content_has_parent_entity_condition to base table name on alias so check for exact match not if (strpos($field_parts[0], $data_table) === 0) {
  4. Cleanup BlockContentEntityReferenceSelectionTest to use dataprovider and to also test 'parent_status' field. Now tests to make sure that if any conditions are set on any of these fields ['parent_entity_id', 'parent_entity_type', 'parent_status'] then _block_content_has_parent_entity_condition will not alter the query.

Status: Needs review » Needs work

The last submitted patch, 54: 2957425-54.patch, failed testing. View results

tedbow’s picture

Working on #2957425-160: Allow the inline creation of non-reusable Custom Blocks in the layout builder I realized the access changes here won't work for layout_builder. 😿

tl;dr; If an inline block is removed from a node in a previous revision the user can still pass 'view' access on the block if they access to the published node revision.

See the other issue for more detail and failing tests that demonstrate the problem.

But related to this issue:

I am leaning towards adding back AccessDependentInterface or at least adding these methods to BlockContentInterface. Then maybe instead of having reusable as a base field add dependent because I think this more descriptive.

........

The downside, and maybe it actually isn't a downside because access is much stricter, is that would couldn't render these blocks easily outside of layout builder because would have to call setDependent().

tedbow’s picture

Status: Needs work » Needs review
FileSize
64.64 KB
30.01 KB
44.23 KB

This patch:

  1. Re #56 this patch reverts back to "reusable" blocks instead of block with parents
  2. Brings back reusable using #33 as a reference. I didn't just revert back to this patch because there were many improvements since then that can be kept. I will provide an interdiff for 33 though to see the changes since the patch was previously using reusable blocks

The last submitted patch, , failed testing. View results

tedbow’s picture

Whoops! In #57 I only include the diff of core/modules/block_content and forgot that there were changes to core/lib

tedbow’s picture

Ok this brings back to using reusable blocks from #33 but with bringing improvements up to #54 such as testing improvements.

It has the inline_block_content_usage table to track blocks that used in the Layout builder so they be deleted.

Berdir’s picture

Didn't really review yet, but one scenario we might want to explicitly test are private files. That's AFAIK the main use case where entity are being checked for view access outside of their usual context that's actually testable from the perspective of a user. So create a block type with private field and then make sure the file can be accessed as expected, e.g. not when it only exists as a pending revision on a node.

That said, there's AFAIK actually a bug with having it working at all on non-default revisions of an entity, that might prevent that kind of test for now, not sure.

tedbow’s picture

@Berdir thanks for bring up private files.

I have the updated the combined patch on #2957425-167: Allow the inline creation of non-reusable Custom Blocks in the layout builder to handle the access to private files case in Layout Builder attached to non-reusable custom blocks with tests.
Since file usage only stores entity id and revision ID there needs some Layout Builder specific logic to determine which revision of the entity should be considered the access dependency.

So I added an event to\Drupal\block_content\BlockContentAccessControlHandler::checkAccess that fires if the block is non-reusable and the dependency has not been set. In the layout builder the only case I know of where this would happen is when checking view or download access for private file attached to an inline block. The event allows modules to set the dependency for a block. If a module doesn't set a dependency in the event the then access is denied so it defaults to no access.

This would also be fired if you had a view or custom code was rendering non-reusable blocks. In #2957425 we update views so they filter out non-reusable blocks. So it seem like this will not be common case.

tedbow’s picture

Issue summary: View changes
tedbow’s picture

Ok here is test coverage for BlockContentAccessControlHandler.

Right not it just covers 'view' operation.

I had a question about other operations. If you have 'update' or 'delete' access to the access dependency should you have the same access to the non-reusable block?

Layout Builder does not make this assumption. It controls editing on these blocks so if you access to the layout you can update/delete the block.

But right now if you knew the ID of a non-reusable block you could put it in the path block/[ID] and be able to edit the block. Layout Builder should not work this way because you configuring layout is separate permission from editing the entity.

So I think there are to options:

  1. In BlockContentAccessControlHandler if the block is not reusable return AccessResultNeutral if the operation is not 'view'
  2. . Any module wanting to change this would implement hook_block_content_access to alter that access. Layout Builder would have to do nothing.

  3. Or leave BlockContentAccessControlHandler the way it is now where access for all operations depends on the access to the same operation on the access dependency. Then any module like Layout Builder would have to implement hook_block_content_access to alter that access.

I am leaning towards 2) because even though layout builder is our first use case for non-reusable the access seems more like a special case.

Status: Needs review » Needs work

The last submitted patch, 64: 2976334-64.patch, failed testing. View results

tedbow’s picture

Status: Needs work » Needs review
FileSize
56.76 KB

Left out files outside of block_content interdiff in #64 is still valid

Wim Leers’s picture

I reviewed 80% of the patch, I didn't review all the test coverage in detail yet. I think you'll find helpful feedback in here already:

  1. +++ b/core/lib/Drupal/Core/Access/AccessDependentInterface.php
    @@ -0,0 +1,43 @@
    + * Objects should implement this interface when their access depends on access
    + * to another object that implements \Drupal\Core\Access\AccessibleInterface.
    ...
    +interface AccessDependentInterface {
    

    Woah! Impressive. We've long needed this.

    To my ears, DependentAccessInterface sounds better, but that's perhaps because I'm a non-native speaker.

  2. +++ b/core/lib/Drupal/Core/Access/AccessDependentInterface.php
    @@ -0,0 +1,43 @@
    + * dependency object. Objects that implement this interface are responsible for
    + * checking access of the access dependency because the dependency may not take
    + * effect in all cases. For instance an entity may only need the access
    

    Can you think of consequences for other parts in Drupal? AFAICT this handily shifts responsibility to every class that implements this interface; because it causes any AccessibleInterface implementation to need to update their ::access() method to take this into account. I think that's fine.

  3. +++ b/core/lib/Drupal/Core/Access/AccessDependentInterface.php
    @@ -0,0 +1,43 @@
    +  public function setAccessDependency(AccessibleInterface $access_dependency);
    

    Ideally this interface would not have any setters.

    I checked and … the only caller of this method is a test! So it's totally possible to do this without the setter 🤞

  4. +++ b/core/lib/Drupal/Core/Access/AccessDependentInterface.php
    @@ -0,0 +1,43 @@
    +  public function getAccessDependency();
    

    What if there are multiple?

    Also: shouldn't this interface extend AccessibleInterface?

  5. +++ b/core/modules/block_content/block_content.module
    @@ -105,3 +108,72 @@ function block_content_add_body_field($block_type_id, $label = 'Body') {
    + * as entity reference values. A module can still
    + * create an instance of
    

    Whitespace nit.

  6. +++ b/core/modules/block_content/block_content.module
    @@ -105,3 +108,72 @@ function block_content_add_body_field($block_type_id, $label = 'Body') {
    + * a condition on either the 'reusable' field.
    

    Incomplete sentence?

  7. +++ b/core/modules/block_content/block_content.module
    @@ -105,3 +108,72 @@ function block_content_add_body_field($block_type_id, $label = 'Body') {
    +      // If no reusable condition create a condition set to TRUE.
    

    Not very clear. This is already explained in the docblock. Let's just remove this.

  8. +++ b/core/modules/block_content/block_content.module
    @@ -105,3 +108,72 @@ function block_content_add_body_field($block_type_id, $label = 'Body') {
    +function _block_content_has_reusable_condition(array $condition, array $tables) {
    

    I wish there was an API for this sort of thing, because this code definitely looks both complex and wonky. Those two adjectives in one sentence make it scary.

    🙈 + 📚 = 😱

  9. +++ b/core/modules/block_content/block_content.post_update.php
    @@ -0,0 +1,46 @@
    + * Adds a 'reusable' filter to Custom Block views.
    

    s/to/to all/

  10. +++ b/core/modules/block_content/src/BlockContentEvents.php
    @@ -0,0 +1,28 @@
    +  const INLINE_BLOCK_GET_DEPENDENCY = 'block_content.get_dependency';
    

    This is the only mention of "inline" in both this patch and in the block_content module. Let's rename that.

  11. +++ b/core/modules/block_content/src/Entity/BlockContent.php
    @@ -282,6 +302,20 @@ public function setRevisionLogMessage($revision_log_message) {
    +  public function setReusable($reusable = TRUE) {
    

    This should not accept a parameter. We need:

    public function setReusable();
    public function setUnreusable();
    

    This is the pattern that \Drupal\Core\Entity\EntityPublishedInterface introduced.

  12. +++ b/core/modules/block_content/tests/modules/block_content_view_override/config/install/views.view.block_content.yml
    @@ -468,7 +468,7 @@ display:
    -      max-age: 0
    +      max-age: -1
    
    @@ -493,5 +493,110 @@ display:
    -      max-age: 0
    +      max-age: -1
    

    Why this change? (I like it, but why is it happening in this issue/patch? What change is triggering it?)

tedbow’s picture

@Wim Leers Thanks for the review! 👍🐎
1. hmm. the way it is now sounds better to me. Will leave for now but interested in others opinions
UPDATE: talke to @tim.plunkett he likes this change also so will change.
2. I think it is fine. In the non-reusable block usage we don't care about the access dependency if the block is reusable(all blocks currently are).

I think other use cases would likely also not have simple always check access dependency for access logic.

3. There are calls to the setter in layout_builder in the related issue which is dependent on this one #2957425: Allow the inline creation of non-reusable Custom Blocks in the layout builder
4.
In a previous version of the patch I had multiple. @larowlan asked the same question in #37
For the non-reusable blocks it makes sense to have only 1 because if it is truely not reusable it would only it would only have 1 dependent(if had 2 you are reusing it)

That being said I could see other uses of the this interface needing multiple dependencies. Do we change it so the other possible future uses could incorporated?

If we do change it do we enforce it in \Drupal\Core\Access\AccessDependentTrait::setAccessDependencies that should only 1 dependencies or could there be cases the dependencies are different from where it is "used" which should be only 1.

Also if we change to multiple should the logic in \Drupal\block_content\BlockContentAccessControlHandler::checkAccess() check that you have access to all dependencies?

Leaving for feedback.

UPDATE: Talked with @tim.plunkett about this and he mentioned that in the past when we have opted for one vs array in these cases we have gotten burned.

Changing it to handle multiples. In \Drupal\block_content\BlockContentAccessControlHandler::checkAccess() i have changed to require access to all dependencies.

5. fixed
6. fixed.
7. removed
8 @larowlan also suggested this. Created a new issue #2984930: Add hasCondition() method to \Drupal\Core\Database\Query\ConditionInterface and added a @todo here.
9. fixed
10. fixed.
11. Added setNonreusable this sounds better to me.
12. This was mistake. This is in the patch because I copied the core/modules/block_content/config/optional/views.view.block_content.yml into a test module to make sure a view with multiple displays gets updated by block_content_post_update_add_views_reusable_filter. When I exported the View it most have updated it.

I also updated \Drupal\Tests\block_content\Kernel\BlockContentAccessHandlerTest() to test UPDATE and DELETE operations

EclipseGc’s picture

Status: Needs review » Needs work
  1. +++ b/core/lib/Drupal/Core/Access/DependentAccessInterface.php
    @@ -0,0 +1,43 @@
    + * $accessible->getAccessDependency()->access($op, $account, TRUE);
    ...
    +  public function setAccessDependencies(array $access_dependencies);
    

    Plural. Also worth noting that this is the perfect place to introduce variadics. It's sad to see us creating a new interface that could benefit so much from that a version before we have it. :-( oh well d9.

    For myself, I'd prefer an "addAccessDependency()" method that guarantees the AccessibleInterface object is passed, but once I've gone that far, I start to wonder about and/or logic and how we have to iterate the getAccessDependencies() method to do this properly. It leaves me wanting 2 classes that just implement AccessibleInterface for and/or that take an array of dependencies in the constructor and just call the access() method.

  2. +++ b/core/lib/Drupal/Core/Access/DependentAccessTrait.php
    @@ -0,0 +1,32 @@
    +    $this->accessDependencies = $access_dependencies;
    

    No error checking to ensure AccessibleInterface.

  3. +++ b/core/modules/block_content/block_content.module
    @@ -105,3 +108,73 @@ function block_content_add_body_field($block_type_id, $label = 'Body') {
    +function _block_content_has_reusable_condition(array $condition, array $tables) {
    

    Any good reason not to make this a service?

  4. +++ b/core/modules/block_content/src/BlockContentAccessControlHandler.php
    @@ -2,27 +2,87 @@
    +        $event = new BlockContentGetDependenciesEvent($entity);
    +        $this->eventDispatcher->dispatch(BlockContentEvents::BLOCK_CONTENT_GET_DEPENDENCIES, $event);
    +        $dependencies = $entity->getAccessDependencies();
    

    This is great, and is probably more evidence to support an addAccessibleDependency() method on the DependentAccessInterface interface above. That being said, I still think there's an opportunity here to use custom dependent access and/or classes in this case to simplify things, but that could totally be done in a follow up or at some later date.

  5. +++ b/core/modules/block_content/src/BlockContentInterface.php
    @@ -48,6 +49,28 @@ public function setInfo($info);
    +  public function setNonreusable();
    

    maybe setNotReusable() ??

  6. +++ b/core/modules/block_content/src/Event/BlockContentGetDependenciesEvent.php
    @@ -0,0 +1,40 @@
    +  public function getBlockContent() {
    

    getEntity() maybe? I know it's a block content entity, but I'd be lost first time I saw this. Maybe it doesn't matter.

    I'd totally add a addAccessDependency() method to this event and use that to interact with the entity properly. That actually means you could abstract away from entities doing all this work later and just introspect the entity to make decision (if at some point that makes sense to do).

    Obviously there are some other methods that might make sense to add on the event once you do that.

All in all I really like this patch and I think it's super close to being ready. We're probably too late in the cycle to seriously consider my DependentAccessAnd/Or class suggestion, but other than that, I think the rest is pretty straight forward and on point. Super excited about this.

Eclipse

tedbow’s picture

Status: Needs work » Needs review
FileSize
12.03 KB
59.02 KB

@EclipseGc thanks for the review!

1. In #68 I changed DependentAccessInterface to allow adding multiple dependencies because a few reviewers asked about this and it seemed it allowed for flexible uses of this interface.

But this required a change to BlockContentAccessControlHandler

      foreach ($dependencies as $dependency) {
        if (empty($dependency->in_preview)) {
          $access = $access->andIf($dependency->access($operation, $account, TRUE));
        }
      }

so this locks the access control to be an "AND" of all the dependencies. Obviously other access control handlers that are handling an entity that implements DependentAccessInterface can do different logic. but the code calling $entity->setDependencies() won't have control over this.

I think the code above $dependency->in_preview also points to an assumption theat the dependencies are going to entities but DependentAccessInterface actually expects just AccessibleInterface implementations.

I think looking at DependentAccessInterface again without the assumption that the dependencies will be entities actually allows for more flexibility for the code calling $entity->setDependency() with a singular dependency.

Right now we don't have a way to group AccessibleInterface implementations but what if introduced(in a follow up) AccessibleGroupInterface which would extend AccessibleInterface.

This would allow code like
$entity->setDependency(new AccessGroupAnd([$entity1, $entity2]))
or even
$entity->setDependency(new AccessGroupAnd([$entity1, new AccessGroupOr([$user1, $user2])]))
Because both expects implementations of AccessibleInterface and is itself an implementation of AccessibleInterface this would allow complex nested access logic.
Then because AccessibleGroupInterface would extend AccessibleInterface the handler logic could always simply be
$entity->getDependency()->access($operation, $account, TRUE)
The handler doesn't need to care if the dependency is a single entity(EntityInterface extends AccessibleInterface) or complex nested AccessibleGroupInterface.

2. Doesn't apply because shifting back to singular dependency.
3. Was going to leave because this will eventually move to \Drupal\Core\Database\Query\ConditionInterface in #2984930: Add hasCondition() method to \Drupal\Core\Database\Query\ConditionInterface. Though we could make this @internal service for now. No change right now.
4. I think my point in 1) would make this very flexible.
5. changed
6 I change it to getBlockContentEntity() since the dependency might also be any entity(though not necessarily) I thought this was clearer.

Added setAccessDependency() and getAccessDependency() because of the changes I made in 1).

Also changed the #2957425: Allow the inline creation of non-reusable Custom Blocks in the layout builder to use this setAccessDependency(), patch to be posted soon.

Wim Leers’s picture

#68: 👍👍

#68.3: Okay, so #2957425: Allow the inline creation of non-reusable Custom Blocks in the layout builder also has some calls to the setter. Is it actually necessary though? Every single time I've worked on a value object and added setters to it, I've regretted it later. I'd like to help us not introduce more of that. Immutable value objects prevents lots of frustrating bugs/debugging.

(Funny that I independently made several remarks that @larowlan also made :) And yay for consulting @tim.plunkett to speed up consensus building!)


#69:

  1. For myself, I'd prefer an "addAccessDependency()" method that guarantees the AccessibleInterface object is passed,

    +1

    This is also relevant wrt #68.3.

    interface CacheableDependencyInterface {
      public function getCacheContexts();
      public function getCacheTags();
      public function getCacheMaxAge();
    }
    

    +

    interface RefinableCacheableDependencyInterface extends CacheableDependencyInterface {
      …
      public function addCacheableDependency($other_object);
    }
    

    i.e. the default interface is immutable, the optional extra interface adds mutability. That'd be much more comfortable.

    So then you end up with:

    interface DependentAccessInterface {
      public function getAccessDependencies()
    }

    +

    interface RefinableDependentAccessInterface {
      public function addAccessDependency()
    }
    

    Although to address Kris' point, you could as well do assert(Inspector::assertAll(function($v) { return $v instanceof AccessibleInterface; }, $dependencies));.


#70:

I think the code above $dependency->in_preview also points to an assumption that the dependencies are going to entities […]

This is fine, use assert(Inspector::assertAll(…)); to verify that your precondition holds true. It's fine here because this is code specifically dealing with BlockContent entities. If this were generic code, it would be a problem.

Right now we don't have a way to group AccessibleInterface implementations but what if introduced(in a follow up) AccessibleGroupInterface which would extend AccessibleInterface.

… this is looking a lot like repeating the structure \Drupal\Core\Config\Entity\Query\Condition.


Patch review:

+++ b/core/lib/Drupal/Core/Access/DependentAccessInterface.php
@@ -25,19 +25,19 @@
-  public function getAccessDependencies();
+  public function getAccessDependency();

Huh? Why did this change back to single instead of multiple? I guess that's what you were getting at in #70.1, but I thought you were just theorizing there.

It doesn't make sense for me to review this patch in detail until we agree on single vs multiple.

I'd like to get @tim.plunkett's thoughts on this.

tedbow’s picture

@Wim Leers thanks for looking again.

    re setter on DependentAccessInterface
  1. Is it actually necessary though? Every single time I've worked on a value object and added setters to it, I've regretted it later.

    You convinced I removed it. Now we have RefinableDependentAccessInterface extends DependentAccessInterface for the setter.

  2. Re $dependency->in_preview

    It's fine here because this is code specifically dealing with BlockContent entities. If this were generic code, it would be a problem.

    Actually the $entity in \Drupal\block_content\BlockContentAccessControlHandler::checkAccess is a BlockContent entity. The $dependency is only guaranteed to be instance of AccessibleInterface. For the layout_builder use case this is actually always an FieldableEntityInterface but since we are adding DependentAcccesInterface to BlockContentInterface we can't be sure how other modules will use this.

  3. this is looking a lot like repeating the structure \Drupal\Core\Config\Entity\Query\Condition.
    I guess in a very lose since 🤷‍♂️. This much less complex because almost would be handled by \Drupal\Core\Access\AccessResultInterface::orIf() and \Drupal\Core\Access\AccessResultInterface::andIf

    I have attached access_group_idea-do-not-test.patch because I think it will be more clear(or PR 😅)

  4. Huh? Why did this change back to single instead of multiple? I guess that's what you were getting at in #70.1, but I thought you were just theorizing there.

    No, I think the single with the groups idea actually is more powerful and flexible because:

    1. It takes the need to understand how combine multiple instances of AccessibleInterface out of access handlers
    2. In the non-reusable blocks it would allow other uses of non-reusables to have different logic for multiple accessible. Currently layout builder doesn't need multiple depenedencies but other uses might and then might need AND or OR or some combo.
    3. It would allow much more complex logic

    Hopefully the attached patch access_group_idea-do-not-test.patch will show this

    It doesn't make sense for me to review this patch in detail until we agree on single vs multiple.

    Ok I would love to get consensus on this.

The last submitted patch, 72: 2976334-72.patch, failed testing. View results

Wim Leers’s picture

It takes the need to understand how combine multiple instances of AccessibleInterface out of access handlers

This is IMHO the best argument.

Ok I would love to get consensus on this.

+1 — can we get feedback from @tim.plunkett on this?

tedbow’s picture

I talked with @tim.plunkett about this problem.

He liked the idea of the DependentAccessInterface only having 1 dependency. Especially we can't simply have DependentAccessInterface implement AccessibleInterface itself if we want to use with Block plugins as layout builder will need because access would clash with \Drupal\Core\Block\BlockPluginInterface::access()(I think are problems beyond that)

But it doesn't want \Drupal\Core\Access\RefinableDependentAccessInterface to only have setAccessDependency() meaning you would be always erase previous access unless you did. He wanted to make sure that it also had mergeAccessDependency() so you can easily merge an accessible without erasing existing ones.

This means will really need the access group classes I added in the access_group_idea-do-not-test.patch.

So I have added them and tests for the access groups and the new mergeAccessDependency()

To explain how the merge works I will just copy in the docblock

/**
   * Merges an access dependency into the existing access dependency.
   *
   * If no existing dependency is currently set this will set the dependency
   * will be set to the new value.
   *
   * If there is an existing dependency and it does not implement
   * AccessibleGroupInterface the dependency will be set as a new AccessGroupAnd
   * instance with the existing and new dependencies as the members of the
   * group.
   *
   * If there is an existing dependency and it does implement
   * AccessibleGroupInterface the dependency well added to the existing access
   * group.
   *
   * @param \Drupal\Core\Access\AccessibleInterface $access_dependency
   *   The access dependency to merge.
   *
   * @return $this
   */
  public function mergeAccessDependency(AccessibleInterface $access_dependency);

The last submitted patch, 75: interdiff-72-75.patch, failed testing. View results

Status: Needs review » Needs work

The last submitted patch, 75: 2976334-75.patch, failed testing. View results
- codesniffer_fixes.patch Interdiff of automated coding standards fixes only.

tedbow’s picture

Status: Needs work » Needs review
FileSize
16.65 KB
75.21 KB
tim.plunkett’s picture

This matches my understanding of our conversation, I think it looks great!
Deferring to Wim for the final sign-off.

Status: Needs review » Needs work

The last submitted patch, 78: 2976334-76.patch, failed testing. View results
- codesniffer_fixes.patch Interdiff of automated coding standards fixes only.

Wim Leers’s picture

But it doesn't want \Drupal\Core\Access\RefinableDependentAccessInterface to only have setAccessDependency() meaning you would be always erase previous access unless you did. He wanted to make sure that it also had mergeAccessDependency() so you can easily merge an accessible without erasing existing ones.

I both agree and disagree with that!

  • I agree setAccessDependency() is bad, for the reason he cited: it overwrites (erases) previous data.
  • I agree with the concept of mergeAccessDependency() but disagree with its name: I think addAccessDependency() is better. I explained in detail why in #71. In short: because it then follows the example set by RefinableCacheableDependencyInterface::addCacheableDependency()
tim.plunkett’s picture

We went back and forth over add vs merge for a while.
Interesting that addCacheableDependency() is a merge operation and exists on the same interface as mergeCacheMaxAge()!

I'm fine with either name.

Wim Leers’s picture

Time for detailed patch review including nits!

  1. +++ b/core/lib/Drupal/Core/Access/AccessGroupAnd.php
    @@ -0,0 +1,17 @@
    +  protected function doCombineAccess(AccessResultInterface $accumulatedAccess, AccessResultInterface $dependencyAccess) {
    

    s/fooBar/foo_bar/ in parameter names.

  2. +++ b/core/lib/Drupal/Core/Access/AccessibleGroupBase.php
    @@ -0,0 +1,64 @@
    +   * Combines the access result of one dependency to previous dependencies.
    ...
    +  abstract protected function doCombineAccess(AccessResultInterface $accumulatedAccess, AccessResultInterface $dependencyAccess);
    

    I think compute would be clearer than do combine?

  3. +++ b/core/lib/Drupal/Core/Access/AccessibleGroupBase.php
    @@ -0,0 +1,64 @@
    +   *   The combine access result of previous dependencies.
    

    Accumulated? Or combined?

    In array_reduce() this is called the carry.

  4. +++ b/core/lib/Drupal/Core/Access/AccessibleGroupInterface.php
    @@ -0,0 +1,29 @@
    + * Extends AccessibleInterface to allow for access objects that have multiple
    + * dependencies.
    

    80 col.s

  5. +++ b/core/lib/Drupal/Core/Access/RefinableDependentAccessInterface.php
    @@ -0,0 +1,45 @@
    +  public function setAccessDependency(AccessibleInterface $access_dependency);
    

    I thought this was supposed to no longer be there?

  6. +++ b/core/lib/Drupal/Core/Access/RefinableDependentAccessInterface.php
    @@ -0,0 +1,45 @@
    +   * AccessibleGroupInterface the dependency well added to the existing access
    

    s/well/will be/

  7. +++ b/core/lib/Drupal/Core/Access/RefinableDependentAccessTrait.php
    @@ -0,0 +1,48 @@
    + * Implements \Drupal\Core\Access\RefinableDependentAccessInterface.
    

    Trait for FCQN_OF_INTERFACE.

  8. +++ b/core/lib/Drupal/Core/Access/RefinableDependentAccessTrait.php
    @@ -0,0 +1,48 @@
    +    if (!$this->accessDependency instanceof AccessibleGroupInterface) {
    

    elseif

  9. +++ b/core/modules/block_content/block_content.module
    @@ -105,3 +108,73 @@ function block_content_add_body_field($block_type_id, $label = 'Body') {
    + * @todo Replace this a function to a call to the API in
    + *   https://www.drupal.org/project/drupal/issues/2984930
    

    s/this a function to a call/this function with a call/

  10. +++ b/core/modules/block_content/src/BlockContentAccessControlHandler.php
    @@ -2,27 +2,87 @@
    +    if ($entity->isReusable() === FALSE) {
    

    This is inspecting the entity. Therefore you must precede it with $access->addCacheableDependency($entity) to ensure the access result varies correctly.

  11. +++ b/core/modules/block_content/src/BlockContentAccessControlHandler.php
    @@ -2,27 +2,87 @@
    +      if (!$dependency instanceof EntityInterface || empty($dependency->in_preview)) {
    

    I don't understand the rationale behind this if-test. It's probably obvious to you because you're so deep in this problem space. I think a brief comment would help a lot :)

  12. +++ b/core/modules/block_content/src/BlockContentEvents.php
    @@ -0,0 +1,28 @@
    +   * \Drupal\Core\Access\AccessDependentTrait::getAccessDependency has not been
    +   * called.
    
    1. It's DependentAccessInterface
    2. I don't think you mean the getter? :)
  13. +++ b/core/modules/block_content/src/BlockContentInterface.php
    @@ -48,6 +49,28 @@ public function setInfo($info);
    +   * Sets the block to be non-reusable.
    ...
    +  public function setNotReusable();
    

    Non vs not, let's be consistent.

  14. +++ b/core/modules/block_content/src/Entity/BlockContent.php
    @@ -282,6 +302,27 @@ public function setRevisionLogMessage($revision_log_message) {
    +    return (bool) $this->get('reusable')->value;
    
    return $this->get('reusable')->getCastedValue();
    
  15. +++ b/core/modules/block_content/tests/src/Functional/Rest/BlockContentResourceTestBase.php
    @@ -92,6 +92,11 @@ protected function getExpectedNormalizedEntity() {
    +      'reusable' => [
    +        [
    +          'value' => TRUE,
    +        ],
    +      ],
    

    New field added, hence also exposed via REST. Makes sense!

    (Let's also mention this in the CR.)

tedbow’s picture

Status: Needs work » Needs review
FileSize
4.28 KB
75.21 KB

#81 that makes sense to stick to existing naming pattern. Fixed.

#78 fail because update test need @group legacy now I guess

tedbow’s picture

@Wim Leers Thanks for detail review!!!
1. fixed
2. Changing the name to combineAcces because "combine" is in the method docs of \Drupal\Core\Access\AccessResultInterface for andIf and orIf. And we are calling this.
3. Changing to "The combined access" b/c 2)
4 exactly 80 now
5. No we wanted to leave this otherwise there is not way to replace all the dependencies with your own. The comment details that existing dependency will be replaced.
6. fixed.
7. fixed
8. fixed
9. fixed
10. fixed
11. add a comment.
12. Changing the comment slightly as now we \Drupal\block_content\Event\BlockContentGetDependencyEvent::setAccessDependency() this allows setting the dependency on the event and not changing the block at all.
13, changed to setNonReusable as this is used everywhere.
14. oh forgot about that. fixed
Nope, that threw an error.
Only: return $this->get('reusable')->first()->get('value')->getCastedValue(); worked.
Thats the only way I could get a \Drupal\Core\TypedData\Plugin\DataType\BooleanData
wow. Drupal, wow
15 👍

larowlan’s picture

Status: Needs review » Needs work
  1. +++ b/core/lib/Drupal/Core/Access/AccessibleGroupBase.php
    @@ -0,0 +1,64 @@
    +    $access_result = NULL;
    ...
    +      if ($access_result === NULL) {
    

    Would it be simpler to initialize this as a neutral and then use ->combineAccess everytime? or would that not work?

  2. +++ b/core/lib/Drupal/Core/Access/RefinableDependentAccessTrait.php
    @@ -0,0 +1,48 @@
    +    elseif (!$this->accessDependency instanceof AccessibleGroupInterface) {
    

    we returned above so we don't need elseif here

My only remaining question is - do we absolutely want to put those access interfaces in the core namespace. It means we only have one go to get them right. Layout builder is still beta but if we put those classes in core, we are fairly locked in to what we do with them. Should we consider marking them internal? Should the event be internal for the time being? Thoughts?

tedbow’s picture

Status: Needs work » Needs review
FileSize
5.68 KB
75.52 KB
  1. After writing the comment @Wim Leers asked for #83.11 about
    if (!$dependency instanceof EntityInterface || empty($dependency->in_preview))
    I started to wonder if there is anyway we could handle this in layout_builder directly since could cause problems for other uses of non-reusable blocks.

    I figured that since in Layout Builder we are setting the dependency and the layout builder knows when it is using preview for the defaults it can just set an accessible dependency that will pass in the circumstance.
    Anyways I will update #2957425: Allow the inline creation of non-reusable Custom Blocks in the layout builder with the changes

    For this issue we can just remove that logic.

  2. @larowlan thanks for the review! just saw as I was about to update the patch addressing....
  3. #86.1 For the truth tables of andIf and orIf I believe it won't matter to start with a neutral access result but what if someone extend it for a group that only passed if there were only allowed. Not likely but I would really not combine any result that weren't explicitly added as a dependency if we can avoid it.

    I did look at the logic again and I simplified it.

    First I made it explicit that if there were no dependencies added then there is access forbidden result. I added test coverage for this.

    Second, instead of starting with a neutral I simply shifted off the first dependency, after determining there were dependencies, and started with the access result of that dependency. Then loop through the remaining dependencies. If there is only 1 dependency this still works fine.

  4. #86.2 Fixed that back to what it was
  5. Re @internal. I am ok with marking these as internal. I have marked them all internal in this patch.

    Layout builder is still beta but if we put those classes in core, we are fairly locked in to what we do with them.

    We can't add the interfaces to only layout_builder because @tim.plunkett was only ok with the DependentAccessInterface if you were able to have addAccessDependency() to merge the access results in.

    I think @Wim Leers was only also behind this. and also having single dependency returned from \Drupal\Core\Access\DependentAccessInterface::getAccessDependency.

    he gave a big ➕1️⃣ to

    It takes the need to understand how combine multiple instances of AccessibleInterface out of access handlers

    So that combination really means that since block_content needs all of this including the groups so it can't live in layout_builder.

Wim Leers’s picture

Status: Needs review » Needs work

#85

  • 2. 👍
  • 5. Why do you need to be able to replace all the dependencies?
  • 11. 👍 EXCELLENT comment! Thank you!

#86 It means we only have one go to get them right Agreed with this concern.

#87:

  1. 👍 Nice, even better!
  2. See patch review below.
  3. 👍 LGTM
  4. Heh. I found it confusing to read, but I defer to the core committer's opinion of course :)
  5. See patch review below.

Patch review

  1. +++ b/core/lib/Drupal/Core/Access/AccessibleGroupBase.php
    @@ -28,15 +28,12 @@ public function addDependency(AccessibleInterface $dependency) {
    +      return AccessResult::forbidden();
    
    +++ b/core/tests/Drupal/Tests/Core/Access/AccessGroupTest.php
    @@ -34,6 +34,10 @@ public function testGroups() {
    +    // Ensure that groups with no dependencies return a forbidden access result.
    +    $this->assertTrue((new AccessGroupOr())->access('view', $this->account, TRUE)->isForbidden());
    +    $this->assertTrue((new AccessGroupAnd())->access('view', $this->account, TRUE)->isForbidden());
    

    Should be AccessResult::neutral().

  2. +++ b/core/lib/Drupal/Core/Access/AccessibleGroupInterface.php
    @@ -4,6 +4,8 @@
    + * @internal
    
    +++ b/core/lib/Drupal/Core/Access/DependentAccessInterface.php
    @@ -19,6 +19,8 @@
    + * @internal
    
    +++ b/core/lib/Drupal/Core/Access/RefinableDependentAccessInterface.php
    @@ -4,6 +4,8 @@
    + * @internal
    

    This only partially addresses @larowlan's concern. The fact that it already lives in core/lib is a problem though; it should live in the Layout Builder or Block Content module. Otherwise many developers will start using it anyway.

tedbow’s picture

Status: Needs work » Needs review
FileSize
99.32 KB
76.58 KB
+++ b/core/lib/Drupal/Core/Access/AccessibleGroupBase.php
@@ -0,0 +1,61 @@
+    $access_result = array_shift($this->dependencies)->access($operation, $account, TRUE);
+    foreach ($this->dependencies as $dependency) {
+      $access_result = $this->combineAccess($access_result, $dependency->access($operation, $account, TRUE));
+    }

We can't actually shift off a dependency from the array here or the dependency will not be here if you call access again.

re #88 Patch review.
1. Fixed
2. Moved all the access classes and tests to Block Content module.
We can't move them to Layout Builder because layout builder needs the Block Content entities to implement RefinableDependentAccessInterface.

Wim Leers’s picture

  1. +++ b/core/modules/block_content/src/BlockContentEvents.php
    @@ -0,0 +1,29 @@
    +final class BlockContentEvents {
    ...
    +  const BLOCK_CONTENT_GET_DEPENDENCY = 'block_content.get_dependency';
    
    +++ b/core/modules/block_content/src/Event/BlockContentGetDependencyEvent.php
    @@ -0,0 +1,68 @@
    +class BlockContentGetDependencyEvent extends Event {
    

    Shouldn't this then also become @internal?

    Also: CR should be updated to not mention these @internal things.

  2. +++ b/core/modules/block_content/src/BlockContentInterface.php
    @@ -48,6 +49,28 @@ public function setInfo($info);
    +  public function isReusable();
    +
    +  /**
    +   * Sets the block to be reusable.
    +   *
    +   * @return $this
    +   */
    +  public function setReusable();
    +
    +  /**
    +   * Sets the block to be non-reusable.
    +   *
    +   * @return $this
    +   */
    +  public function setNonReusable();
    
    +++ b/core/modules/block_content/src/Entity/BlockContent.php
    @@ -200,6 +212,14 @@ public static function baseFieldDefinitions(EntityTypeInterface $entity_type) {
    +    $fields['reusable'] = BaseFieldDefinition::create('boolean')
    

    These are the key changes in this issue, and it has a CR 👍

tedbow’s picture

1. marking classes as internal
2. Will update change record next.

Also did
was also going to remove:
`AccessibleGroupInterface` `AccessibleGroupBase` and `AccessGroupOr`

and just have `AccessGroupAnd extent Accessible`

Then the only code that would be left that would reference `AccessibleGroupInterface` and I could hard code that to `AccessGroupAnd`

tedbow’s picture

Updated change record and issue summary

Wim Leers’s picture

Status: Needs review » Reviewed & tested by the community
  1. Now 100% of this patch is scoped to core/modules/block_content.
  2. All "reusability"-related changes are the actual API/data model changes (well, additions). They're communicated in the CR
  3. All access-related things are @internal.

Thanks for helping to descope based on input from @larowlan and me!

tim.plunkett’s picture

effulgentsia’s picture

This looks great to me. Adding credit to all the reviewers. Thank you!

  • effulgentsia committed 98430f1 on 8.7.x
    Issue #2976334 by tedbow, Wim Leers, johndevman, tim.plunkett,...
effulgentsia’s picture

Title: Allow Custom blocks to be set as non-reusable adding access restriction based on where it was used. » [Cherry-pick to 8.6?] Allow Custom blocks to be set as non-reusable adding access restriction based on where it was used.
Issue tags: +Needs release manager review

I pushed this to 8.7.x. Since alpha is this week and this patch is not trivial and might have side effects to stable core code that we haven't foreseen, leaving at RTBC for 8.6.x for a release manager to evaluate if and when to cherry pick to 8.6.

amateescu’s picture

Kind of late to the party, but I see that this patch has quite some code dealing with entity reference and an upgrade path, so here's a review:

  1. +++ b/core/modules/block_content/block_content.module
    @@ -105,3 +108,73 @@ function block_content_add_body_field($block_type_id, $label = 'Body') {
    +function block_content_query_entity_reference_alter(AlterableInterface $query) {
    

    I don't understand why this was implemented like this, wouldn't it be easier to write a custom entity reference selection plugin for the block_content entity type and add a default condition in buildEntityQuery()?

    Is it because of the nested logic? If so, selection handlers also have a entityQueryAlter() which gives you access to the underlying db query.

  2. +++ b/core/modules/block_content/block_content.post_update.php
    @@ -0,0 +1,46 @@
    +  $data_table = \Drupal::entityTypeManager()
    +    ->getDefinition('block_content')
    +    ->getDataTable();
    

    Some sites might have disabled translatability of custom blocks, so we need to do the "is translatable ? get data table : get base table" dance, like everywhere else in Views :)

    Also, we should get the table names from the storage (after making sure that the entity type is using core's SqlContentEntityStorage), because the table names specified in the entity type definition are about to be deprecated.

  3. +++ b/core/modules/block_content/block_content.post_update.php
    @@ -0,0 +1,46 @@
    +        $display['display_options']['filters']['reusable'] = [
    +          'id' => 'reusable',
    +          'plugin_id' => 'boolean',
    +          'table' => $data_table,
    +          'field' => 'reusable',
    +          'value' => '1',
    +          'entity_type' => 'block_content',
    +          'entity_field' => 'reusable',
    +        ];
    

    I thought we were supposed to include the full plugin definition in update functions, otherwise any change in defaults for that plugin would result in a config difference.

  4. +++ b/core/modules/block_content/src/Access/AccessGroupAnd.php
    @@ -0,0 +1,50 @@
    +class AccessGroupAnd implements AccessibleInterface{
    +
    +
    +  /**
    

    Extra empty line here and a missing space before { :)

  5. +++ b/core/modules/block_content/src/BlockContentAccessControlHandler.php
    @@ -2,27 +2,85 @@
    +    $access->addCacheableDependency($entity);
    

    Why do we need to add the entity as a cacheable dependency unconditionally here?

    Shouldn't we only add in the if below?

  6. +++ b/core/modules/block_content/src/Entity/BlockContent.php
    @@ -200,6 +212,14 @@ public static function baseFieldDefinitions(EntityTypeInterface $entity_type) {
    +      ->setInitialValue(TRUE);
    

    We shouldn't include the initial value in the base field definition, it has no effect here because it's only supposed to be used in the update function which adds the field.

  7. +++ b/core/modules/block_content/src/Entity/BlockContent.php
    @@ -282,6 +302,27 @@ public function setRevisionLogMessage($revision_log_message) {
    +    return $this->get('reusable')->first()->get('value')->getCastedValue();
    

    We usually call (bool) ... instead of ->getCastedValue(). In fact, this is the first time I see a call to getCastedValue() in a content entity class.

  8. +++ b/core/modules/block_content/tests/src/Functional/Update/BlockContentReusableUpdateTest.php
    @@ -0,0 +1,155 @@
    + * @group Update
    + * @group legacy
    + */
    +class BlockContentReusableUpdateTest extends UpdatePathTestBase {
    

    This should also be in the block_content group.

  9. +++ b/core/modules/block_content/tests/src/Functional/Update/BlockContentReusableUpdateTest.php
    @@ -0,0 +1,155 @@
    +   * @see block_content_update_8600
    +   * @see block_content_post_update_add_views_reusable_filter
    

    Needs () at the end of the method names.

  10. +++ b/core/modules/block_content/tests/src/Functional/Update/BlockContentReusableUpdateTest.php
    @@ -0,0 +1,155 @@
    +    $this->container->get('module_installer')->install(['block_content_view_override']);
    

    I'm not sure it's safe to install a module before running the updates, we usually add/alter views and other config objects in a special "db preparation" file, like core/modules/field/tests/fixtures/update/drupal-8.views_entity_reference_plugins-2429191.php for example.

  11. +++ b/core/modules/block_content/tests/src/Kernel/BlockContentEntityReferenceSelectionTest.php
    @@ -0,0 +1,189 @@
    +class BlockContentEntityReferenceSelectionTest extends KernelTestBase {
    

    We have EntityReferenceSelectionAccessTest for testing all of our custom selection handlers, can we move this one over there in a testBlockContent() method? Otherwise it would be a lot harder to find...

  12. +++ b/core/modules/block_content/tests/src/Unit/Access/DependentAccessTest.php
    @@ -0,0 +1,160 @@
    +    $this->assertEquals('I have no opinion', $accessResultNeutral->getReason());
    +
    +  }
    

    Extra empty line :)

amateescu’s picture

+++ b/core/modules/block_content/block_content.install
@@ -138,3 +138,19 @@ function block_content_update_8400() {
+function block_content_update_8600() {

Also, if this patch doesn't end up in 8.6.x after all, we need to rename this update function to block_content_updae_8700() otherwise we'll end up with the same confusing situation as we had with block_content_update_8400(), which only exists in 8.5.x :/

tedbow’s picture

@amateescu thanks for taking a look.
I have just addressed the points that I have answer for right now.
1. The reason I wrote the alter this way instead of writing a new entity reference selection plugin for block_content entities is that there could be custom entity reference selection plugins already in existence that have been written to handle block_content entities.

Because we are now using block_content entities in a much more access restricted way we to whatever level possible do not want these existing entity reference selection plugins to pick up any of the new non-reusable blocks. I don't think we can rely on these plugins being update to filter out non-reusable blocks.

You know this stuff better than me but since there no access checking on the autocomplete for entity reference selection (except nodes?) any existing selection plugins would let you see the labels of non-reusable block. They won't actually render because of the block content access handler but you could figure out labels using the autocomplete suggestions.

But also the plugins would be able to select not reusable block but then on edit the access checking would happen and you would get an error on your previous selection.

So block_content_query_entity_reference_alter works for the default selection plugin and for any custom ones we can't know about.

5. Because we inspecting the entity in the if we need to add the cacheablity because the fact that we are testing isReusable() at all means the access should be cached on the entity.

7. Yes I got this suggestion from @Wim Leers to change from (bool) .... Although this is long winded it also seems like this is decent usage of getCastedValue(). Is there a reason to use (bool), a cast, when we have an explicit method to get the casted value of our typed data?

Version: 8.6.x-dev » 8.7.x-dev

Drupal 8.6.0-alpha1 will be released the week of July 16, 2018, which means new developments and disruptive changes should now be targeted against the 8.7.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

tedbow’s picture

Status: Reviewed & tested by the community » Needs review
FileSize
9.62 KB

Addressing the rest of the review in #98

2. fixed
3. added full definition
4. fixed
5. fixed
8 added @group
9. fixed
10. Thanks for the example, updated the test. Removed the test module
11. It seems weird to move it because it is covers code in the block_content. Also that is BrowserTestBase and this is kernel test.
12. fixed.

Settings to Needs Review to run the tests.

tim.plunkett’s picture

  1. +++ b/core/modules/block_content/block_content.post_update.php
    @@ -6,18 +6,28 @@
    +  if ($storage instanceof SqlContentEntityStorage) {
    

    Is SqlEntityStorageInterface not good enough here? What's magic about that specific implementation?

  2. +++ b/core/modules/block_content/block_content.post_update.php
    @@ -6,18 +6,28 @@
    +    // The storage is not an instance SqlContentEntityStorage we cannot
    +    // correctly determine the table from the storage.
    

    Missing some words here? "If the storage is not an instance of..."

    Also might be worth moving above the if

  3. +++ b/core/modules/block_content/block_content.post_update.php
    @@ -6,18 +6,28 @@
    -    if ($view->get('base_table') != $data_table) {
    +    if ($view->get('base_table') != $table) {
    

    Might as well switch to !==

Other changes look good!

tedbow’s picture

1. Apparently SqlContentEntityStorage is magic! getDataTable() and getBaseTable() are not defined in the interface. The only interface these methods are defined is \Drupal\Core\Entity\EntityTypeInterface.

This issue #2232465: Deprecate table names from entity definitions deals with the methods but doesn't move them to an interface as far as I can tell.

2. Moved the comment up. Also checked after the if/else if $table is empty in which case we can't update the views. Not likely but possible.

3. fixed

dsnopek’s picture

This issue #2232465: Deprecate table names from entity definitions deals with the methods but doesn't move them to an interface as far as I can tell.

See #2960037: Add a way to get the primary table of an entity type from TableMappingInterface

That's about replacing those methods with a method on TableMappingInterface.

There's many SqlContentEntityStorage-related warts - this meta issue is about fixing them: #2960147: Finalize the entity storage

amateescu’s picture

@tedbow, re #100:

since there no access checking on the autocomplete for entity reference selection (except nodes?)

On the contrary, the main purpose of entity reference selection plugins is query-level access checking for all entity reference queries, at least until we get #2909970: Implement a query-level entity access API in core. See the buildEntityQuery() method of various selection handlers, for example UserSelection, FileSelection or CommentSelection.

So I really think that this query access logic should live in a BlockContentSelection plugin, because that should the canonical "source of truth" for any custom block_content selection plugin.

You do bring up a good point about possible pre-existing custom selection plugin implementations, but I would prefer if we can change block_content_query_entity_reference_alter to only cater for those cases, i.e. by checking if the selection plugin class is different than (and doesn't extend) Drupal\block_content\Plugin\EntityReferenceSelection\BlockContentSelection which should be introduced by this patch.

But also the plugins would be able to select not reusable block but then on edit the access checking would happen and you would get an error on your previous selection.

The access checking on edit for pre-existing references should not be a problem since #2791269: Allow saving pre-existing references to inaccessible items.

Because we inspecting the entity in the if we need to add the cacheablity because the fact that we are testing isReusable() at all means the access should be cached on the entity.

Right, so this means we can remove the addCacheableDependency($entity) call from the first if, since we're adding it unconditionally below.

Also, It would be a bit more clear if we move the line with $access->addCacheableDependency($entity); below the if ($entity->isReusable() === FALSE) { block, since that implies a thinking like "we determined access based on a property of of the entity, so we need to add it as a cacheable dependency". I think that's the common practice.. at least in core.

Is there a reason to use (bool), a cast, when we have an explicit method to get the casted value of our typed data?

A (bool) cast will make that code always return a boolean value, even in cases when the underlying field might change for whatever reason, and getCastedValue() would not return a boolean anymore.

11. It seems weird to move it because it is covers code in the block_content. Also that is BrowserTestBase and this is kernel test.

That test class handles all the existing access-related code from selection handlers (node, user, comment) and taxonomy will soon be added there as well in #2981887: Add a publishing status to taxonomy terms. Having it all in one place makes it easier to maintain, for me at least. As for it being a browser test, I just opened #2987084: Convert EntityReferenceSelectionAccessTest to a kernel test to fix that.

--

About #104.1: This whole area is very confusing, I know. It's because the table names from the entity type definition are about to be deprecated, but we don't yet have an "official" way of getting them, at least not until #2960037: Add a way to get the primary table of an entity type from TableMappingInterface lands.

tedbow’s picture

@amateescu thanks for the detailed response

  • Also, It would be a bit more clear if we move the line with $access->addCacheableDependency($entity); below the if ($entity->isReusable() === FALSE) {

    This will not work because in the if the dependency is not set we

    if (empty($dependency)) {
       return AccessResult::forbidden("Non-reusable blocks must set an access dependency for access control.");
    }

    So in that case the dependency on the entity would not have been added if we are after the if.
    I have added comment as to why we are adding the dependency.

  • 4. Changing back to (bool) $this->get('reusable')->value; which it was in #78

So I really think that this query access logic should live in a BlockContentSelection plugin,

Creating a follow up issue #2987159: Create an entity reference selection plugin for custom blocks that filters out non-reusable blocks because block_content_query_entity_reference_alter() does prevent the selection of non-reusable blocks but a plugin would be better, though we still won't be able to remove block_content_query_entity_reference_alter()

effulgentsia’s picture

Title: [Cherry-pick to 8.6?] Allow Custom blocks to be set as non-reusable adding access restriction based on where it was used. » Allow Custom blocks to be set as non-reusable adding access restriction based on where it was used.
Issue tags: -Needs release manager review

I discussed this with @catch and he and I are both ok with this going into 8.6 before beta, so removing the "Needs release manager review" tag. That's still assuming though that #107 or something like it lands by the end of this week. If it does, I'll cherry pick both that commit and the #96 commit to 8.6. If at all possible, I'd love to do that by tomorrow or Thursday instead of Friday, just to give a bit of extra time for anyone who's testing the 8.6 branch tip to report back on if they run into any unanticipated problems with this code prior to beta getting tagged.

tim.plunkett’s picture

Status: Needs review » Reviewed & tested by the community

I think between #2987159: Create an entity reference selection plugin for custom blocks that filters out non-reusable blocks and this patch here, @tedbow has sufficiently addressed @amateescu's feedback.
This change will make it that much better for backporting to 8.6

amateescu’s picture

FWIW, I'm very happy with the changes from #107 and the followup issue to use a proper selection plugin.

effulgentsia’s picture

Adding issue credit to @amateescu. Thanks!

  • effulgentsia committed 9d6dcef on 8.7.x
    Issue #2976334 by tedbow, Wim Leers, johndevman, tim.plunkett, amateescu...

  • effulgentsia committed 51c8979 on 8.6.x
    Issue #2976334 by tedbow, Wim Leers, johndevman, tim.plunkett,...
  • effulgentsia committed 91001d1 on 8.6.x
    Issue #2976334 by tedbow, Wim Leers, johndevman, tim.plunkett, amateescu...
effulgentsia’s picture

Version: 8.7.x-dev » 8.6.x-dev
Status: Reviewed & tested by the community » Fixed

Pushed #107 to 8.7.x and cherry picked #96 and #112 to 8.6. Thanks for the great work and reviews on this!

effulgentsia’s picture

Issue tags: +8.6.0 release notes

Also published the CR and tagging for the release notes.

penyaskito’s picture

This made one of my upgrade tests fail:

1) Drupal\Tests\lingotek\Functional\Update\LingotekTargetStatusFormatterUpdate8209Test::testUpgrade
Schema key views.view.block_content:display.default.display_options.filters.reusable.group failed with: variable type is string but applied schema class is Drupal\Core\TypedData\Plugin\DataType\IntegerData

Found out the culprit here. The attached patch fixed it. Maybe we are missing some coverage in the upgrade tests?

effulgentsia’s picture

Status: Fixed » Needs review

Thank you for discovering that and reporting it! Setting to Needs Review for evaluation. If the bug and fix are correct, then yes we need a test for it too.

tedbow’s picture

Created a follow up #2988435: block_content_post_update_add_views_reusable_filter uses an string instead of int for "group"

It is unclear why LingotekTargetStatusFormatterUpdate8209Test failed and ours didn't

effulgentsia’s picture

Status: Needs review » Fixed

I raised that followup to Critical. Setting this back to Fixed.

Status: Fixed » Closed (fixed)

Automatically closed - issue fixed for 2 weeks with no activity.

Gábor Hojtsy’s picture

Issue tags: +8.6.0 highlights
xjm’s picture

It makes sense to mention this as a highlighted feature, but is there some disruption or important update information that requires a release notes mention?

xjm’s picture

Issue tags: -8.6.0 release notes

Reading the CR, doesn't look like there's anything to mention in the 8.6.0 release notes for this fix. Thanks!

benjifisher’s picture

Looking at the last patch committed for this issue, I was confused until I realized that an earlier patch was also committed. I am hiding all the interdiffs and other patches to make it easier for the next person who decides to look at how this issue was implemented.