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:

UX
Currently to add a custom block to a Layout the user would have to

  1. Navigate to the custom block page
  2. Create the block
  3. Navigate back to the layout builder
  4. Click Add block
  5. Find the block they just created.

Access Control
If we allow inline block creation a user who creates a custom block on the Layout Builder for an access controlled entity probably expects only those that can view the entity that has the Layout to be able to view the custom block.

Currently blocks have no connection on an access level to where they are placed so this block would be able to placed via the block admin page, if you listed the block in a View or it was referenced in an Entity Reference value anyone would be able to see it.

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. These blocks would be shown every place you are able to pick a block including the layout builder and the current block place listing.

Revision
Currently if you placed a block via the Layout Builder there is no connection between the revision of the block and revision of the entity with the Layout. If you updated a custom block all previous revisions of the entity with the layout would display the update version.

Deletion
If a custom block is deleted then it would be removed from all placements via the Layout Builder across all revisions.

Proposed resolution

Add the ability for users to create custom non-reusable blocks from within the Layout Builder. These blocks will be the same entity type as Custom Block provide by the block_content module therefore all the site's current block types will be available from within the Layout Builder.

The non-reusable blocks will only be viewable within the context of the layout that they were placed in. For layout entities where the bundle is set to create a new revision for each update the changes to custom inline blocks will be tracked with the parent entities revision.

The new custom inline blocks should be "owned" by the layout that they are placed in in that they should *not* be editable or deleted from outside of the layout builder.

Implementation

In #2976334: Allow Custom blocks to be set as non-reusable adding access restriction based on where it was used. but included here in combined patch, review the do-not-test.patch for this issue.
See that issue Custom Block related implementation.

For integration with Layout Builder, we should create a new Block Plugin that stores Custom Block revision IDs and implement an inline form for creating and editing these Custom Blocks. All inline blocks created with this plugin will be non-reusable.

A simple database backed usage tracking service will be created to track usage of the inline custom blocks. These inline custom blocks will only ever 1 parent so it was determined that it does not need generic usage tracking
@see #2976366: Create generic entity usage tracking service

Remaining tasks

Write a patch to explore the feasibility of this approach.
Write tests
Review

User interface changes

Users would be able to create Custom Blocks inline from the Layout Builder. These blocks would not be available in the Custom Block library.

API changes

API changes needed for this change are being made in #2976334: Allow Custom blocks to be set as non-reusable adding access restriction based on where it was used.

Data model changes

Data model changes needed for this change are being made in #2976334: Allow Custom blocks to be set as non-reusable adding access restriction based on where it was used.

CommentFileSizeAuthor
#250 2957425-250.patch92.42 KBtedbow
#250 interdiff-239-250.txt572 bytestedbow
#249 interdiff_339_248.txt72.09 KBmpotter
#248 2957425-248.patch17.8 KBmpotter
#242 2957425-241-test-only-FAIL.patch93.34 KBtedbow
#239 interdiff.txt8.92 KBtedbow
#239 2957425-239.patch92.42 KBtedbow
#219 2957425-219.patch91.56 KBtedbow
#219 interdiff-217-218.txt638 bytestedbow
#217 2957425-217.patch91.54 KBtedbow
#217 interdiff-211-217.txt20.07 KBtedbow
#211 2957425-210.patch92 KBtedbow
#211 interdiff-206-210.txt1.61 KBtedbow
#206 2957425-206.patch91.91 KBtedbow
#206 interdiff-204-206.txt1.11 KBtedbow
#204 2957425-204.patch91.94 KBtedbow
#204 interdiff-202-204.txt18.98 KBtedbow
#202 2957425-201.patch94.96 KBtedbow
#202 interdiff-200-201.txt8.91 KBtedbow
#200 2957425-200.patch94.65 KBtedbow
#199 2957425-199.patch94.58 KBtedbow
#199 interdiff-197-199.txt1.82 KBtedbow
#197 interdiff-196-197.txt17.49 KBtedbow
#197 2957425-197.patch94.59 KBtedbow
#196 2957425-196.patch95.4 KBtedbow
#195 2957425-105.patch95.31 KBtedbow
#195 interdiff-191-195.txt3.29 KBtedbow
#191 2957425-191-plus_2976334_91.patch167.51 KBtedbow
#191 2957425-191-layout_only-do-not-test.patch95.13 KBtedbow
#191 interdiff-189-191.txt15.71 KBtedbow
#189 2957425-189-plus_2976334_91.patch160.95 KBtedbow
#189 2957425-189-layout_only-do-not-test.patch88.56 KBtedbow
#189 interdiff-186-189.txt12.16 KBtedbow
#186 interdiff-183-186.txt4.71 KBtedbow
#186 2957425-186-layout_builder_only-do-not-test.patch84.93 KBtedbow
#186 2957425-186_plus_2976334-87.patch160.45 KBtedbow
#183 2957425-182-layout_builder_only-do-not-test.patch83.37 KBtedbow
#183 interdiff-180-182.txt12.97 KBtedbow
#183 2957425-182_plus_2976334-70.patch142.39 KBtedbow
#180 2957425-180_plus_2976334-68.patch142.53 KBtedbow
#180 2957425-180-layout_builder_only-do-not-test.patch84.21 KBtedbow
#180 interdiff-177-180.txt6.31 KBtedbow
#177 2957425-177_plus_2976334-64.patch140.93 KBtedbow
#177 2957425-177-layout_build_only-do-not-test.patch84.17 KBtedbow
#177 interdiff-176-177.txt3.83 KBtedbow
#176 2957425-176_plus_2976334_62.patch132.53 KBtedbow
#176 2957425-176_layout_only-do-not-test.patch81.5 KBtedbow
#176 interdiff-174-176.txt8.36 KBtedbow
#174 2957425-174__plus_2976334_62.patch131.58 KBtedbow
#174 2957425-174__layout_builder_only-do-not-test.patch80.55 KBtedbow
#174 interdiff--172-174.txt4.21 KBtedbow
#172 2957425-172_plus_2976334_62.patch128.7 KBtedbow
#172 2957425-172-layout_builder_only-do-not-test.patch77.67 KBtedbow
#172 interdiff-169-172.txt11.11 KBtedbow
#169 2957425-169-plus_2976334_62.patch128.35 KBtedbow
#169 interdiff-167-168.txt2.5 KBtedbow
#169 2957425-169-layout_builder_only-do-not-test.patch77.32 KBtedbow
#167 2957425-167-plus_2976334.patch127.85 KBtedbow
#167 2957425-167-layout_builder_only-do-not-test.patch76.81 KBtedbow
#167 interdiff-165-167.txt34.94 KBtedbow
#165 interdiff-163-165.txt29.72 KBtedbow
#165 2957425-165_plus-2976334-60.patch117.28 KBtedbow
#165 2957425-165-layout-builder_only--do-not-test.patch70.39 KBtedbow
#163 2957425-163_plus_2976334-59.patch104.33 KBtedbow
#163 2957425-163-layout_builder_only-do-not-test.patch57.53 KBtedbow
#162 2976334-57__plus_2957425-149-to-see-if-still-passes.patch104.54 KBtedbow
#160 2957425-160_plus_2976334-54.patch110.97 KBtedbow
#160 2957425-160-review--do-not-test.patch50.7 KBtedbow
#160 interdiff-158-160.txt8 KBtedbow
#158 interdiff-layout_builder_only-155-158.txt815 bytestedbow
#158 2957425-158--review-do-not-test.patch47.31 KBtedbow
#158 2957425-158_plus_2976334_53.patch105.88 KBtedbow
#155 2957425-155-review--do-not-test.patch47.35 KBtedbow
#155 2957425-155_plus_2957425-47.patch108.9 KBtedbow
#155 interdiff-154-155.txt10.71 KBtedbow
#154 2957425-154_plus_2976334_44.patch101.39 KBtedbow
#154 2957425-154-review--do-not-test.patch54.57 KBtedbow
#154 interdiff-151-154.txt3.12 KBtedbow
#151 2957425-151-review--do-not-test.patch54.57 KBtedbow
#151 2957425-151_plus_2976334_41.patch101.5 KBtedbow
#151 interdiff-149-151.patch22.36 KBtedbow
#149 2957425-149_plus_2976334-33.patch103.39 KBtedbow
#149 2957425-149-review-do-not-test.patch57.74 KBtedbow
#149 interdiff-148-149.txt4.95 KBtedbow
#148 2957425-148_plus_2976334-33.patch102.53 KBtedbow
#148 2957425-148-review-do-not-test.patch56.87 KBtedbow
#148 interdiff-layout_builder-145-148.txt13.92 KBtedbow
#145 2957425-145.patch78 KBtedbow
#145 2957425-145-do-not-test.patch56.25 KBtedbow
#145 interdiff-144-145.txt57.81 KBtedbow
#144 2957425-144.patch81.84 KBtedbow
#144 2957425-144-do-not-test.patch60.16 KBtedbow
#144 interdiff-142-145.txt2.26 KBtedbow
#142 2957425-142-do-not-test.patch60.36 KBtedbow
#142 interdiff-140-142.txt4.7 KBtedbow
#142 2957425-142_plus_2976334-14.patch82.04 KBtedbow
#140 2957425-140_plus_2976334-14.patch80.92 KBtedbow
#140 2957425-140-do-not-test.patch59.24 KBtedbow
#140 interdiff-138-140.txt25.65 KBtedbow
#138 2957425-138-do-not-test.patch57.48 KBtedbow
#138 interdiff-136-138.txt1.04 KBtedbow
#138 2957425-138_plus_2976334-14.patch79.16 KBtedbow
#136 2957425-136_plus_2976334-10.patch79.07 KBtedbow
#134 2957425-134_plus_2976334-10.patch70.1 KBtedbow
#134 2957425-134-do-not-test.patch57.38 KBtedbow
#134 interdiff-2957425-131-134.txt20.71 KBtedbow
#131 2957425-130-combined.patch94.51 KBtedbow
#131 2957425-130-do-not-test.patch52.62 KBtedbow
#129 interdiff-127-129.txt22.47 KBtedbow
#129 2957425-129.patch94.48 KBtedbow
#127 2957425-127.patch97.24 KBtedbow
#127 interdiff-125-127.txt1.86 KBtedbow
#125 2957425-125.patch97.13 KBtedbow
#125 interdiff-122-125.txt29.01 KBtedbow
#122 2957425-122.patch99.17 KBtedbow
#122 interdiff-117-122.txt6.53 KBtedbow
#120 2957425-120.patch99.14 KBtedbow
#120 interdiff-117-120.txt6.5 KBtedbow
#117 interdiff-114-117.txt12.37 KBtedbow
#117 2957425-117.patch92.64 KBtedbow
#114 2957425-114.patch87.24 KBtedbow
#114 interdiff-113-114.txt632 bytestedbow
#113 2957425-113.patch87.23 KBtedbow
#113 interdiff-101-113.txt39.62 KBtedbow
#101 interdiff-98-100.txt9.38 KBtedbow
#101 2957425-100.patch85.11 KBtedbow
#98 2957425-98.patch78.01 KBtedbow
#98 interdiff-95-98.txt27.59 KBtedbow
#95 interdiff_91-95.txt3.03 KBjohnwebdev
#95 2957425-95.patch72.41 KBjohnwebdev
#91 interdiff_87-91.txt5.54 KBjohnwebdev
#91 2957425-91.patch69.21 KBjohnwebdev
#87 2957425-87.patch71.2 KBtedbow
#87 interdiff-83-87.txt38.94 KBtedbow
#83 2957425-83.patch50.79 KBtedbow
#83 interdiff-81-83.txt1.91 KBtedbow
#81 2957425-81.patch51.98 KBtedbow
#81 interdiff-77-81.txt15.12 KBtedbow
#77 2957425-77.patch59.71 KBtedbow
#77 interdiff-72-77.txt6.54 KBtedbow
#72 2957425-72.patch61.17 KBtedbow
#72 interdiff-62-72.txt12.45 KBtedbow
#66 2957425-66.patch59.73 KBjohnwebdev
#66 interdiff_62-66.txt6.74 KBjohnwebdev
#62 2957425-62.patch53.58 KBtedbow
#62 interdiff-57-62.txt12.22 KBtedbow
#57 2957425-57.patch47.11 KBtedbow
#57 interdiff-55-57.txt10.72 KBtedbow
#55 2957425-55.patch47.22 KBtedbow
#55 interdiff-53-55.txt9.94 KBtedbow
#53 2957425-53.patch46.88 KBtedbow
#53 interdiff-48-53.txt6.22 KBtedbow
#48 2957425-48.patch42.17 KBjohnwebdev
#47 interdiff_45-47.txt1.32 KBjohnwebdev
#47 2957425-47.patch42.19 KBjohnwebdev
#46 interdiff_44-45.txt6.28 KBjohnwebdev
#46 2957425-45.patch42.31 KBjohnwebdev
#44 2957425-44.patch37.24 KBtedbow
#44 interdiff-43-44.txt6.21 KBtedbow
#41 interdiff_37-41.txt3.43 KBjohnwebdev
#41 2957425-41.patch32.63 KBjohnwebdev
#37 2957425-37.patch29.6 KBtedbow
#37 interdiff-34-37.patch3.46 KBtedbow
#34 interdiff_32-33.txt3 KBjohnwebdev
#34 2957425-33.patch29.34 KBjohnwebdev
#32 2957425-32.patch27.4 KBtedbow
#32 interdiff-30-32.txt2.69 KBtedbow
#30 2957425-30.patch25.54 KBtedbow
#30 interdiff-12.patch17.36 KBtedbow
#12 interdiff_7-12.txt1.01 KBjohnwebdev
#12 2957425-12.patch20.5 KBjohnwebdev
#8 2957425-7.patch19.75 KBjohnwebdev
#5 2957425-5.patch19.75 KBjohnwebdev
#4 2957425-4.patch19.03 KBjohnwebdev
#3 2957425-3.patch5.1 KBjohnwebdev
Support from Acquia helps fund testing for Drupal Acquia logo

Comments

samuel.mortenson created an issue. See original summary.

larowlan’s picture

Plus one

johnwebdev’s picture

FileSize
5.1 KB

Since it was unassigned, I just started hacking something together, here's a starter patch with the reusability. Will create a block plugin and more tests later in the week.

johnwebdev’s picture

FileSize
19.03 KB

Neat. I could pretty much use the whole code provided from https://www.drupal.org/project/drupal/issues/2948064 and change the way its stored.

Heres an updated patch that allows you to add inline blocks. Still need to work with revisioning, update hook, etc.

johnwebdev’s picture

FileSize
19.75 KB
johnwebdev’s picture

Status: Active » Needs review
johnwebdev’s picture

Status: Needs review » Needs work
johnwebdev’s picture

FileSize
19.75 KB

Forgot the updated test.

johnwebdev’s picture

Status: Needs work » Needs review
johnwebdev’s picture

New patch.

johnwebdev’s picture

Status: Needs work » Needs review
Berdir’s picture

This is a possible solution for #2940755: block_content block derivatives do not scale to thousands of block_content entities.

I think we should just not have used derivates for this initially, same for menus and other things and just gone with an autocomplete widget + some kind of pre-fill feature through a place action on the block overview, but too late for that :)

In general I agree with the idea but I don't think we should make it layout builder specific but allow all content blocks to be placed like this instead. That means also thinking about the exact wording for this and how the UI would work for normal blocks. And maybe eventually even deprecate the old way?

I'm also not convinced to use the block revision ID for this. That would IMHO be extremely confusing for users as it would make updating very hard? But I also didn't look closely at the patch.

samuel.mortenson’s picture

@Berdir Using the revision ID is basically required for having Content Moderation and Layout Builder work nicely together - users expect Custom Blocks placed in Layout Builder to be tracked with the parent entity's revision history, much like how Paragraphs users need entity reference revisions when using Content Moderation.

Edit: The above doesn't mean the user experience makes sense in the current patch, just that we need this functionality for Layout Builder.

Berdir’s picture

Good point. So we're basically building poormanscron paragraphs functionality here ;)

hawkeye.twolf’s picture

Adding related issue #2948064: Layout Builder should support inline creation of Custom Blocks using entity serialization, as Custom Block (Non-)Reusability should be considered a replacement for the "inline blocks" approach mentioned there.

kevincrafts’s picture

Trying to understand what would happen with revisions in this scenario:

  1. User creates a custom block places it in the main region on Page A and in the Sidebar on Page B.
  2. User edits the layout on Page B.
  3. User makes a change to the custom block content.
  4. User reverts the layout on Page B to a previous version

Is the output of the custom block on Page A and Page B identical? Or because of where the revision happened, the pages are displaying two different versions of the block?

johnwebdev’s picture

@kevincrafts

Here's what I think:

Page A and Page B would have separately blocks in the first place because inline blocks are not intended to be "reusable" but only where it was created.

Instead, I think it should be something like:

1. User create a Page A (revision 1), and adds a Inline custom block "Hello foo!"
2. User updates Page A (revision 2) and also change the inline custom block to "Hello bar!"
3. User reverts Page A to revision 1 and inline custom block should now be back at "Hello foo!"

hawkeye.twolf’s picture

There might actually be an argument for having _both_ approaches (this issue's "re-usable" checkbox vs. inline-blocks of #2948064: Layout Builder should support inline creation of Custom Blocks using entity serialization). Most notably, this approach supports translation of individual blocks. If a site needed to maintain parity of layout between translations, editors could place the content as a "standard" block instead of inline-block and then accept the inherent limitations (e.g., no content moderation support until ERR makes it into core).

hawkeye.twolf’s picture

@kevincrafts, I believe the behavior outlined by @johndevman is the ideal approach but falls outside the scope of this ticket. To support this kind of functionality, we would need ERR (or something akin to it) in core. Solving this very hard problem is one of the main motivators for going with a "serialized" approach (a la #2948064: Layout Builder should support inline creation of Custom Blocks using entity serialization) rather than saving blocks to the database as normal entities.

Currently, all we have is an ER field, which targets the entity ID and not the revision ID, so this content would not follow the expected revision pattern. This sub-optimal behavior is how things currently work when placing blocks via Layout Builder but is more acceptable / makes more sense when a block can be used in more than one place (its content doesn't "belong" to that particular node).

hawkeye.twolf’s picture

Adding related core issue that's needed to help make ERR a viable solution.

kevincrafts’s picture

So how would one go about placing a reusable block content on multiple pages with Layout Builder that would not cause the block content to diverge on the editing of a page?

tim.plunkett’s picture

All blocks are currently reusable. So it would work the same as it does today.
I guess this issue is more about "non-re-usability"?

hawkeye.twolf’s picture

@kevincrafts, yes, it works like @tim.plunkett described. There would be two types of blocks:

  1. "Re-usable" ones (what we already have in core) that behave like regular Entity Reference fields, targeting block entity ID instead of revision ID.
  2. "Non-re-usable" ones that would use an ERR (Entity Reference Revisions) type of relationship and target the block entity revision, allowing it to follow a content moderation workflow, etc.

Make sense?

Berdir’s picture

The thing with re-using (as in, the entity type, not a single entity) block_content with a new field however is that at least unless we adapt the view, /admin/structure/block/block-content will still list all those blocks and users will try to edit them through that, which will then not actually do anything as the embedded blocks reference a specific revision ID.

Also translations sounds like an interesting problem as each translation will have different revision id), I guess it would work with a translatable layout_selection field, but as config for an entity view display? And publishing draft translation revisions results in new revisions.

I might be biased as a paragraphs.module co-maintainer, but I would recommend to be very careful about trying to cover this with some kind of quick fix like this in core. As mentioned here, ERR/paragraphs is very complex.

As an different but possibly interesting idea, imagine what you could do if you couldn't just put fields into layouts but control each field item separately? E.g. you could have a multi-value body field and then split two deltas up into a two-column layout, or have a large image with smaller images below. Or have N paragraphs in a single paragraph field and place them on a page in whatever way you want ;)

tim.plunkett’s picture

That's sorta how https://www.drupal.org/project/paragraph_blocks works, and douggreen suggested a per-delta feature back in Baltimore. Not sure that it has a core issue yet.

hawkeye.twolf’s picture

@tim.plunkett/@Berdir, if the content can be edited elsewhere (e.g., the node edit form), then referencing field deltas with Layout Builder presents some challenges. Targeting the actual entity revision ID (as we do here) gets around those challenges, but then we get back into the realm of needing ERR-like functionality in core (#2812595: Support revision tracking of entity references in core), and I echo @Berdir's concerns about attempting to re-solve the issues ERR has already surfaced and tackled w.r.t. moderation, translation, etc.

@Berdir, I'd love to get your thoughts on #2948064: Layout Builder should support inline creation of Custom Blocks using entity serialization as an alternate approach. Will you chime in there? Many thanks!

tedbow’s picture

I have been working on #2948064-49: Layout Builder should support inline creation of Custom Blocks using entity serialization but I ran into a bunch of problems that make me(and others) think that issue won't work. You can see the reasons in the comment I linked to here.

So now looking at this issue to solve the problem. It looks like this patch has made great progress so far!

  1. +++ b/core/modules/layout_builder/src/Plugin/Block/InlineBlockContentBlock.php
    @@ -0,0 +1,227 @@
    +    $block->save();
    +    $this->configuration['block_revision_id'] = $block->getRevisionId();
    

    If we save blocks in form submit in this way then any block that is added in the Layout Builder will exist whether the user actually saves the Layout or not.

    So if you add 5 new inline blocks but never actually save the layout then you have will have 5 new content blocks that aren't actually used anywhere.

    It would probably make sense to keep this in tempstore and only save the blocks when the layout is saved.

    This is also the case for editing existing blocks. If the blocks would be saved and new revision created, if applicable, and then if the user cancels the layout changes they likely do not want the changes to the blocks either.

  2. re #26

    The thing with re-using (as in, the entity type, not a single entity) block_content with a new field however is that at least unless we adapt the view, /admin/structure/block/block-content will still list all those blocks and users will try to edit them through that, which will then not actually do anything as the embedded blocks reference a specific revision ID.

    Can we ship an updated View? Not sure how that works.

    If not we documented that View should updated or reverted back to new core View. We may want to disable access to the block edit form for non-reusable blocks outside of the layout builder or at least at the existing edit route. Because presumably editing of reusable blocks should only ever happen in block plugin form. So we could prevent that.

tedbow’s picture

  1. Changed \Drupal\layout_builder\Plugin\Derivative\InlineBlockContentDeriver::getDerivativeDefinitions() to \Drupal\Core\Entity\EntityInterface::getConfigDependencyKey() and \Drupal\Core\Entity\EntityInterface::getConfigDependencyName() instead of using hardcoded string
  2. Renamed BlockContentDeriver to InlineBlockContentDeriver to match the plugin name
  3. Override ::buildConfigurationForm() to take away the default title when adding a new block. The block type label doesn't make sense for the block title shown on entity view, i.e. "Basic Block" and the label will not be used anywhere else such as admin listing.
  4. Updated InlineBlockContentBlockTest be most comprehensive based on changes from #2948064: Layout Builder should support inline creation of Custom Blocks using entity serialization. To make this test work I had to add $block->setNewRevision(TRUE); otherwise it wouldn't make a new revision. Without the new revision when you change the block in an override it actually changed the default also because it wasn't a new revision.
  5. A bunch of nit from @phenaproxima's review of #2948064: Layout Builder should support inline creation of Custom Blocks using entity serialization because a lot of the code was the same.
tedbow’s picture

Here is a failing test that proves #29.1

If you add an inline block to a layout but never end up saving the Layout because you either Cancel or Revert the content block still gets created and remains

@todo Create a similar test for editing existing inline blocks. When you edit a block but don't save the layout the content block changes remain.

Also in writing this test I found this #2970801: If you add block then try to Revert the layout it doesn't revert

johnwebdev’s picture

First attempt on #29.1.

johnwebdev’s picture

Status: Needs work » Needs review
tedbow’s picture

Status: Needs work » Needs review
FileSize
3.46 KB
29.6 KB

@johndevman thanks for the patch.
I think this method will work though I am not sure what would happen with field fields. Will the files be removed?

I think the test failure is because of #2970801: If you add block then try to Revert the layout it doesn't revert

So created a dataProvider function for \Drupal\Tests\layout_builder\FunctionalJavascript\InlineBlockContentBlockTest::testNoLayoutSave to demonstrate that it works for "cancel" but not "revert"

So this will still fail for the "revert" case but should pass for the "cancel" case.

johnwebdev’s picture

@tedbow Good question. What would the expected state of the block be if, it's deleted on an entity? It should not be deleted, because of revisioning. So I suppose the image will remain, right?

It should be deleted if the host entity is deleted (like Paragraphs) though(?) We should write tests for this.

johnwebdev’s picture

Added a start on testing revisioning. Needs more scenarios covered... but this is a start :)

tedbow’s picture

Status: Needs work » Needs review

@johndevman thanks for the continued progress! Setting to need review to run tests.

tedbow’s picture

Status: Needs review » Needs work
  1. +++ b/core/modules/layout_builder/tests/src/FunctionalJavascript/InlineBlockContentBlockTest.php
    @@ -245,2 +245,78 @@
    +  /**
    +   * {@inheritdoc}
    +   */
    

    Can't use @inheritdoc on test methods that don't override a method.

  2. +++ b/core/modules/layout_builder/tests/src/FunctionalJavascript/InlineBlockContentBlockTest.php
    @@ -245,2 +245,78 @@
    +      'access contextual links',
    +      'configure any layout',
    +      'administer node display',
    +      'administer node fields',
    +      'administer nodes',
    +      'bypass node access',
    

    I don't think all of these permissions are needed. We should only have the permission needed to do the actions in the test.

  3. +++ b/core/modules/layout_builder/tests/src/FunctionalJavascript/InlineBlockContentBlockTest.php
    @@ -245,2 +245,78 @@
    +    $assert_session->elementExists('css', '.form-submit');
    +    $this->click('.form-submit');
    

    I think we should be able to just do $page->pressButton('Revert') like the other button presses in this method.

tedbow’s picture

Status: Needs work » Needs review
FileSize
6.21 KB
37.24 KB
  1. I added an in progress fix from #2970801: If you add block then try to Revert the layout it doesn't revert to \Drupal\layout_builder\Form\RevertOverridesForm::submitForm
  2. Updated \Drupal\Tests\layout_builder\FunctionalJavascript\InlineBlockContentBlockTest::testInlineBlocks() to cover canceling/reverting a layout with blocks that existing in the layout but with edits since last layout save. The block edits should not be saved. This passes.
  3. But also if there is inline content block in override and the layout is reverted and the inline content block is used nowhere else it should be deleted. I added test coverage for this but it will not pass 😞.

    This case is much more complicated and must handle these cases:

    1. The inline content block was created in the default and still is referenced in the default. In this case the content block should not be deleted.
    2. The inline content block was created in the default but is no long referenced in the default.

    In this 2nd case the inline content block may still be referenced in a layout override for different entity for the same bundle if the override was created before the inline content block was removed from default. So in this case the content block should not deleted.
    But also in this 2nd case the inline content block may not be referenced if has removed from all overrides or if all the overrides were created after the inline block was removed from the default.

    There is actually no way to know this except by checking all entities of this bundle that have overrides.

    For this reason it may make more sense when first creating a layout override to duplicate any non-reusable blocks reference. Then when it is removed from an override it will then can safely be deleted from the content block storage.

johnwebdev’s picture

This is a started patch on duplicating inline blocks when overriding.

Still needs to update test.

johnwebdev’s picture

johnwebdev’s picture

.. Meh.. whitespace.

johnwebdev’s picture

Status: Needs work » Needs review
johnwebdev’s picture

#44 Regarding deletion, there many scenarios we need to take under consideration:

a) Override
1) When the entity is deleted
2) When an block is removed and the layout is saved
3) When the layout is reverted to default

b) Default
1) When the content type is deleted
2) When the block is removed and the layout is saved

c) Layout Builder uninstall.

Initially, I planned to create a class, and do something like:

    $storage = \Drupal::service('entity_type.manager')->getStorage('block_content');

    foreach ($sections as $section) {
      $components = $section->getComponents();

      foreach ($components as $component) {
        $plugin = $component->getPlugin();
        if ($plugin->getBaseId() === 'inline_block_content') {
          $block_revision_id = $component->getConfiguration()['block_revision_id'];
          $block = $storage->loadRevision($block_revision_id);

          $block->delete();
        }
      }
    }

on each of these scenarios through out the code, but I think it would be cleaner if we could implement some event handling for Section storages for these events post save instead? What do you think?

Edit: Posted this idea in Slack:

There are various places where Inline blocks would need to “react” on somethings, based on
a) Something changed to the section list, and b) it is saved. What about adding various events for this? It would save so much hard-coding stuff in LB module to get it to work properly, and it would not need to rely on UIs like the Revert controller, and would work just fine if a contrib. implemented its own, etc.

tedbow’s picture

re @Berdir comment on #14

In general I agree with the idea but I don't think we should make it layout builder specific but allow all content blocks to be placed like this instead.

The changes we having been making to the patch to support not saving blocks before that layouts are saved have actually been connecting to Layout Builder module.

  1. +++ b/core/modules/layout_builder/src/Plugin/Block/InlineBlockContentBlock.php
    @@ -189,9 +190,7 @@
    -    $block->setNewRevision(TRUE);
    -    $block->save();
    -    $this->configuration['block_revision_id'] = $block->getRevisionId();
    +    $this->configuration['block_serialized'] = serialize($block);
    

    So with these changes the content block won't actually be saved/created when the block plugin configuration form is save.

    This means if Block UI module is used to place the block then the content block won't actually be saved.

  2. +++ b/core/modules/layout_builder/src/Plugin/SectionStorage/OverridesSectionStorage.php
    @@ -230,6 +230,28 @@ public function label() {
       public function save() {
    +
    +    $sections = $this->getSections();
    +
    +    foreach ($sections as $section) {
    +      $components = $section->getComponents();
    +
    +      foreach ($components as $component) {
    +        $plugin = $component->getPlugin();
    +        if ($plugin->getBaseId() === 'inline_block_content') {
    +          $configuration = $component->getConfiguration();
    +          if (!empty($configuration['block_serialized'])) {
    +            $block = unserialize($configuration['block_serialized']);
    +            $block->setNewRevision(TRUE);
    +            $block->save();
    +            $configuration['block_serialized'] = NULL;
    +            $configuration['block_revision_id'] = $block->getRevisionId();
    +            $component->setConfiguration($configuration);
    +          }
    +        }
    +      }
    +    }
    

    Here we actually do the saving in \Drupal\layout_builder\Plugin\SectionStorage\OverridesSectionStorage::save(). This connects the functionality to the Layout Builder module.

    I don't see how we get around this.

    One idea would be to save the content blocks in \Drupal\layout_builder\Plugin\Block\InlineBlockContentBlock::blockSubmit so that it would work for Block UI. Then \Drupal\layout_builder\Plugin\SectionStorage\OverridesSectionStorage::save if the block is no longer referenced, in the case it was removed, we delete the content block.

    But then what if user never actually saves the layout but just closes the tab. Then a content would have saved but it is marked as not reusable but it is not used anywhere, orphaned.

We basically need the block form to act 2 different ways. With the Layout Builder don't save Content Block then with other uses save the Content Block in \Drupal\layout_builder\Plugin\Block\InlineBlockContentBlock::blockSubmit.

Could use a different plugin form for layout builder using the functionality we created in #2763157: Allow plugins to provide multiple forms?
This is what we used in Settings Tray to provide a different block plugin form when used with the settings tray.

So basically in \Drupal\layout_builder\Form\ConfigureBlockFormBase::doBuildForm()

We could do something like

if ($block instanceof PluginWithFormsInterface) {
      return $this->pluginFormFactory->createInstance($block, 'layout_builder', 'configure');
    }

Copied from \Drupal\settings_tray\Block\BlockEntitySettingTrayForm::getPluginForm()

tedbow’s picture

Status: Needs work » Needs review
FileSize
6.22 KB
46.88 KB

This gives a try at

We basically need the block form to act 2 different ways. With the Layout Builder don't save Content Block then with other uses save the Content Block in \Drupal\layout_builder\Plugin\Block\InlineBlockContentBlock::blockSubmit.

Could use a different plugin form for layout builder using the functionality we created in #2763157: Allow plugins to provide multiple forms?
This is what we used in Settings Tray to provide a different block plugin form when used with the settings tray.

It allows the configuration form to not save the content block when used with layout builder but to save the content block when used with all other uses, i.e. block ui.

tedbow’s picture

Status: Needs work » Needs review
FileSize
9.94 KB
47.22 KB
  1. +++ b/core/modules/layout_builder/src/Plugin/SectionStorage/SectionStorageBase.php
    @@ -103,4 +103,30 @@ public function removeSection($delta) {
    +      foreach ($components as $component) {
    +        $plugin = $component->getPlugin();
    +        if ($plugin->getBaseId() === 'inline_block_content') {
    

    This requires the Layout Builder \Drupal\layout_builder\Plugin\SectionStorage\SectionStorageBase to know about 'inline_block_content' plugins.

    Really what need here is concept that plugin form could be submitted but we don't yet want to make the changes permanent. In this case it because don't want to create Content Block entity when the plugin configuration form is submitted. For all we know the user may never say the layout and the content block entity would be orphaned.

    So in this patch I am created a PermanentSavePluginInterface to denote a plugin that has extra save step that makes permanent changes.

    So also renaming saveInlineBlocks() to permanentlySaveComponents().

  2. +++ b/core/modules/layout_builder/src/SectionStorageInterface.php
    @@ -138,4 +138,11 @@ public function label();
    +  public function saveInlineBlocks();
    

    I renamed this in the patch but also does actually need to be on the interface? Couldn't this be a protect method on SectionStorageBase

tedbow’s picture

+++ b/core/modules/layout_builder/src/Plugin/SectionStorage/OverridesSectionStorage.php
@@ -230,7 +231,34 @@ public function label() {
+  public function duplicateDefaultsInlineCustomBlocks($sections) {
+    /** @var Section $section */
+    foreach ($sections as $section) {
+      $components = $section->getComponents();
+
+      foreach ($components as $component) {
+        $plugin = $component->getPlugin();
+        if ($plugin->getBaseId() === 'inline_block_content') {
+          $configuration = $component->getConfiguration();
+          if (!empty($configuration['block_revision_id'])) {
+            $entity = $this->entityTypeManager->getStorage('block_content')->loadRevision($configuration['block_revision_id']);
+            $duplicated_entity = $entity->createDuplicate();
+            $configuration['block_revision_id'] = NULL;
+            $configuration['block_serialized'] = serialize($duplicated_entity);
+            $component->setConfiguration($configuration);
+          }
+        }
+      }
+    }
+
+    return $sections;
+  }

I feel like we should also abstract this away from inline_block_content plugins.

Should we have some concepts of plugins that are not safe to just create duplicate configuration of. Meaning we should tell the plugin that we want to create duplicate and would do any necessary operations if any and update configuration if needed.

tedbow’s picture

Mostly nits.

  1. +++ b/core/modules/block_content/src/BlockContentInterface.php
    @@ -48,6 +48,28 @@ public function setInfo($info);
    +  public function setReusable();
    +
    +  /**
    +   * Sets the block not to be re-usable.
    +   *
    +   * @return $this
    +   */
    +  public function setNotReusable();
    

    Don't think we need both here we can just a have a $reusable parameter.

  2. +++ b/core/modules/block_content/src/Entity/BlockContent.php
    @@ -200,6 +200,11 @@ public static function baseFieldDefinitions(EntityTypeInterface $entity_type) {
    +      ->setLabel(t('Re-usable'))
    

    "Reusable" is one word. Changing all.

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

    "upto" is 2 words. Won't comment on whether "spiffiness" is a word 😉

  4. +++ b/core/modules/layout_builder/src/Form/ConfigureBlockFormBase.php
    @@ -237,7 +237,8 @@ protected function successfulAjaxSubmit(array $form, FormStateInterface $form_st
    diff --git a/core/modules/layout_builder/src/Form/InlineBlockDefaultForm.php b/core/modules/layout_builder/src/Form/InlineBlockDefaultForm.php
    
    +++ b/core/modules/layout_builder/src/Plugin/Block/InlineBlockContentBlock.php
    @@ -0,0 +1,275 @@
    +   * @var \Drupal\Core\Session\AccountInterface.
    

    Should not have period.

  5. +++ b/core/modules/layout_builder/src/Plugin/Block/PermanentSavePluginInterface.php
    @@ -0,0 +1,18 @@
    +}
    \ No newline at end of file
    diff --git a/core/modules/layout_builder/src/Plugin/Derivative/InlineBlockContentDeriver.php b/core/modules/layout_builder/src/Plugin/Derivative/InlineBlockContentDeriver.php
    

    Whoops I have fixed my editor now.

  6. +++ b/core/modules/layout_builder/src/Plugin/SectionStorage/OverridesSectionStorage.php
    @@ -230,7 +231,34 @@ public function label() {
    +    $this->permanentlySaveComponents();
    

    This can be move up to the parent class.

  7. +++ b/core/modules/layout_builder/src/SectionComponent.php
    @@ -197,7 +197,7 @@ public function setWeight($weight) {
    -  protected function getConfiguration() {
    +  public function getConfiguration() {
    

    Can't change this back to protected but if we change \Drupal\layout_builder\Plugin\SectionStorage\OverridesSectionStorage::duplicateDefaultsInlineCustomBlocks to something more generic we can.

johnwebdev’s picture

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

    It's taken from a similar test in core!

  2. +++ b/core/modules/layout_builder/src/Plugin/Block/InlineBlockContentBlock.php
    @@ -0,0 +1,274 @@
    +      $this->setConfigurationValue('block_serialized', NULL);
    +      $this->configuration['block_serialized'] = NULL;
    

    Nit: We can remove one of these.

  3. +++ b/core/modules/layout_builder/src/Plugin/SectionStorage/OverridesSectionStorage.php
    @@ -230,7 +231,36 @@ public function label() {
    +        if ($plugin instanceof DerivativeInspectionInterface) {
    +          if ($plugin->getBaseId() === 'inline_block_content') {
    

    Could be one-liner?

tedbow’s picture

Assigned: Unassigned » tedbow
+++ b/core/modules/layout_builder/src/Controller/LayoutBuilderController.php
@@ -121,6 +122,11 @@ protected function prepareLayout(SectionStorageInterface $section_storage, $is_r
+        // Create a duplicate of each default inline custom block.
+        if ($sections) {
+          $sections = $section_storage->duplicateDefaultsInlineCustomBlocks($sections);
+        }

Now I am thinking what we actually need here is an event for preparing the layout for the UI.

Since Inline Content Blocks ultimately probably should live in the Layout Builder module then we don't want specific logic for them here.

I will give this a try.

tedbow’s picture

Status: Needs work » Needs review
FileSize
12.22 KB
53.58 KB

Here is rough start for #61

The event doesn't work how I would like yet though I think it works for what inline blocks needs. But it would be nice if it fired whenever a layout was prepared for the UI not just creating an override.

I have to switch to something else now so just wanted to get this start out there but should able to get back this in a few hours.

tedbow’s picture

So more thoughts

  1. #51

    #44 Regarding deletion, there many scenarios we need to take under consideration:

    a) Override
    1) When the entity is deleted
    2) When an block is removed and the layout is saved
    3) When the layout is reverted to default

    b) Default
    1) When the content type is deleted
    2) When the block is removed and the layout is saved

    For override we couldn't delete a content block when it was removed from the layout unless the parent entity is not using revisions
    We could only delete when delete content blocks when the parent entity is deleted. and then we would have to make sure every content block referenced in ever revision is delete.

    Since we could have 100's or 1000's of revisions this might be difficult to do on saving the parent entity.

  2. We would probably want to avoid non-reusable blocks from having their own layout overrides. or when delete parent entity that references content blocks we would also to check for every revision of the content block for inline non-usable content blocks... and so on
  3. Been chatting with @johndevman about how we could track all referenced non-usable content blocks. To avoid tracking table we could keep append usage for every revision to the override layout information of the latest revision.

    We would only have to store the ids. This way we wouldn't have deserialize every layout revision.

    We could add `OverridesSectionStorage::addNonResubleEntityUsage()` and `::getAllNonReusbleEnityUses()` then on parent entity delete would could all `::getAllNonReusbleEnityUses()` to get all entities to delete

    The only problem I can think of right now would be what if `::getAllNonReusbleEnityUses()` returned 100's of entities. could you delete them all when deleting the parent entity?

  4. We could just add an EntityUsage service like have for FileUsage service but I think should explore other options first like OverridesSectionStorage::addNonResubleEntityUsage()

    if we had a `EntityUsageService` then in `hook_entity_delete` we could call `EntityUsage::removeUsagesWhereEntityIsUser($entity_deleted)`

    Assuming that you only use this service for entities that should be deleted when not used elsewhere.

johnwebdev’s picture

  1. +++ b/core/modules/layout_builder/src/Plugin/Block/InlineBlockContentBlock.php
    @@ -0,0 +1,274 @@
    +    $form['title']['#description'] = $this->t('The title of the block as shown to the user.');
    

    This doesn't do anything right now.

  2. +++ b/core/modules/layout_builder/src/Plugin/Block/InlineBlockContentBlock.php
    @@ -0,0 +1,274 @@
    +      unset($form['label']['#default_value']);
    

    Can we make the Block description optional? It is not really required for many blocks when used inline. (it is required for the block UI, and if to be changed through Block UI, since required) but not in LB context, Perhaps we could use some sensible fallback if used in LB and you don't want the field shown.

johnwebdev’s picture

This patch begins to explore the idea of tracking dependent entities on the storage. It implements a new interface PluginWithDependentEntitiesInterface, an idea that tedbow had.

Some hacky code from my experiments remains, but it works.

johnwebdev’s picture

Status: Needs work » Needs review
johnwebdev’s picture

  1. +++ b/core/modules/layout_builder/src/Plugin/Block/InlineBlockContentBlock.php
    @@ -273,2 +273,20 @@
    +      $entity = $this->entityTypeManager->getStorage('block_content')->loadRevision($this->configuration['block_revision_id']);
    ...
    +    // @TODO Store dependent entity on configuration instead?
    

    I think we can store the ID here instead on the configuration?

  2. +++ b/core/modules/layout_builder/layout_builder.module
    @@ -81,3 +82,22 @@ function layout_builder_field_config_delete(FieldConfigInterface $field_config)
    +    $dependent_entities = unserialize($entity->get('layout_builder__layout')->dependent_entities);
    

    Can the unserialize be done on the field item instead?

  3. +++ b/core/modules/layout_builder/src/Plugin/Block/PluginWithDependentEntitiesInterface.php
    @@ -0,0 +1,25 @@
    +   * Add dependent entities.
    

    Needs docblock updated with return type.

  4. +++ b/core/modules/layout_builder/src/Plugin/Field/FieldType/LayoutSectionItem.php
    @@ -35,6 +35,10 @@ public static function propertyDefinitions(FieldStorageDefinitionInterface $fiel
    +    $properties['dependent_entities'] = DataDefinition::create('any')
    

    Not sure which data type to use here?

  5. +++ b/core/modules/layout_builder/src/Plugin/Field/FieldType/LayoutSectionItem.php
    @@ -46,7 +50,9 @@ public function __get($name) {
    +    if (method_exists($this, 'getProperties')) {
    +      $this->getProperties();
    +    }
    

    This is not needed. It failed when I used the wrong data type.

  6. +++ b/core/modules/layout_builder/layout_builder.module
    @@ -81,3 +82,22 @@ function layout_builder_field_config_delete(FieldConfigInterface $field_config)
    +    $storage = \Drupal::service('entity_type.manager')->getStorage('block_content');
    ...
    +      $entity = $storage->load($dependent_entity_id);
    

    Should we support any entity type rather than just block content?

tedbow’s picture

  1. +++ b/core/modules/layout_builder/layout_builder.module
    @@ -81,3 +82,22 @@ function layout_builder_field_config_delete(FieldConfigInterface $field_config)
    +  if ($entity->hasField('layout_builder__layout')) {
    

    Need to check if $entity instance of FieldableEntityInterface

  2. +++ b/core/modules/layout_builder/src/Plugin/Block/PluginWithDependentEntitiesInterface.php
    @@ -0,0 +1,25 @@
    +   * Add dependent entities.
    +   */
    +  public function addDependentEntities();
    

    Not sure this method is needed on the interface. InlineBlockContentBlock doesn't need it and really neither does OverridesSectionStorage because it is only called internally

    If each plugin keeps track of it's own dependent entities this doesn't need to be public.

  3. +++ b/core/modules/layout_builder/src/Plugin/Block/PluginWithDependentEntitiesInterface.php
    @@ -0,0 +1,25 @@
    +  /**
    +   * Get dependent entities.
    +   *
    +   * @return array
    +   *   An array of entity IDs.
    +   */
    +  public function getDependentEntities();
    

    For this to work outside of layout builder would need to return an array where the keys are entity type ids and values are array of entity ids.

    layout_builder_entity_predelete currently is hardcode for block_content entity type.

  4. +++ b/core/modules/layout_builder/src/Plugin/Field/FieldType/LayoutSectionItem.php
    @@ -35,6 +35,10 @@ public static function propertyDefinitions(FieldStorageDefinitionInterface $fiel
    +    $properties['dependent_entities'] = DataDefinition::create('any')
    +      ->setLabel(new TranslatableMarkup('Dependent Entities'))
    +      ->setRequired(FALSE);
    

    This maybe the part I like best about the change in this interdiff, keeping the dependent_entities at a field property.

    But it does kind of field weird to keep information about pass revisions in the current revision field value.

  5. +++ b/core/modules/layout_builder/src/Plugin/Field/FieldType/LayoutSectionItem.php
    @@ -46,7 +50,9 @@ public function __get($name) {
    +    if (method_exists($this, 'getProperties')) {
    +      $this->getProperties();
    +    }
    

    Not sure how this is related.

  6. +++ b/core/modules/layout_builder/src/Plugin/SectionStorage/OverridesSectionStorage.php
    @@ -31,7 +32,7 @@
    -class OverridesSectionStorage extends SectionStorageBase implements ContainerFactoryPluginInterface, OverridesSectionStorageInterface {
    +class OverridesSectionStorage extends SectionStorageBase implements ContainerFactoryPluginInterface, OverridesSectionStorageInterface, PluginWithDependentEntitiesInterface {
    

    DefaultsSectionStorage storage would also need to implement PluginWithDependentEntitiesInterface because default also will have InlineContentBlock plugins. So maybe SectionStorageBase would implement it instead.

Now I wondering if PluginWithDependentEntitiesInterface was a good idea 😜

johnwebdev’s picture

The proposed solution for dependent entities in #66 won't work because it assumes that the latest/current revision keeps track of ALL dependent entities. If the entity is reverted to an older revision that could mean that some dependent entities are NOT in the new revision and latest revision.

So we'll continue from patch #62 instead, moving forward with perhaps the entity usage table.

tedbow’s picture

Status: Needs work » Needs review
FileSize
12.45 KB
61.17 KB

This adds tests for making sure content blocks are deleted when they should be. Currently it will fail.

Agree with #71, @johndevman thanks for exploring the PluginWithDependentEntitiesInterface idea.

Thoughts

  • This patch add schema for Inline Blocks plugin which was missing. It was causing undetected errors because some places we were saving the layout but not confirming the save message.
  • Interested EntityUsage service/table idea which would be similar to file usage. 1 big difference to file usage is that the way it works currently is that a inline block content would only be used by 1 entity or default.

    If we didn't duplicate blocks then that won't be true. I suggested duplicating blocks in override to make the usage tracking easier. So maybe if we had better tracking duplicating would not be better.

    One strange situation if we didn't duplicate blocks in overrides would be you could get revision histories that would be:

    • default create block
    • edit from default
    • edit from node/1
    • edit from default
    • edit from node/2
    • edit from default

    Of course maybe this doesn't matter if you don't look at revisions for an individual inline block but if someone wanted to build a view to look at them there won't be a way to separate out what the revisions are connect to.

  • One reason to keep duplicating blocks in overrides is that it might make tracking unnecessary. Looking at how \Drupal\file\FileUsage\FileUsageBase::delete actually
    works deleting a file usage just marks the file has temporary and then file_cron actually deletes the files. In our case we would need to delete blocks on cron because there could be hundreds in the revisions for a single node. But if we are duplicating the content blocks from the default to the override we maybe able to skip the tracking.

    Here is an idea:

    1. Change the new reusable field from a boolean to reusable_state an integer. Have 3 constants for the states REUSABLE, NON_REUSABLE_ACTIVE, and NON_REUSABLE_INACTIVE,
    2. When an entity that has an override is delete. got through all the revisions and set the reusable_state to NON_REUSABLE_INACTIVE.
    3. on block_content_cron delete NON_REUSABLE_INACTIVE content blocks.

    Now that I write it out maybe that doesn't help 😟. Doing hundreds of ::save() vs hundreds of ::delete() calls is probably not that much better.

  • since defaults don't have revisions we should be safe to delete any inline block content entities as they are removed the layout and the layout is saved.
    For that reason when deleting a bundle we would only have to delete the inline content blocks that were currently on the layout
johnwebdev’s picture

Assigned: tedbow » johnwebdev
tedbow’s picture

Assigned: johnwebdev » tedbow

Talked with @johndevman. Going to give EntityUsage Service a try.

tedbow’s picture

Assigned: tedbow » Unassigned
  1. Looking back I think was wrong in #55 when I introduced the PermanentSavePluginInterface.

    The problem I was trying to solve was that I thought there wasn't a point where we could save the block after the configuration form was saved and we didn't want to save it when the configuration form was save because we would end up with orphaned content blocks.

    Now I realize that what I added in \Drupal\layout_builder\Plugin\Block\InlineBlockContentBlock::savePermanently() can actually be done in hook_presave if it is fieldable entity then just need to check for layout_builder__layout and if the entity type is entity_view_display it will be in the third party settings.

  2. +++ b/core/modules/layout_builder/src/EventSubscriber/PrepareLayoutForUiSubscriber.php
    @@ -0,0 +1,75 @@
    +  public function onPrepareLayout(PrepareLayoutForUiEvent $event) {
    +    if (!$event->isRebuilding()) {
    +      foreach ($event->getSections() as $section) {
    +        $components = $section->getComponents();
    +        foreach ($components as $component) {
    +          $plugin = $component->getPlugin();
    +          if ($plugin instanceof DerivativeInspectionInterface) {
    +            if ($plugin->getBaseId() === 'inline_block_content') {
    +              $configuration = $component->getConfiguration();
    +              if (!empty($configuration['block_revision_id'])) {
    +                $entity = $this->entityTypeManager->getStorage('block_content')->loadRevision($configuration['block_revision_id']);
    +                $duplicated_entity = $entity->createDuplicate();
    +                $configuration['block_revision_id'] = NULL;
    +                $configuration['block_serialized'] = serialize($duplicated_entity);
    +                $component->setConfiguration($configuration);
    +              }
    +            }
    +          }
    +        }
    +
    +      }
    +    }
    

    I am also wondering if we could get rid of this event altogether. Do we need to duplicate the blocks here or would be able to do this also in hook_entity_presave or hook_entiy_save.

    When creating a new override we could check $entity->original to see if it is new or is there another way to know?

    Either when creating a new override we could duplicate the blocks we are actually saving the entity.

    One drawback would this fire on every entity save vs this custom event.

  3. I have started work on the EntityUsage service but probably won't get back to till Monday. But my current work is here https://github.com/tedbow/drupal/tree/2957425-48_entity_usage/core/modul...

Unassigning myself since not working on it currently

tedbow’s picture

Status: Needs work » Needs review
FileSize
6.54 KB
59.71 KB

This does #76.1 gets rid of the need for PermanentSavePluginInterface.

I think it makes sense not to introduce this if we don't need it.

amateescu’s picture

  1. +++ b/core/modules/block_content/block_content.install
    @@ -138,3 +138,18 @@ function block_content_update_8400() {
    +    ->setDefaultValue(TRUE);
    +
    +  $reusable->setInitialValue(TRUE);
    

    There's no need to call setInitialValue()on a separate line :)

  2. +++ 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->loadMultiple();
    +    $block_contents = $this->blockContentStorage->loadByProperties(['reusable' => TRUE]);
    

    I remember that we had to do some kind of optimization for loading config entities only by their ID, because it was quite slow otherwise. So we need to ensure that we're not losing it with this change..

  3. +++ b/core/modules/layout_builder/src/Controller/LayoutBuilderController.php
    @@ -128,11 +146,16 @@ protected function prepareLayout(SectionStorageInterface $section_storage, $is_r
             $sections[] = new Section('layout_onecol');
           }
     
    +
    +
           foreach ($sections as $section) {
             $section_storage->appendSection($section);
           }
    +
    +
           $this->layoutTempstoreRepository->set($section_storage);
         }
    +
    

    There are some extra empty lines here that can be removed.

  4. +++ b/core/modules/layout_builder/src/Plugin/Block/InlineBlockContentBlock.php
    @@ -0,0 +1,274 @@
    + * Defines an inline custom block type.
    

    I think this means that the patch is doing two things:

    1) allows a custom block to be re-usable or not
    2) adds a new concept of "inline block content"

    If that's the case, why aren't we doing it in two separate issues?

tedbow’s picture

Issue summary: View changes

@amateescu thanks for the review!

I think this means that the patch is doing two things:

1) allows a custom block to be re-usable or not
2) adds a new concept of "inline block content"

If that's the case, why aren't we doing it in two separate issues?

I updated the summary to say why I think we should keep this as 1 big patch for now

This issue actually include several concepts that should probably be split out into separate issues.
To determine if end goal is achievable and advisable with this approach lets keep this as a big patch containing the sub tasks until it looks like it is good idea. We won't want to commit small supporting parts if we are going abandon this approach for something like #2948064: Layout Builder should support inline creation of Custom Blocks using entity serialization(currently that issue seems not doable)

tedbow’s picture

Status: Needs work » Needs review
FileSize
15.12 KB
51.98 KB
+++ b/core/modules/layout_builder/src/Controller/LayoutBuilderController.php
@@ -122,11 +137,8 @@ protected function prepareLayout(SectionStorageInterface $section_storage, $is_r
+        $event = new PrepareLayoutForUiEvent($sections, $is_rebuilding);
+        $this->eventDispatcher->dispatch(LayoutBuilderEvents::PREPARE_SECTIONS_FOR_UI, $event);

This patch removes the need for this event that was added in #62. The event was used to duplicate the content blocks from the default when creating an override layout.

This can actually be done in layout_builder_entity_presave without the need for a new event.

The only problem is that to determine if the layout is new override I am using $original_sections_field->isEmpty().
This seems to work now because if you remove all the sections from an override and save the layout you actually revert to default. So if that is intentional then this method will work.

Would we ever want to allow the ability to save an override with no sections?

So that reduced the size of this patch a good bit 🎉

tedbow’s picture

Status: Needs work » Needs review
FileSize
1.91 KB
50.79 KB

Review #79
1. fixed
2. BlockContent entities are not "config entities" does you comment still apply.
3. Fixed this in #81

Other changes.

  1. +++ b/core/modules/block_content/src/Entity/BlockContent.php
    @@ -200,6 +200,11 @@ public static function baseFieldDefinitions(EntityTypeInterface $entity_type) {
    +    $fields['reusable'] = BaseFieldDefinition::create('boolean')
    +      ->setLabel(t('Reusable'))
    +      ->setDescription(t('Determine if the block should be reusable or not.'))
    +      ->setDefaultValue(TRUE);
    +
         return $fields;
       }
     
    @@ -282,6 +287,21 @@ public function setRevisionLogMessage($revision_log_message) {
    
    @@ -282,6 +287,21 @@ public function setRevisionLogMessage($revision_log_message) {
         return $this;
       }
     
    +  /**
    +   * {@inheritdoc}
    +   */
    +  public function isReusable() {
    +    return (bool) $this->get('reusable')->value;
    +  }
    +
    +  /**
    +   * {@inheritdoc}
    +   */
    +  public function setReusable($reusable = TRUE) {
    +    $this->set('reusable', $reusable);
    +    return $this;
    +  }
    +
    

    Just note here these are outside layout builder. After we have a patch validates this whole reuse concept we will probably want split change to block_content module to it's own issue.

    Or if we really wanted to be experimental in this module we could actually extend \Drupal\block_content\Entity\BlockContent

    Use our own class for this entity. We could also swap out the deriver for our own to handle reuse.

    Probably want a separate issue but just saying it could be done.

  2. +++ b/core/modules/layout_builder/src/Plugin/SectionStorage/OverridesSectionStorage.php
    @@ -2,6 +2,7 @@
    +use Drupal\Component\Plugin\DerivativeInspectionInterface;
    

    Unused "use"

  3. +++ b/core/modules/layout_builder/src/SectionComponent.php
    @@ -197,7 +197,7 @@ public function setWeight($weight) {
    -  protected function getConfiguration() {
    +  public function getConfiguration() {
    

    Re #57.7 we don't need to be public anymore 👍

johnwebdev’s picture

  1. +++ b/core/modules/layout_builder/config/schema/layout_builder.schema.yml
    @@ -44,3 +44,20 @@ layout_builder.component:
    +      label: 'Block revisions'
    

    Block revision ID?

  2. +++ b/core/modules/layout_builder/layout_builder.module
    @@ -81,3 +85,51 @@ function layout_builder_field_config_delete(FieldConfigInterface $field_config)
    +      $new_revision = $entity->isNewRevision();
    +      $new_revision = TRUE;
    

    This will always be TRUE.

  3. +++ b/core/modules/layout_builder/src/Plugin/Block/InlineBlockContentBlock.php
    @@ -0,0 +1,295 @@
    +  public function saveBlockContent($new_revision = FALSE, $duplicate_block = FALSE) {
    

    Nice rename. I am unsure about doing duplicate logic within the save method though. This behaviour is kinda coupled to LB only as well. Right?

johnwebdev’s picture

I did some manual testing:

General (things adressed in current patch)

  1. The duplicated block from default layout changed property time is the same, do we want to make sure this is actually the timestamp from when it was duplicated instead?
  2. Do we need to distinguish somehow that the block has been duplicated, or is that implicitly expected? Like adding a placeholder (copy) perhaps?
  3. I feel a bit uncomfortable when working with inline blocks, when there currently are no new parent revision when I save layout. Since, I can't assure old work will remain, because I'm overriding it unless, I tactically create a new revision on the parent beforehand, which is not very usable. This issue would solve that: #2937199: Track Layout override revisions on entities which support revisioning

I started playing some with Content Moderation, and I noticed this issue as well:

Also consider to #2942675: Layout builder should use the active variant of an entity to avoid orphaned revisions add as a blocker? We cannot properly Content Moderation integration without that issue solved.
I am mainly referring to the fact that blocks can be moderated as well.

tedbow’s picture

Ok this patch does a few things(also sorry have not read #85 & #86 yet, thanks @johndevman for looking into these issues)

  1. Removes changes in #53 where I added the 2 forms for the plugin. I realized that since when used with the Block UI placing the block will always involved saving a Block config entity we can use this as a trigger to save the Inline Block. This also means we can use the Block config entity delete hook to delete the Inline Block.
  2. Adds an generic entity usage service. This service is different than file usage service because it doesn't require any changes to the entities being tracked like setting the file to temporary. It keeps records where $count == 0 to denote entities that were used but have had all there usages removed.
  3. Adds an InlineBlockContentUsage service to keep track of Inline Block entities and make sure they are deleted when necessary. This logic is bit complicated I was doing this all in entity_crude hooks so I changed these hooks to just call the service.

    #2937199: Track Layout override revisions on entities which support revisioning keeps this from working correctly with revisions.

    \Drupal\Tests\layout_builder\FunctionalJavascript\InlineBlockContentBlockTest::testDeletion is still not passing. My manual testing seems to indicate that it working so I have figure if the test is wrong or if there is some other bug.

    Except for Inline Blocks that are placed via the Block UI the Inline Blocks are actually deleted on cron not entity deleted. It is possible that for LayoutBuilderEntityViewDisplay we should just delete the Inline Blocks when the block is deleted.

    It also attempts to delete Inline Blocks if the entity is not a new revision on update.

johnwebdev’s picture

Great work @tedbow, here's just a quick review when looking at first part of the patch (it's quite big). Will look in to it more in depth later.

  1. +++ b/core/modules/layout_builder/layout_builder.install
    @@ -38,3 +38,64 @@ function layout_builder_install() {
    +      'entity_id' => [
    

    An entity ID can also be a string, so I don't think we can store it as int due to generic usage.

  2. +++ b/core/modules/layout_builder/layout_builder.install
    @@ -38,3 +38,64 @@ function layout_builder_install() {
    +        'length' => 64,
    

    We can use EntityTypeInterface::ID_MAX_LENGTH instead.

  3. +++ b/core/modules/layout_builder/layout_builder.install
    @@ -38,3 +38,64 @@ function layout_builder_install() {
    +      // @todo will the user always be an entity. File usage uses object
    

    I think we should keep it open, if possible.

  4. +++ b/core/modules/layout_builder/src/DatabaseBackendEntityUsage.php
    @@ -0,0 +1,184 @@
    +   * The name of the SQL table used to store file usage information.
    

    file usage => entity usage

  5. +++ b/core/modules/layout_builder/src/DatabaseBackendEntityUsage.php
    @@ -0,0 +1,184 @@
    +   *   (optional) The table to store entitys usage info. Defaults to 'v'.
    

    Defaults to 'v' => Defaults to 'entity_usage'

  6. +++ b/core/modules/layout_builder/src/DatabaseBackendEntityUsage.php
    @@ -0,0 +1,184 @@
    +   * Adds usage by ids.
    

    Nit: id => IDs?

  7. +++ b/core/modules/layout_builder/src/DatabaseBackendEntityUsage.php
    @@ -0,0 +1,184 @@
    +  public function removeByEntities(EntityInterface $used_entity, EntityInterface $user_entity, $count = 1) {
    

    $count is not used here.

  8. +++ b/core/modules/layout_builder/src/DatabaseBackendEntityUsage.php
    @@ -0,0 +1,184 @@
    +  public function getEntitiesWithNoUses($entity_type_id, $limit = 100) {
    

    Nice with $limit, was just thinking about it, when looking earlier in code.

  9. +++ b/core/modules/layout_builder/src/DatabaseBackendEntityUsage.php
    @@ -0,0 +1,184 @@
    +  public function deleteByEntity(EntityInterface $entity) {
    

    Can we make the method name more explicit, is this used or user?

johnwebdev’s picture

Assigned: Unassigned » johnwebdev
johnwebdev’s picture

Ok, let's see what test bot says.

So I updated patch to delete node through UI, and it works. I also tested this manually. And I also tried deleting through Node::load(1)->delete() via CLI, which also worked. So the failing tests seems to be only happening in the test case...

I also updated testNoLayoutSave::revert to not assert empty blocks because with current logic in patch this is not the intended behaviour anymore.

johnwebdev’s picture

Status: Needs work » Needs review
johnwebdev’s picture

Assigned: johnwebdev » Unassigned
johnwebdev’s picture

This is just a rough start on working on test coverage for Entity Usage service. I think @tedbow planned doing some changes, so I just upload this now, that I can continue later when the API becomes more in its finished state.

Edit: Looks like random fails, since the diffs in this patch should not affect these tests.

johnwebdev’s picture

Status: Needs work » Needs review
tedbow’s picture

Status: Needs work » Needs review
FileSize
27.59 KB
78.01 KB

re #95 thanks for starting \Drupal\Tests\layout_builder\Kernel\EntityUsageTest this patch expands on it which uncovered some bugs.

In this patch

  1. Changed terminology from used/user to parent/child - much more readable
  2. Added \Drupal\layout_builder\EntityUsageBase because some the methods didn't actually do any database logic. These were entity related helper methods just to improve DX.
  3. Changed the logic in ::remove() to return the total remaining uses of the child entity not just by parent being removed.
  4. Expanded test coverage
tedbow’s picture

+++ b/core/modules/layout_builder/tests/src/FunctionalJavascript/InlineBlockContentBlockTest.php
@@ -0,0 +1,539 @@
+    // Remove block from override.
+    // Currently revisions are not actually created so this check will not pass.
+    // @see https://www.drupal.org/node/2937199
+    /*$this->removeInlineBlockFromLayout();
+    $this->assertSaveLayout();
+    $cron->run();
+    // Ensure content block is not deleted because it is needed in revision.
+    $this->assertNotEmpty(BlockContent::load($node_1_block_id));
+    $this->assertCount(2, BlockContent::loadMultiple());*/

I have confirmed the patch here #2937199-13: Track Layout override revisions on entities which support revisioning allows these test lines to be uncommented.

So that will need to be blocker for this issue to work correctly

tedbow’s picture

Status: Needs work » Needs review
FileSize
85.11 KB
9.38 KB

This does a few things

  1. Prevent access to block_content entity update and delete routes if the block is not reusable. These blocks should never been edited or deleted outside of their parent entities.
  2. Creates the entity_usage table in an update hook.
  3. Updates the configuration for the "Custom Block Library" view to filter on reusable blocks. Non-usable blocks should not be shown here.
  4. In an update hook adds a is reusable filter to this view. The config changes will only take effect on module install. It specifically only change the "Custom Block Library" view not other views against this entity type. Should we attempt to add the filter to all views against this entity type? I would think not be because then we would have to deal with relationships also where block_content entity type is brought in as a relationship.
johnwebdev’s picture

#101

+++ b/core/modules/block_content/block_content.install
@@ -152,3 +152,34 @@ function block_content_update_8600() {
+ * Update 'block_content' view to filter on new 'reusable' field.
...
+function block_content_update_8601() {

Can we do this? Given the user might have changed that view?

Nvm: Looks like this is being done in #2909435: Update block library page to adapt publishable block content implementation, so I suppose it's not an issue.

Berdir’s picture

Am I the only one who thinks this issue is getting way too big ? :) Adding poormans-paragraphs *and* entity_usage in a single issue?

Did not actually review the patch yet, but it's 80+ kb changes, a lot of it on an existing stable module/entity type and I guess there is still a lot to do? If we do really want to go down that path, would it maybe make more sense to do this with a completely new, experimental module/entity type? I'd be much less worried about introducing such a huge change then and we would have a clean separation and not have to worry about existing views and all that.

johnwebdev’s picture

#104

Am I the only one who thinks this issue is getting way too big ? :) Adding poormans-paragraphs *and* entity_usage in a single issue?

I think this part in the IS summarises it well:

This issue actually include several concepts that should probably be split out into separate issues.
To determine if end goal is achievable and advisable with this approach lets keep this as a big patch containing the sub tasks until it looks like it is good idea. We won't want to commit small supporting parts if we are going abandon this approach for something like #2948064: Layout Builder should support inline creation of Custom Blocks using entity serialization(currently that issue seems not doable)

...

Did not actually review the patch yet, but it's 80+ kb changes, a lot of it on an existing stable module/entity type and I guess there is still a lot to do? If we do really want to go down that path, would it maybe make more sense to do this with a completely new, experimental module/entity type? I'd be much less worried about introducing such a huge change then and we would have a clean separation and not have to worry about existing views and all that.

I think most of the required changes are present in this patch now.

Yeah, it's not a bad idea. Introducing a new entity type would most likely make some issues easier to solve. For instance, we could have a parent id stored on that entity type and no longer need the entity_usage?
Though, if we want that new entity type to be re-usable as well, we are just implementing Block content again, but with the use case flipped. If not, a User might need to maintain both a Inline block and a Content block entity type doing the same thing, would that be reasonable?

Berdir’s picture

Fine if this is just a place to experiment, did not read the updated issue summary, sorry :)

Well, if they want to use a reusable block they can still use a standard block_content entity, because with reusable, the expectations on revisions are very different and while the UX might not be great when editing things inline (same with media and other similar use cases), it can't be by-reference-id because editing the block over the overview must work. That's what workspaces are for then.

tedbow’s picture

@Berdir thanks for taking look again. Yes it is huge issue but I think it is still an experiment. Using a different entity might eliminate the need for entity tracking for instance.

For using a new entity type instead of block content:

Pros:

  1. Don't have add property to existing entity type
  2. Don't have to alter existing views
  3. Might not have to add entity usage tracking service(if we can add parent id to new entity type)
  4. Don't have to add access check on existing for block content edit and delete forms(b/c they shouldn't be update outside of parent)
  5. Having the parent_id on the new inline_content entity type as base entity reference field would add possibilities for making relation ships via Views This would only work if we had ERR
  6. All the changes could happen inside Layout Builder which is still an experimental module(though beta so we still have BC concerns)

Cons:

  1. Existing sites would have to configure bundles instead of using existing ones
  2. Blocks that were inline vs existing will have different bundles so they will have different field configuration. If would want a "Video Block" or "Gallery" block to be usable with block UI and layout builder inline block you would have to keep the the fields in sync.
  3. Theme inline and reusable blocks would be different.
  4. New permissions to manage(this could be Pro for more control)

For the biggest win from a site builder perspective for using the existing "Block Content" entity type would be the same block types could be used in both situations. I assume this would be much easier for theming but that is out of experience area.

Maybe though if this is really a "poor man's paragraph" then having different block types will not be a drawback as this what people are use to with paragraphs.

hawkeye.twolf’s picture

re:

Blocks that were inline vs existing will have different bundles so they will have different field configuration. If would want a "Video Block" or "Gallery" block to be usable with block UI and layout builder inline block you would have to keep the the fields in sync.

and:

Theme inline and reusable blocks would be different.

Is there some clever way of making a new entity type that actually just inherits the bundle configuration and theme hooks of another entity type? (and disable Field UI for the new entity type). That all might be confusing as hell for developers and a bastardization that we don't want in core, but the idea just occurred to me.

samuel.mortenson’s picture

@derek.hawkeye.deraps That's basically how #2844054: Inline Revisionable Content Blocks works, sounds like we're getting back to the CTools approach after all this time.

DamienMcKenna’s picture

I'd vote for allowing the same block entity bundles to be useable here, it feels like an arbitrary separation otherwise.

tedbow’s picture

Title: Explore the concept of Custom Block re-usability » Allow the inline creation of non-reusable Custom Blocks in the layout builder

Updating the title for now. Need to update the summary soon.

tedbow’s picture

tedbow’s picture

Status: Needs work » Needs review
FileSize
39.62 KB
87.23 KB

This patch creates \Drupal\Tests\layout_builder\FunctionalJavascript\InlineBlockTestBase which will be shared with #2974860: Allow inline content creation in layout builder through a single purpose entity type.

This ensure we are testing the same functionality where appropriate. @see #2974864-7: [Meta] Determine best method of allowing inline content creation Layout Builder for reasoning

tedbow’s picture

+++ b/core/modules/layout_builder/tests/src/FunctionalJavascript/InlineBlockTestBase.php
@@ -533,7 +541,28 @@ protected function addInlineBlockToLayout($title, $body) {
+  protected function loadBlock($id) {
+    return $this->loadBlock($id);

Copy & paste recursion error 🙀

tedbow’s picture

Status: Needs work » Needs review
FileSize
92.64 KB
12.37 KB

in the comment 5 on #2974864-5: [Meta] Determine best method of allowing inline content creation Layout Builder I was worried about

I thought of another problem with [THIS_ISSUE] block access.
We have to worry about the case where the site builder either creates view that exposes list of the inline blocks or sets up a entity reference to point to inline blocks.

Because determining view access will be tricky, based on view access the revision that inline block placed on.
Right now \Drupal\block_content\BlockContentAccessControlHandler::checkAccess just gives view access if the block is published.

I go on for another 5 paragraphs after... 😜

I am hoping we can actually handle this access problem.

This patch does

  1. Adds AccessDependentInterface(and AccessDependentTrait to implement it). This simply has getter and setter for an access dependee.
  2. InlineBlockContentBlock and BlockContent both implement AccessDependentInterface
  3. In \Drupal\layout_builder\EventSubscriber\BlockComponentRenderArray::onBuildRender set call $block->setAccessDependee($entity) passing on layout entity as a dependency.
  4. In InlineBlockContentBlock::getEntity() pass on dependency to BlockContent
  5. Now when ::\Drupal\layout_builder\Plugin\Block\InlineBlockContentBlock::blockAccess get called the block entity will have the dependency set
  6. Also the dependency will set in \Drupal\layout_builder\Plugin\Block\InlineBlockContentBlock::build this allows field blocks on Block entities one level down to still be rendered in the layout builder default. (i.e. Node entity -> Content Block -> field block body(checks entity access) 💥🧠)
  7. Updates BlockContentAccessControlHandler to check non-reusable block and checks the access on the dependee.
  8. All existing blocks will be reusable so this logic should not effect them.
tedbow’s picture

Issue summary: View changes
tedbow’s picture

Status: Needs work » Needs review
FileSize
6.5 KB
99.14 KB

So #117 stop you from being able to view non-reusable blocks but they will still should up as referenceable entities in entity reference field.

This is because:

  1. The block_content module does not provide a EntityReferenceSelection plugin
  2. The \Drupal\Core\Entity\Plugin\EntityReferenceSelection\DefaultSelection::buildEntityQuery does add the tag 'block_content_access' but the block_content module does not do anything with this because up to tell now this was not a problem. Blocks really didn't have access control

We could add a EntityReferenceSelection plugin to the block_content module to override buildEntityQuery but this would not effect any custom EntityReferenceSelection plugins that currently exist and are used with entity reference fields targeting block_content entities.

Instead I am adding block_content_query_block_content_access_alter which:
Checks for tags 'entity_query_block_content', 'entity_reference', 'block_content_access'(because of function name)
checks for table block_content_field_data
checks for any conditions against block_content_field_data.reusable(nested also)
If there are no conditions set against this field and all other conditions are true the it $query->condition('block_content_field_data.reusable', TRUE);

This should stop the DefaultSelection plugin from returning any non-reusable block entities. It should also work against other EntityReferenceSelection plugins unless they are adding the proper tags in which we should not alter them.

tedbow’s picture

Status: Needs work » Needs review
FileSize
6.53 KB
99.17 KB

Unable to generate test groups

🙀
Fixed.

Doing interdiff against #117

tedbow’s picture

Assigned: Unassigned » tedbow

Working on big self-review patch

tedbow’s picture

Status: Needs work » Needs review
FileSize
29.01 KB
97.13 KB
  1. +++ b/core/modules/block_content/block_content.services.yml
    @@ -4,7 +4,3 @@ services:
    -  access_check.entity.block_content:
    -    class: Drupal\block_content\Access\ReusableAccessCheck
    -    tags:
    -      - { name: access_check, applies_to: _is_reusable_block_content }
    

    This service to check route access is not no longer needed because the logic has been moved into the access controller.

  2. +++ b/core/modules/block_content/src/BlockContentAccessControlHandler.php
    @@ -19,26 +19,23 @@ class BlockContentAccessControlHandler extends EntityAccessControlHandler {
    -    if ($operation === 'view') {
    +    /** @var \Drupal\block_content\BlockContentInterface $entity */
    +    if (!$entity->isReusable()) {
    

    checking all operations. This means you can't update non-reusable custom online blocks outside of layout builder.

  3. +++ b/core/modules/block_content/tests/src/Kernel/BlockContentEntityReferenceSelectionTest.php
    @@ -33,6 +33,8 @@ class BlockContentEntityReferenceSelectionTest extends KernelTestBase {
    +    $this->installSchema('system', ['sequence']);
    +    $this->installEntitySchema('user');
    

    This test was passing locally for me but not on DrupalCI. Seeing if this fixes it.

  4. +++ b/core/modules/layout_builder/src/DatabaseBackendEntityUsage.php
    @@ -130,35 +130,31 @@ public function listUsage(EntityInterface $child_entity) {
    +    $sub_query = $this->connection->select($this->tableName, 't2');
    +    $sub_query->where('t1.entity_id = t2.entity_id');
    +    $sub_query->condition('t2.entity_type', $child_entity_type_id)
    +      ->condition('count', 0, '>');
    +    $sub_query->fields('t2', ['entity_id']);
    +    $query->notExists($sub_query);
    

    Implement the $limit logic which needed to actually be converted to use ::notExists

  5. +++ b/core/modules/layout_builder/src/DatabaseBackendEntityUsage.php
    @@ -130,35 +130,31 @@ public function listUsage(EntityInterface $child_entity) {
    -  public function delete($child_entity_type_id, $child_entity_id) {
    +  public function deleteMultiple($child_entity_type_id, array $child_entity_ids) {
    

    This is more efficient than having many separate update calls.

  6. +++ b/core/modules/layout_builder/src/Plugin/Block/InlineBlockContentBlock.php
    @@ -39,13 +39,6 @@ class InlineBlockContentBlock extends BlockBase implements ContainerFactoryPlugi
    -  /**
    -   * The Drupal account to use for checking for access to block.
    -   *
    -   * @var \Drupal\Core\Session\AccountInterface
    -   */
    -  protected $account;
    

    $account was never used after being set.

  7. +++ b/core/modules/layout_builder/src/Plugin/Block/InlineBlockContentBlock.php
    @@ -154,7 +143,6 @@ public function blockForm($form, FormStateInterface $form_state) {
    -    $form['title']['#description'] = $this->t('The title of the block as shown to the user.');
         return $form;
       }
     
    @@ -270,6 +258,7 @@ public function buildConfigurationForm(array $form, FormStateInterface $form_sta
    
    @@ -270,6 +258,7 @@ public function buildConfigurationForm(array $form, FormStateInterface $form_sta
           // can't check $this->getEntity()->isNew().
           unset($form['label']['#default_value']);
         }
    +    $form['label']['#description'] = $this->t('The title of the block as shown to the user.');
    

    This wasn't actually showing. Fixed

  8. +++ b/core/modules/layout_builder/tests/src/FunctionalJavascript/InlineBlockTestBase.php
    @@ -310,6 +310,7 @@ protected function assertSaveLayout() {
    +    $this->assertNotEmpty($assert_session->waitForElement('css', '.messages--status'));
    

    I was getting random failures that I think was because there was no wait for the message.

tedbow’s picture

Status: Needs work » Needs review
FileSize
1.86 KB
97.24 KB

Whoops logic error in \Drupal\block_content\BlockContentAccessControlHandler::checkAccess

Fixed

tedbow’s picture

Assigned: tedbow » Unassigned
Issue summary: View changes
FileSize
94.48 KB
22.47 KB

This patch does

  1. Refactor InlineBlockTestBase to use existing method addInlineBlockToLayout() for all block placements.
  2. Refactor InlineBlockTestBase to sue new method configureInlineBlock() for configuring existing blocks
  3. Remove unneeded test module dependencies in InlineBlockTestBase
  4. Change EntityUsageTest to not create actual entities since most method accept just IDs. Mock an entity when needed.
  5. Simplify the logic in \Drupal\block_content\BlockContentAccessControlHandler::checkAccess(), no change in functionality.
  6. Update BlockContentEntityReferenceSelectionTest to make sure the block is referenceable when change to reusable.

Updated Issue summary.

tedbow’s picture

Status: Needs work » Needs review
tedbow’s picture

tedbow’s picture

In [#2976366-] @phenaproxima rightly stressed how problematic entity usage tracking could be based on our experience with File usage.

See #2821423: Dealing with unexpected file deletion due to incorrect file usage.
but we may have need for Media usage tracking #2835840: Track media usage and present it to the site builder (in the media library, media view, on media deletion confirmation, etc.)

But that got me thinking:

Just seems weird to make the generic entity tracking when our first use case is expressly for for entity that should not be reused.

These inline *non-reusable* blocks are *owned* in a real sense by the layout for the entity they are placed in. We only need to track them because there could hundreds referenced in a single entity because we could have hundreds of revision each with their own *non-reusable* blocks. So I think we could get a timeout if we tried to delete them in hook_entity_delete.

So basically are use of tracking is not to determine if the entity is use still anywhere in the system. Each non-reusable block as used by the Layout Builder will only have 1 entity that is using it.

So this patch adds non-generic usage tracking only for this use case to the Layout Module itself.

tedbow’s picture

Status: Needs work » Needs review
FileSize
79.07 KB

Whoops not all the files got in the combined patch. the interdiff and do-not-test patches should be the same.

tedbow’s picture

+++ b/core/modules/layout_builder/tests/src/FunctionalJavascript/InlineBlockTestBase.php
@@ -0,0 +1,549 @@
+    $assert_session->linkExists('Save Layout');
+    $this->clickLink('Save Layout');
+    $this->assertNotEmpty($assert_session->waitForElement('css', '.messages--status'));

This seems to fail on DrupalCI and randomly fail on my local.

I am not sure why. When I output the page when the message is not there after the wait then it is still on the layout builder page. But usually it passes.

I am changing this for now to manually drupalGet() the href of the link. This seems to fix the random failure for me.

It will at least let us see if the rest of the test passes. Anyone else has any ideas why this might happening?

tedbow’s picture

This patch removes all logic from \Drupal\layout_builder\InlineBlockContentUsage that does deal directly with the usage tracking.

I added \Drupal\layout_builder\EntityOperations that is called from the entity crud hooks and cron. It calls the usage service.

This seems cleaner.

tedbow’s picture

  1. Until #2937199: Track Layout override revisions on entities which support revisioning is fixed we can't know when to make revision of a inline block. So that would mean we would never make revisions for inline blocks and looking back at previous revisions would always give you same revision and we would lose all changes.

    To get around this for now I am making a new revision of the inline block whenever block is edit in a layout and the parent entity supports revisions. If the layout is updated but a particular existing inline block is not edit then this will not cause a new revision. This makes \Drupal\Tests\layout_builder\FunctionalJavascript\InlineBlockTestBase::testInlineBlocksRevisioning() pass for me locally.

  2. For the other test failure I can't reproduce them locally so I am going to add small wait and see if it is a timing issue on DrupalCI
tedbow’s picture

  • Ok the problem with the test failure was actually be
    +++ b/core/modules/layout_builder/layout_builder.install
    @@ -38,3 +40,75 @@ function layout_builder_install() {
    +      'parent_entity_id' => [
    +        'description' => 'The ID of the object using the entity.',
    +        'type' => 'varchar_ascii',
    +        'length' => EntityTypeInterface::ID_MAX_LENGTH,
    

    This was actually causing the last test failure.

    The id length was too short and getting a truncate error.

tedbow’s picture

Self review, big interdiff but hopefully b/c simplifying.

  1. Currently the patch allows adding inline blocks in the Block UI. Since this an experimental module so we should only offer this in layout.

    re @Berdir in #14

    In general I agree with the idea but I don't think we should make it layout builder specific but allow all content blocks to be placed like this instead. That means also thinking about the exact wording for this and how the UI would work for normal blocks. And maybe eventually even deprecate the old way?

    If we want to add this functionality in the Block UI then we should create a follow up. The wording will be very important to not confuse users.

    For now I have implemented hook_plugin_filter_TYPE_alter to disallow this new block plugin except when the $consumer is layout_builder.

    I have also removed the logic from \Drupal\layout_builder\EntityOperations that handle block config entities that used this plugin.

  2. +++ b/core/modules/layout_builder/layout_builder.install
    @@ -38,3 +40,75 @@ function layout_builder_install() {
    +        'length' => 64,
    ...
    +        'length' => 64,
    

    Replace with EntityTypeInterface::ID_MAX_LENGTH,

  3. +++ b/core/modules/layout_builder/layout_builder.install
    @@ -38,3 +40,75 @@ function layout_builder_install() {
    + * Create the 'entity_usage' table.
    

    Table name wrong

  4. +++ b/core/modules/layout_builder/src/EventSubscriber/BlockComponentRenderArray.php
    @@ -56,6 +57,18 @@ public function onBuildRender(SectionComponentBuildRenderArrayEvent $event) {
    +    // Set block access dependee even if we are not checking access on
    

    s/dependee/dependency

  5. +++ b/core/modules/layout_builder/src/EventSubscriber/BlockComponentRenderArray.php
    @@ -56,6 +57,18 @@ public function onBuildRender(SectionComponentBuildRenderArrayEvent $event) {
    +    // this level. The lock itself may render another AccessDependentInterface
    

    s/The lock/The block

  6. +++ b/core/modules/layout_builder/tests/src/FunctionalJavascript/InlineBlockTestBase.php
    @@ -0,0 +1,565 @@
    +/**
    + * Temporary inline-block base test.
    + *
    + * 2 issues are currently trying to solve the same problem of inline block
    + * creation. The 2 different methods should function the same way so the
    + * functional test should be nearly identical to make sure they do.
    + *
    + * @see https://www.drupal.org/node/2957425
    + * @see https://www.drupal.org/node/2974860
    + */
    +abstract class InlineBlockTestBase extends JavascriptTestBase {
    

    I think it is safe to remove this base class because this issue seems more promising than #2974860: Allow inline content creation in layout builder through a single purpose entity type

    This patch remove the need for the base class and simplifies InlineBlockContentBlockTest.

johnwebdev’s picture

Review:

  1. +++ b/core/modules/layout_builder/config/schema/layout_builder.schema.yml
    @@ -44,3 +44,20 @@ layout_builder.component:
    +      label: 'Block revisions'
    

    Block revisions => Block revision ID

  2. +++ b/core/modules/layout_builder/layout_builder.install
    @@ -38,3 +40,75 @@ function layout_builder_install() {
    +        'description' => 'The name of the object type in which the entity is used.',
    ...
    +        'description' => 'The ID of the object using the entity.',
    

    Why do we use the phrasing object?

  3. +++ b/core/modules/layout_builder/layout_builder.module
    @@ -81,3 +83,43 @@ function layout_builder_field_config_delete(FieldConfigInterface $field_config)
    +  $entity_operations->handlePreSave($entity);
    ...
    +  $entity_operations = \Drupal::service('class_resolver')->getInstanceFromDefinition(EntityOperations::class);
    +  $entity_operations->handleEntityDelete($entity);
    ...
    +  $entity_operations = \Drupal::service('class_resolver')->getInstanceFromDefinition(EntityOperations::class);
    +  $entity_operations->removeUnused();
    

    These can be chained in to a one line.

    i.e.

    \Drupal::service('class_resolver')->getInstanceFromDefinition(EntityOperations::class)
    ->handlePreSave($entity);
    
  4. +++ b/core/modules/layout_builder/layout_builder.module
    @@ -81,3 +83,43 @@ function layout_builder_field_config_delete(FieldConfigInterface $field_config)
    +  if ($consumer !== 'layout_builder') {
    

    Could we just add a simple comment to explain why we do this?

  5. +++ b/core/modules/layout_builder/src/EntityOperations.php
    @@ -0,0 +1,294 @@
    + * @internal
    ...
    +class EntityOperations implements ContainerInjectionInterface {
    

    Is this class even overridable?

  6. +++ b/core/modules/layout_builder/src/EntityOperations.php
    @@ -0,0 +1,294 @@
    +    if ($entity->isNew() || !isset($entity->original) || $sections === NULL || ($entity->getEntityTypeId() !== 'entity_view_display' && empty($sections))) {
    

    Can we make a comment to explain better what we're not targeting here, or break them up to more return conditionals to make it more clear?

  7. +++ b/core/modules/layout_builder/src/EntityOperations.php
    @@ -0,0 +1,294 @@
    +    if ($this->storage && $this->isLayoutCompatibleEntity($entity)) {
    

    Why do we need to check $this->storage?

  8. +++ b/core/modules/layout_builder/src/EntityOperations.php
    @@ -0,0 +1,294 @@
    +   * @internal
    

    I don't think we need @internal here?

  9. +++ b/core/modules/layout_builder/src/EntityOperations.php
    @@ -0,0 +1,294 @@
    +      $sections_field = $entity->get('layout_builder__layout');
    +      return $sections_field->getSections();
    

    Can this be a one-line?

  10. +++ b/core/modules/layout_builder/src/EntityOperations.php
    @@ -0,0 +1,294 @@
    +    if ($sections = $this->getEntitySections($entity)) {
    

    I think we can opt-out early if there are no sections?

  11. +++ b/core/modules/layout_builder/src/EntityOperations.php
    @@ -0,0 +1,294 @@
    +   * @param array $sections
    ...
    +   * @param \Drupal\layout_builder\Section[] $sections
    

    We should probably use same param type hint here.

  12. +++ b/core/modules/layout_builder/src/EntityOperations.php
    @@ -0,0 +1,294 @@
    +      if ($block = $this->getPluginBlockRevision($component->getPlugin())) {
    ...
    +  protected function getPluginBlockRevision(PluginInspectionInterface $plugin) {
    

    Instead of loading all blocks, we could make a entity query instead

jibran’s picture

Status: Needs review » Needs work
+++ b/core/modules/block_content/block_content.install
@@ -138,3 +138,47 @@ function block_content_update_8400() {
+function block_content_update_8601() {

This should be post update hook.

tedbow’s picture

re #146
@johndevman thanks for review!

1. fixed.
2. This was left from when it was generic usage. Changed also change for type. Using "parent entity" in both cases now instead of "entity using".

Maybe we just change this and column names from "parent entity" to "layout entity" since now it is very specific.....

Yes changing to 'layout_entity_id', 'layout_entity_type', and 'block_content_id'. This code started out as generic entity tracking usage table now it is specifically for the Layout Builder use case. Right now you can't use it out of layout builder.

If we decide to do #2979142: Determine if the Inline Block plugin should allow via the Block UI and other module other than Layout Builder then we won't need usage for that case because it will always be a 1-to-1 and we can delete the block_content entity when the block config entity is deleted.
3. I had this as online 'content_moderation' module but then I realized it is much hard to debug because in IDE(phpstorm in my case) cannot this as a "usage" of the methods.

i.e. I am at \Drupal\layout_builder\EntityOperations::handlePreSave and I try "Find usages" nothing will come up.

Do we make code changes for ease of use in IDEs? I don't think this is PHPStorm specific case. I would vote for leaving it.

4. Create follow up #2979142: Determine if the Inline Block plugin should allow via the Block UI and other module other than Layout Builder and @todo.
5. Note sure what the question is? Should we make it "final". Think we almost never do that in core.
6. Add split up into 2 if statements with comments which allows having the 1st if before calling
$this->getEntitySections($entity)
This is hopefully more understandable plus more performant!

7. Because the constructor checks $entityTypeManager->hasDefinition('block_content') setting storage.
We could just check before we call public methods here if 'block_content' module is enabled but I would rather have all the logic in the class if possible.

Maybe we should check if the module is enabled instead.

Added isStorageAvailable() helper function which as doc comment.

8. We don't need it but it is more clear that according to API policy this should internal. Nobody should be extending this.

9. Fixed

10. This ops out of the rest of method. $this->removeUnusedForEntityOnSave($entity); actually should be called even if there are no sections because for a 'entity_view_display' entity you could have no sections but $entity->original might have had them.
11. fixed.
12. Nice catch!. Changing getPluginBlockRevision() to getPluginBlockId() because it is only ever called where we need the ID.

re #147
@jibran. You're right! Fixed in #2976334: Allow Custom blocks to be set as non-reusable adding access restriction based on where it was used. since the current issue is only for layout_builder changes. The combined patch I upload here, which is for testing, though will have the fix.

Will name the patches better.

tedbow’s picture

  1. +++ b/core/modules/layout_builder/src/EntityOperations.php
    @@ -0,0 +1,311 @@
    +  protected function getInlineBlockIdsInSections(array $sections) {
    +    $block_ids = [];
    +    $components = $this->getInlineBlockComponents($sections);
    +    foreach ($components as $component) {
    +      if ($id = $this->getPluginBlockId($component->getPlugin())) {
    

    Thinking more about @johndevman's suggestion to not load is this method which had me change getPluginBlockRevision() to getPluginBlockId().

    Even though we aren't loading the block entity anymore when we don't need to, it is still calling getPluginBlockId() individually for each component. This means if there are 30 components there will be 30 entity queries each with their own alter calls.

    Instead it would be better to first get all the revison_ids from all components and then do 1 query with an IN clause.

    So this made me change the method to:

    protected function getInlineBlockIdsInSections(array $sections) {
        $block_ids = [];
        $components = $this->getInlineBlockComponents($sections);
        $revision_ids = [];
        foreach ($components as $component) {
          $configuration = $component->getPlugin()->getConfiguration();
          if (!empty($configuration['block_revision_id'])) {
            $revision_ids[] = $configuration['block_revision_id'];
          }
        }
        if ($revision_ids) {
          $query = $this->storage->getQuery();
          $query->condition('revision_id', $revision_ids, 'IN');
          $block_ids = $query->execute();
        }
        return $block_ids;
      }
    

    But then....

    I looked at where getInlineBlockIdsInSections() was being called in removeUnusedForEntityOnSave()

    if ($removed_ids = array_diff($this->getInlineBlockIdsInSections($original_sections), $this->getInlineBlockIdsInSections($sections))) {

    This code is finding which block IDs are no longer used and so can be removed(if needed for revisions this code will not be called).

    The problem is that 2 calls to getInlineBlockIdsInSections() will be made even if the revision ids of the original and current are exactly the same. So that would mean 2 entity queries with IN clauses when there might not need to be any.

    So I changed this to:

    // If there are any revisions in the original that aren't current there may
        // some blocks that need to be removed.
        if ($original_revision_ids = array_diff($this->getInBlockRevisionIdsInSection($original_sections), $current_revision_ids)) {
          if ($removed_ids = array_diff($this->getBlockIdsForRevisionIds($original_revision_ids), $this->getBlockIdsForRevisionIds($current_revision_ids))) {
    

    The outer if will only pass if there are revisions in the original that are no longer in the current. Because it is just looking at the components it does not have to do any entity queries for this.

    So in the case where the user only adds or edits inline blocks or adds/edits/deletes other block plugins then we saved ourselves 2 entity queries here.

    The inner if statement with it's 2 calls to getBlockIdsForRevisionIds() still would only makes 2 entity queries.

    I think it is very important to think about performance with the layer builder because this is now more likely to be done with the frequency of content editing vs site building since no configuration is actually being edited here.

  2. +++ b/core/modules/layout_builder/src/EntityOperations.php
    @@ -0,0 +1,311 @@
    +    if ($removed_ids = array_diff($this->getInlineBlockIdsInSections($original_sections), $this->getInlineBlockIdsInSections($sections))) {
    +      $this->deleteBlocksAndUsage($removed_ids);
    +    }
    

    Although I changed the code around this we are still calling deleteBlocksAndUsage() when it is safe to delete block entities on save.

    But in handleEntityDelete() we don't delete the blocks entities but just remove the uses. On cron the actual block entities will be deleted.

    We do this because with hundreds of revisions we might have hundreds of inline blocks so it is not safe to do when just submitting a delete form. Or when deleting 10s of node entities via the content listing page.

    My question is should we only be deleting the usage and not deleting the blocks in removeUnusedForEntityOnSave(). Will there ever be a situation that you are removing so many inline blocks on a save that it is not safe to delete them all.

    Probably it is ok the way it is. But interested if others have use cases where it won't be.

    If it is safe the way it is should we then we also update handleEntityDelete() to delete all block entities not just usage iif the parent entity does not support revisions.

  3. +++ b/core/modules/layout_builder/tests/src/FunctionalJavascript/InlineBlockContentBlockTest.php
    @@ -0,0 +1,540 @@
    +    // Remove block from default.
    +    $this->removeInlineBlockFromLayout();
    +    $this->assertSaveLayout();
    +    $cron->run();
    +    // Ensure the block in the default was deleted.
    +    $this->blockStorage->resetCache([$default_block_id]);
    +    $this->assertEmpty($this->blockStorage->load($default_block_id));
    

    I removed the cron call here because the block entities will actually be removed in removeUnusedForEntityOnSave() which does wait on cron.

    This is because it is non-revisionable entity.

twfahey’s picture

Status: Needs review » Needs work

Have been testing this out, and I am excited to see this new functionality. It is a really nice upgrade for LB!

Minor thing I noticed - Should we add an @todo in the commented out portion of the test:

+++ b/core/modules/layout_builder/tests/src/FunctionalJavascript/InlineBlockContentBlockTest.php
@@ -0,0 +1,539 @@
+    // Remove block from override.
+    // Currently revisions are not actually created so this check will not pass.
+    // @see https://www.drupal.org/node/2937199
+    /*$this->removeInlineBlockFromLayout();
+    $this->assertSaveLayout();
+    $cron->run();
+    // Ensure entity block is not deleted because it is needed in revision.
+    $this->assertNotEmpty($this->blockStorage->load($node_1_block_id));
tedbow’s picture

I uploaded a big change to #2976334-41: Allow Custom blocks to be set as non-reusable adding access restriction based on where it was used. that change from adding a reusable flag to block content entities to adding the ability for blocks to have a parent set.

This allows this issue to get rid of the usage table for content blocks.

Since the blocks know their own parents it is easier find blocks who parents have been deleted.

It is little more complicated because we have to take into account that some entity types are not stored a database table. This is standard for entity_view_display entity type but also for when a site is setup to not store a certain entity type like node in database table.

tedbow’s picture

tedbow’s picture

tim.plunkett’s picture

This is really not a problem at all, but I thought I'd point it out since it's a recent change that is nice to have.

$entity_operations = \Drupal::service('class_resolver')->getInstanceFromDefinition(EntityOperations::class);
$entity_operations = \Drupal::classResolver(EntityOperations::class);

These do the same thing now!

tedbow’s picture

tedbow’s picture

  1. 😱Ok it looks like the changes to #2976334: Allow Custom blocks to be set as non-reusable adding access restriction based on where it was used. to which removed AccessDependentInterface and add ::setParent actually are not going to work with layout builder access wise. At least I can't see how they would.

    I added failing test coverage to prove this

    I added the comment in the code explaining which will copy here:

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

    /* @todo *** block access is broken **
         Because content blocks are storing parent id not revisions then
         the user will always have access to the blocks if they have access to the current parent entity
         even if they can not access to node revision that the block is currently on. In this case the block
         has been removed from the published revision of the node but the user can still
         access the block if they can access the published node.
         ** What to do? **
         We could store the parent_entity_revision_id instead but inline blocks are
         not re-saved unless they have changed. Resaving every time would lead to the
         explosion of revisions the ERR module currently has.
         If an inline block is created and we set parent_revision_id and then the parent
         has 10 more revisions created. When checking access we would always check the
         original revision. The user may not have access to that revision.
         We could conceivablely iterate through all revisions of the parent until
         we find a revision the user has access or until the parent no longer references
         the block revision id but that could be expensive.
         */
        $this->assertFalse($block->access('view', $regular_user));
    

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

    This would also mean this patch would have to be changed. Basically layout_builder would have call setDependent() during rendering process again. This seems to be much surer way to lock down access.

    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().

  2. Updated \Drupal\layout_builder\EntityOperations::removeUnusedForEntityOnSave to not remove inline blocks that are removed from layout if the parent is a revisionable entity. I thought you could remove them if it was not a new revision but I realized that even in that case previous revision may reference the block.
tedbow’s picture

Assigned: Unassigned » tedbow
Status: Needs work » Needs review
FileSize
104.54 KB

Assigning to myself. Need to bring back up-to-date with #2976334-57: Allow Custom blocks to be set as non-reusable adding access restriction based on where it was used. which re #160 switches from using blocks with parents back to the idea of reusable blocks.

#149 was the last patch here that was built update reusable blocks.

I don't think we want to revert back to that 149 because there has been other problems solved since then and at the very least we want to keep the better test coverage.

For quick test though going to test the layout_builder changes from #149 to make sure they still pass now that we have rolled block_content to use reusable blocks. In theory this should be green..

After this runs I will start back making sure we keep all the improvements since added between #149 and #160 if they still apply.

So it should go back to "needs work" after this test run regardless of whether the patch is green.

tedbow’s picture

Ok here the further improves after #149 that still apply after switching back to isReusable().

tedbow’s picture

@Berdir mentioned in #2976334-54: Allow Custom blocks to be set as non-reusable adding access restriction based on where it was used. that we should be testing for private files on blocks

I have added test for access to private files on inline blocks in this patch.

Initially private files on inline blocks would always return access denied because \Drupal\file\FileAccessControlHandler::checkAccess checks if the user has access at least 1 entity where the file is used.

Because non-reusable blocks need to have ::setDependency() called for them to return access allowed then private file check on the inline block would always return access denied.

So basically I needed to figure out a way that if $block->access() is called before ::setDependency() is called we automatically set the dependency to a revision of the layout node where the block entity is used.

I am using an implementation of hook_ENTITY_TYPE_acccess to do this. Basically this hook is called before access handler is called so we can check if the access dependency has been set and set if it is not.

Although this solution works there 2 concerns I would have with it:

  1. It relies on the internal logic of \Drupal\Core\Entity\EntityAccessControlHandler::access in that the hook must be invoked before handler itself checks access. But maybe that is logical assumption. Also that way it would break is to deny access when it maybe should have been allow not to give access when it maybe should have been denied.
  2. It is using hook_ENTITY_TYPE_acccess is way it may not have been attended for. Basically it is using as a chance to alter the entity before access is checked.

The other options I see:

  1. always set the dependency on hook_ENTITY_TYPE_load() but then we would be setting dependency on load when most of the time for inline blocks we will set it explicitly with the revision we want to check. File usage does not use revisions 😢
  2. In \Drupal\Core\Access\AccessDependentTrait::getAccessDependency() if the access dependency fire an event that would allow layout builder(or other modules using non-reusable blocks) to set the dependency.
tedbow’s picture

tedbow’s picture

Ok, this patch removes the use of hook_ENTITY_TYPE_acccess to ensure that there is an access dependency set.

I updated \Drupal\block_content\BlockContentAccessControlHandler::checkAccess to fire a new BlockContentGetDependencyEvent event if no dependency has been set on non-reusable block when $block->access() has been called. I did this instead of inside \Drupal\Core\Access\AccessDependentTrait::getAccessDependency() because if $block->getAccessDependency() was called inside one of the subscribers reacting to the event then this would cause recursion. A subscriber might call $block->getAccessDependency() to determine if a previously called subscriber had already set the dependency.

This addresses the 2 concerns I mentioned in #164 about using hook_ENTITY_TYPE_acccess.

I am just about upload patch to #2976334: Allow Custom blocks to be set as non-reusable adding access restriction based on where it was used. with the block_content module changes only.

tedbow’s picture

The fail in #167 was random test failure that I have been trying to figure out. I think I have got it now.

I have added wait to this patch that waits for the file link to show up on the file widget after uploading the file. This will ensure that the test doesn't try to close the dialog before the file properly uploaded.

tedbow’s picture

Assigned: tedbow » Unassigned

Unassigning myself, will continue to work on this but also looking for reviews

sjerdo’s picture

Issue tags: +DevDaysLisbon

Some review comments:

  1. +++ b/core/modules/layout_builder/layout_builder.install
    @@ -38,3 +40,75 @@ function layout_builder_install() {
    +        'length' => EntityTypeInterface::ID_MAX_LENGTH,
    
    +++ b/core/modules/layout_builder/layout_builder.module
    @@ -81,3 +83,51 @@ function layout_builder_field_config_delete(FieldConfigInterface $field_config)
    +  // @todo Determine the 'inline_block_content' blocks should be allowe outside
    +  //   of layout_builder https://www.drupal.org/node/2979142.
    

    typo: allowe => allowed

  2. +++ b/core/modules/layout_builder/src/EventSubscriber/SetInlineBlockDependency.php
    @@ -0,0 +1,174 @@
    +  /**
    +   * The database connection.
    +   *
    +   * @var \Drupal\Core\Database\Connection
    +   */
    +  protected $database;
    

    There seems to be some inconsistency in naming the Database Connection variable. In class SetInlineBlockDependency its named $database, in class InlineBlockContentUsage its named $connection. Current usage in core: 27x $database, 63x $connection. I suggest we use $connection as variable name.

  3. +++ b/core/modules/layout_builder/src/EventSubscriber/SetInlineBlockDependency.php
    @@ -0,0 +1,174 @@
    +  public function __construct(EntityTypeManagerInterface $entity_type_manager, Connection $database, InlineBlockContentUsage $usage) {
    +    $this->entityTypeManager = $entity_type_manager;
    +    $this->database = $database;
    +    $this->usage = $usage;
    +
    +  }
    

    Unnecessary empty line

  4. +++ b/core/modules/layout_builder/src/EventSubscriber/SetInlineBlockDependency.php
    @@ -0,0 +1,174 @@
    +  public static function create(ContainerInterface $container) {
    +    return new static(
    +      $container->get('entity_type.manager'),
    +      $container->get('database'),
    +      $container->get('inline_block_content.usage')
    +
    +    );
    +  }
    

    Unnecessary empty line

  5. +++ b/core/modules/layout_builder/src/EventSubscriber/SetInlineBlockDependency.php
    @@ -0,0 +1,174 @@
    +  public function setLayoutDependency(BlockContentInterface $block_content) {
    

    This method is too complex. You could add early returns to remove the deep nesting. Also the code wrapped in the else-block is unnecessary wrapped in a block (since the if block contains return;)

  6. +++ b/core/modules/layout_builder/src/Form/RevertOverridesForm.php
    @@ -103,6 +103,9 @@ public function buildForm(array $form, FormStateInterface $form_state, SectionSt
    +    // @todo Remove this quick fix after https://www.drupal.org/node/2970801
    +    $this->sectionStorage = \Drupal::service('plugin.manager.layout_builder.section_storage')->loadFromStorageId($this->sectionStorage->getStorageType(), $this->sectionStorage->getStorageId());
    

    I guess this 'quick fix' should be fixed in the correct way before committing this patch.

  7. +++ b/core/modules/layout_builder/src/Plugin/Block/InlineBlockContentBlock.php
    @@ -0,0 +1,287 @@
    +    $complete_form_state = ($form_state instanceof SubformStateInterface) ? $form_state->getCompleteFormState() : $form_state;
    

    Unnecessary parenthesis around $form_state instanceof SubformStateInterface

tedbow’s picture

@sjerdo Thanks for the review! Hope Dev Days is going well!

1. fixed
2 Good catch. I actually changed to $database because I checked other uses and it seems more recent modules are using this in core. Also it is more descriptive. But all the same now.
3. fixed
4. fixed
5. fixed
6. Will see what I can do to get #2970801: If you add block then try to Revert the layout it doesn't revert fixed. leaving comment for now.

UPDATE: I cleaned up the patch in #2970801: If you add block then try to Revert the layout it doesn't revert and I now think it is correct fix. Be great to get reviews over there.
7. fixed.

I also changed InlineBlockTestBase to extend WebDriverTestBase now that #2942900: Convert JavascriptTestBase Tests to use DrupalSelenium2Driver has been fixed! 🎉. I tested locally and seems to still work.
I had to remove calls to $assert_session->statusCodeEquals(403); in InlineBlockPrivateFilesTest because this isn't supported by the driver to
$assert_session->pageTextContains('You are not authorized to access this page');

I think this fine because we are also testing that the contents of the file is not output.

tedbow’s picture

This patch does a couple things:

  1. The test failure in #172 is a problem the test is trying to click on link during an animation so it doesn't work. I think this the random failure that has been happening before but the new webdriver for testing has an actual error message 🎉

    So this patch adds new test module that removes css animation for testing like we did in #2782915: Standardize the behavior of links when Outside In editing mode is enabled. I added a @todo to #2901792: Disable all animations in Javascript testing so we can just have 1 test module for all other modules or just remove animations for all JS testing.

  2. It also updates the @todo comment to #2970801: If you add block then try to Revert the layout it doesn't revert. I think we should be able to continue with issue and fix the problem in the other issue.
tedbow’s picture

Issue summary: View changes
Status: Needs work » Needs review
FileSize
8.36 KB
81.5 KB
132.53 KB

This patch

  1. In \Drupal\Tests\layout_builder\FunctionalJavascript\InlineBlockTestBase::assertSaveLayout() change to use drupalGet() save the layout. Using clickLink() causes random test failures. I explored many options to fix this(I mostly did this in scratch issue #2977515: [ignore] Test Package manager core merge). I think this solution is fine but create a follow up #2984161: Convert Layout Builder Javascript tests to NightWatch because hopefully NightWatch won't have these random failures.
  2. Rename the class EntityOperations to InlineBlockEntityOperations and updated the comment. I think this is clearer and avoids any unrelated operations need for Layout Builder going into this class in the future.
  3. Made sure \Drupal\layout_builder\InlineBlockEntityOperations::removeUnusedForEntityOnSave() actually removes the usage rows in the case it deletes content block entities on save(for non-revisionable entities). Also added test coverage for this change.

Update summary to not detail the API changes that are actually happening in #2976334: Allow Custom blocks to be set as non-reusable adding access restriction based on where it was used. but rather to just reference that issue.

tedbow’s picture

With the access changes introduced in #167 it means you can now call $block_content->access() and if the block doesn't have access dependency set then the dependency will be set automatically to a revision of the entity that using the block in a layout.

\Drupal\block_content\BlockContentAccessControlHandler::checkAccess() now calls $block_content->getAccessDependency()->access($operation, $account, TRUE);. This means the access is controlled by access dependency which as the name suggests is good thing.

But for Layout Builder this doesn't actually fit our use case. Only "view" access should actually be dependent on the "view" access to the dependency. For update and delete those should controlled by layout builder but effectively they should only be granted if you have access to configure any layout permission.

Basically even if you can edit a entity you don't have access to the configure the layout for that entity.

I have added an implementation of hook_ENTITY_TYPE_access to ensure this. I also added test coverage.

tedbow’s picture

Status: Needs work » Needs review

Changing to need review and retesting. Not sure how the failure in FormErrorHandlerQuickEditTest could be related

tedbow’s picture

EclipseGc’s picture

Status: Needs review » Needs work
  1. +++ b/core/modules/layout_builder/src/EventSubscriber/SetInlineBlockDependency.php
    @@ -0,0 +1,178 @@
    +class SetInlineBlockDependency implements EventSubscriberInterface, ContainerInjectionInterface {
    

    Event subscribers are always documented as a service anyway, so you should be able to inject their dependencies w/o using container injection interface.

  2. +++ b/core/modules/layout_builder/src/EventSubscriber/SetInlineBlockDependency.php
    @@ -0,0 +1,178 @@
    +  public function setLayoutDependency(BlockContentInterface $block_content) {
    

    is this suppose to be public?

  3. +++ b/core/modules/layout_builder/src/InlineBlockEntityOperations.php
    @@ -0,0 +1,260 @@
    +    $current_revision_ids = $this->getInBlockRevisionIdsInSection($sections);
    +    // If there are any revisions in the original that aren't current there may
    +    // some blocks that need to be removed.
    +    if ($original_revision_ids = array_diff($this->getInBlockRevisionIdsInSection($original_sections), $current_revision_ids)) {
    +      if ($removed_ids = array_diff($this->getBlockIdsForRevisionIds($original_revision_ids), $this->getBlockIdsForRevisionIds($current_revision_ids))) {
    +        $this->deleteBlocksAndUsage($removed_ids);
    +      }
    +    }
    

    getInBlock??? These methods and variables all have the word "revision" in them, but you're bailing on revisionable entities earlier, so per our discussion, these need renaming because this is about entities that aren't revisionable.

  4. +++ b/core/modules/layout_builder/src/InlineBlockEntityOperations.php
    @@ -0,0 +1,260 @@
    +            // @todo Is there a better way to tell if Layout Override is new?
    +            // what if is overridden and all sections removed. Currently if you
    +            // remove all sections from an override it reverts to the default.
    +            // Is that a feature or a bug?
    

    That's a feature. :-D

  5. +++ b/core/modules/layout_builder/src/InlineBlockEntityOperations.php
    @@ -0,0 +1,260 @@
    +        $post_save_configuration = $plugin->getConfiguration();
    ...
    +        $component->setConfiguration($plugin->getConfiguration());
    

    Seems like we can just use this variable w/o recalling the method.

  6. +++ b/core/modules/layout_builder/src/InlineBlockEntityOperations.php
    @@ -0,0 +1,260 @@
    +  protected function getPluginBlockId(PluginInspectionInterface $plugin) {
    +    /** @var \Drupal\Component\Plugin\ConfigurablePluginInterface $plugin */
    +    $configuration = $plugin->getConfiguration();
    +    if (!empty($configuration['block_revision_id'])) {
    

    I know this is all internal and you probably have other error checking at some point, but you're not actually looking at only PluginInspectionInterface objects here because they don't do what you need. You're looking at something more specific, and I'd be way more comfortable if there was a check for that.

  7. +++ b/core/modules/layout_builder/src/LayoutEntityHelperTrait.php
    @@ -0,0 +1,92 @@
    +    return ($entity->getEntityTypeId() === 'entity_view_display' && $entity instanceof LayoutBuilderEntityViewDisplay) ||
    

    Tim should confirm if this is future proof wrt his opt in/out patch.

  8. +++ b/core/modules/layout_builder/src/Plugin/Block/InlineBlockContentBlock.php
    @@ -0,0 +1,287 @@
    +    if ($this->blockContent instanceof DependentAccessInterface && $dependencies = $this->getAccessDependencies()) {
    +      $this->blockContent->setAccessDependencies($dependencies);
    +    }
    

    Shouldn't this be inside the previous "if" statement? Otherwise we do it on every getEntity() call.

  9. +++ b/core/modules/layout_builder/src/Plugin/Block/InlineBlockContentBlock.php
    @@ -0,0 +1,287 @@
    +    if ($duplicate_block && !empty($this->configuration['block_revision_id']) && empty($this->configuration['block_serialized'])) {
    +      $entity = $this->entityTypeManager->getStorage('block_content')->loadRevision($this->configuration['block_revision_id']);
    +      $block = $entity->createDuplicate();
    +    }
    +    elseif (isset($this->configuration['block_serialized'])) {
    +      $block = unserialize($this->configuration['block_serialized']);
    +      if (!empty($this->configuration['block_revision_id']) && $duplicate_block) {
    +        $block = $block->createDuplicate();
    +      }
    +    }
    

    This is confusing to read because both situations are dealing with duplicate_block == TRUE && !empty($this->configuration['block_revision_id'], but they state it in really different ways when the only difference is whether or not empty($this->configuration['block_serialized']. If we could just nest an if statement instead of the if/elseif, I think it'd be way more readable.

I really like the direction of this patch and feel like it's pretty close to being where it needs to be. Honestly, most of my issues here are pretty nit-picky. I'd like to see them all solved, but there's nothing in here that's obviously broken in any way. Let's fix the bits and get this in.

Eclipse

Tim Bozeman’s picture

Issue summary: View changes
tedbow’s picture

@EclipseGc thanks for the review!

I have changed the DependentAccessInterface back to having only 1 dependency see changes in #2976334-70: Allow Custom blocks to be set as non-reusable adding access restriction based on where it was used.
1. fixed
2. Nope, fixed
3. In our discussion I forgot that we are bailing on revisionable layout entities. This revision_ids are actually the revision_ids of the block_content entities used in the layout. Leaving.
4. Thanks for the clarification removing the todo and updating the comment.
5. fixed.
6. Changed to use InlineBlockContentBlock as the parameter type.
7. ok
8. fixed
9. yep that is confusing. thanks for pointing this out.
I have simplified it this. I think it more readable now.

AaronMcHale’s picture

Hi all

I've been loosely following this issue for a while, just read the summary to familiarise myself with it again, and wanted to raise a couple of things:

Firstly, since these non-reusable blocks will only be accessible in the context of the Entity they were created in, does a side effect of this issue mean that once the patch is applied any block (including regular reusable custom blocks) will be editable from directly within the Layout Builder?

In using LB for a project this is something that could be useful if a Block is shared between a few Entities, where navigating to the Custom Block Library and having to find the block among lots of Blocks can be rather time consuming and annoying.

Secondly, as mentioned I've been using Layout Builder on a project and we have built up quite a collection of Custom Blocks, has anyone considered a migration path for existing Custom Blocks to allow site builders to easily migrate them to these new non-reusable Custom Blocks?

Apologies if these have already been discussed or covered in the past, as you can appreciate there are so many comments on this issue it would be hard to find what I'm looking for, and I also don't have enough time to test the patch right now so thought I'd just bring up these points in the event it hasn't been covered in the patch.

Thanks
-Aaron

tedbow’s picture

@AaronMcHale thanks for taking a look.

does a side effect of this issue mean that once the patch is applied any block (including regular reusable custom blocks) will be editable from directly within the Layout Builder?

Nope this will not effect reusable blocks which is all current blocks. The inline blocks and the existing custom blocks are 2 different plugins. It maybe useful to be able to be able to edit reusable blocks in there block plugin configuration form but that problem exists outside of layout builder as this would also be useful in the regular block UI. I believe 1 reason it isn't done is because it is hard to get across to users that they edit something that could effect on other parts of the site.

I believe that should be separate issue for Custom Block module. If the Custom Block module changed the configuration form to be able to edit the blocks this would take effect in both the layout builder and block UI regardless of this patch.

has anyone considered a migration path for existing Custom Blocks to allow site builders to easily migrate them to these new non-reusable Custom Blocks?

This would be a separate issue and I think handled better by contrib. Of course if you did this all the blocks that were the same entity would after migration all be separate entities.

tedbow’s picture

Bring up-to-date with #2976334-87: Allow Custom blocks to be set as non-reusable adding access restriction based on where it was used.

The big change for layout_builder is in \Drupal\layout_builder\EventSubscriber\BlockComponentRenderArray::onBuildRender() if we know we are know we are preview in the defaults layout builder instead passing the entity itself(which will be a sample entity) on as the dependency we pass a accessible object that will pass on view but fail on everything else.

This eliminates the need to put special logic in \Drupal\block_content\BlockContentAccessControlHandler::checkAccess to allow access on preview.

tedbow’s picture

Self review, while waiting for other reviews

  1. +++ b/core/modules/layout_builder/src/Access/LayoutPreviewAccessAllowed.php
    @@ -0,0 +1,25 @@
    +    if ($operation === 'view' && $return_as_object) {
    +      return AccessResult::allowed();
    +    }
    +    // If expected arguments return forbidden access.
    +    return AccessResult::forbidden();
    

    This assumes that $return_as_object will always be called as TRUE in block access handler. Although this is true with update block access handler in #2976334: Allow Custom blocks to be set as non-reusable adding access restriction based on where it was used. we shouldn't assume that.
    Fixed

  2. +++ b/core/modules/layout_builder/src/EventSubscriber/SetInlineBlockDependency.php
    @@ -0,0 +1,162 @@
    +      if (!$layout_entity->getEntityType()->isRevisionable()) {
    +        return $layout_entity;
    +      }
    

    Which should check here that actually block revision id is used in a component. Some other module may have made a new revision of this block entity.

    Fixed

tim.plunkett’s picture

Priority: Normal » Major

This is huge for site builders.

tedbow’s picture

whoops didn't upload patch for #187 so this includes those changes also

  1. Bring up to date with #2976334-91: Allow Custom blocks to be set as non-reusable adding access restriction based on where it was used.(now RTBC🎉)
  2. Refactored InlineBlockEntityOperations some more to make the methods smaller after reviewing with @phenaproxima
johnwebdev’s picture

This patch starts to look really good! Most nitpicks here.

  1. +++ b/core/modules/layout_builder/layout_builder.install
    @@ -38,3 +40,75 @@ function layout_builder_install() {
    +    'description' => 'Track where a block_content entity is used.',
    ...
    +    'description' => 'Track where a entity is used.',
    

    The descriptions does not match.

  2. +++ b/core/modules/layout_builder/layout_builder.module
    @@ -81,3 +85,69 @@ function layout_builder_field_config_delete(FieldConfigInterface $field_config)
    +    return AccessResult::Neutral();
    

    This should be AccessResult::neutral()?

  3. +++ b/core/modules/layout_builder/src/EventSubscriber/BlockComponentRenderArray.php
    @@ -56,6 +58,25 @@ public function onBuildRender(SectionComponentBuildRenderArrayEvent $event) {
    +          if ($event->inPreview() && !empty($entity->in_preview)) {
    ...
    +            $block->setAccessDependency(new LayoutPreviewAccessAllowed());
    

    Do we have test coverage for this?

  4. +++ b/core/modules/layout_builder/src/EventSubscriber/SetInlineBlockDependency.php
    @@ -0,0 +1,193 @@
    +   * entity will returned.
    

    Nit: entity will BE returned

  5. +++ b/core/modules/layout_builder/src/EventSubscriber/SetInlineBlockDependency.php
    @@ -0,0 +1,193 @@
    +   *   Returns the layout dependency
    

    Nit missing a .

  6. +++ b/core/modules/layout_builder/src/EventSubscriber/SetInlineBlockDependency.php
    @@ -0,0 +1,193 @@
    +    if (in_array($block_content->getRevisionId(), $sections_blocks_revision_ids)) {
    +      return TRUE;
    +    }
    +    return FALSE;
    

    This could be shorten down to one line.

  7. +++ b/core/modules/layout_builder/src/EventSubscriber/SetInlineBlockDependency.php
    @@ -0,0 +1,193 @@
    +  protected function getEntityRevisionIds(EntityInterface $entity) {
    

    Nice with a todo for this..

  8. +++ b/core/modules/layout_builder/src/Form/RevertOverridesForm.php
    @@ -103,6 +103,10 @@ public function buildForm(array $form, FormStateInterface $form_state, SectionSt
    +    // @todo Remove after https://www.drupal.org/node/2970801.
    +    $this->sectionStorage = \Drupal::service('plugin.manager.layout_builder.section_storage')->loadFromStorageId($this->sectionStorage->getStorageType(), $this->sectionStorage->getStorageId());
    

    we should really try to get this committed

  9. +++ b/core/modules/layout_builder/src/InlineBlockContentUsage.php
    @@ -0,0 +1,113 @@
    + * Service class to track non-reusable Blocks entities usage.
    ...
    +   * Gets unused inline block content IDs.
    ...
    +   * Delete the content blocks and delete the usage records.
    

    Hmm, we are using three different terms for describing this concept. Can we try to decide for one instead? This kinda applies to the whole patch.

  10. +++ b/core/modules/layout_builder/src/InlineBlockContentUsage.php
    @@ -0,0 +1,113 @@
    +    return $query->execute()->fetch();
    

    We should use fetchObject or fetchAssoc() instead, as per recommendation in the Drupal docs (https://www.drupal.org/docs/8/api/database-api/result-sets) also, I'm not sure if Drupal changes this, but I think you can change the default fetch mode which would break stuff.

  11. +++ b/core/modules/layout_builder/src/InlineBlockEntityOperations.php
    @@ -0,0 +1,304 @@
    +  protected function removeUnusedForEntityOnSave(EntityInterface $entity) {
    

    Really like how this method has changed! It's much more clear what's going on.

  12. +++ b/core/modules/layout_builder/src/LayoutBuilderServiceProvider.php
    @@ -0,0 +1,35 @@
    +class LayoutBuilderServiceProvider implements ServiceProviderInterface {
    

    Can we add a comment to explain what this really does?

tedbow’s picture

@johndevman thanks for the review again!
1. fixed
2. fixed
3. The JS test cover this because otherwise the inline blocks would not render in defaults.
but I have also updated BlockComponentRenderArrayTest to cover blocks that implement RefinableDependentAccessInterface
4. fixed
5. fixed
6. fixed
7. 👍
8. yep
9. Good point.
Changing to use "inline blocks" whenever possible.
I still think it in some places it makes to use "block content" like parameters for\Drupal\layout_builder\InlineBlockContentUsage::deleteUsage and other parameters where we want to clear the ID or the entity itself's type.
10. Changed to fetchObject()
11. Great!
12. updated the comment to reflect the service must be provided dynamically because it is dependent on another module.

+++ b/core/modules/layout_builder/src/EventSubscriber/BlockComponentRenderArray.php
@@ -56,6 +58,25 @@ public function onBuildRender(SectionComponentBuildRenderArrayEvent $event) {
+          $event->getContexts();

This call is not needed

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.

tim.plunkett’s picture

Issue tags: +Blocks-Layouts
johnzzon’s picture

Super-grammar-nazi-nitpicks, sorry 🙈:

  1. +++ b/core/modules/block_content/src/Access/RefinableDependentAccessInterface.php
    @@ -0,0 +1,48 @@
    +   * If there is an existing dependency and it is a instance of AccessGroupAnd
    

    an instance

  2. +++ b/core/modules/layout_builder/src/EventSubscriber/SetInlineBlockDependency.php
    @@ -0,0 +1,190 @@
    +   * Get the access dependency of a inline block.
    

    an inline block

  3. +++ b/core/modules/layout_builder/src/InlineBlockEntityOperations.php
    @@ -0,0 +1,304 @@
    +   * Gets a block ID for a inline block plugin.
    

    an inline block

  4. +++ b/core/modules/layout_builder/src/InlineBlockEntityOperations.php
    @@ -0,0 +1,304 @@
    +   *   The section component with a inline block.
    

    an inline block

  5. +++ b/core/modules/layout_builder/tests/src/FunctionalJavascript/InlineBlockContentBlockTest.php
    @@ -0,0 +1,422 @@
    +    // Add a entity block.
    

    an entity block.
    Also, should this be "an inline block"?

tedbow’s picture

@johnzzon thanks for the review! No worries, grammar is important.
1. This is actually in the block_content so this would be in #2976334: Allow Custom blocks to be set as non-reusable adding access restriction based on where it was used.
2. Fixed
3. Fixed
4. fixed
5. yep change to "an inline block"

Also made a @todo for #2976334: Allow Custom blocks to be set as non-reusable adding access restriction based on where it was used.

tedbow’s picture

Just a re-roll

tedbow’s picture

self review after a few days. Saw a couple things to simplify

  1. +++ b/core/modules/layout_builder/tests/src/Unit/BlockComponentRenderArrayTest.php
    @@ -33,6 +38,16 @@ class BlockComponentRenderArrayTest extends UnitTestCase {
    +  public function providerOnBuildRender() {
    

    Renaming to providerBlockTypes()

  2. +++ b/core/modules/layout_builder/src/EventSubscriber/SetInlineBlockDependency.php
    @@ -0,0 +1,190 @@
    + * A known example of when the access dependency will not have been is when
    

    s/have been is/have been set is/

  3. +++ b/core/modules/layout_builder/src/InlineBlockContentUsage.php
    @@ -0,0 +1,113 @@
    +   * Add a usage record.
    

    Changing to "Adds"

  4. +++ b/core/modules/layout_builder/src/InlineBlockContentUsage.php
    @@ -0,0 +1,113 @@
    +  public function addUsage($block_content_id, $layout_entity_type, $layout_entity_id) {
    

    Changing to accept an Entity. This method is only called 1 place and we have the entity.

  5. +++ b/core/modules/layout_builder/src/InlineBlockEntityOperations.php
    @@ -0,0 +1,304 @@
    +  protected $storage;
    

    Changing to $blockContentStorage to be clear.

  6. +++ b/core/modules/layout_builder/src/InlineBlockEntityOperations.php
    @@ -0,0 +1,304 @@
    +    if ($entity instanceof RevisionableInterface) {
    +      return;
    +    }
    

    Move this check into the first if statement of the this methods because we don't need to call getEntitySections() if we already know this is revisionable entity.

  7. +++ b/core/modules/layout_builder/src/InlineBlockEntityOperations.php
    @@ -0,0 +1,304 @@
    +      $original_block_content_ids = $this->getBlockIdsForRevisionIds($unused_original_revision_ids);
    

    Renaming to $unused_original_block_content_ids to be clear.

  8. +++ b/core/modules/layout_builder/src/InlineBlockEntityOperations.php
    @@ -0,0 +1,304 @@
    +          /** @var \Drupal\layout_builder\Field\LayoutSectionItemList $original_sections_field */
    +          $original_sections_field = $entity->original->get('layout_builder__layout');
    +          if ($original_sections_field->isEmpty()) {
    

    Changing to

    if (empty($this->getEntitySections($entity->original))) 
    

    To have 1 less point where we have to know this implementation details.

  9. +++ b/core/modules/layout_builder/src/InlineBlockEntityOperations.php
    @@ -0,0 +1,304 @@
    +        /** @var \Drupal\layout_builder\Plugin\Block\InlineBlockContentBlock $plugin */
    +        $plugin = $component->getPlugin();
    +        $pre_save_configuration = $plugin->getConfiguration();
    +        $plugin->saveBlockContent($new_revision, $duplicate_blocks);
    +        $post_save_configuration = $plugin->getConfiguration();
    +        if ($duplicate_blocks || (empty($pre_save_configuration['block_revision_id']) && !empty($post_save_configuration['block_revision_id']))) {
    +          $this->usage->addUsage($this->getPluginBlockId($plugin), $entity->getEntityTypeId(), $entity->id());
    +        }
    +        $component->setConfiguration($post_save_configuration);
    

    in a previous refactoring I added saveInlineBlockComponent but somehow didn't replace this block with a call to the new method. Fixed.

  10. +++ b/core/modules/layout_builder/src/InlineBlockEntityOperations.php
    @@ -0,0 +1,304 @@
    +      $query = $this->storage->getQuery();
    +      $query->condition('revision_id', $configuration['block_revision_id']);
    +      return array_values($query->execute())[0];
    

    This can be replaced with
    return array_pop($this->getBlockIdsForRevisionIds([$configuration['block_revision_id']]));
    Since we already would have getBlockIdsForRevisionIds().

  11. +++ b/core/modules/layout_builder/src/LayoutEntityHelperTrait.php
    @@ -0,0 +1,109 @@
    +    $inline_components = [];
    

    Changing to $inline_block_components to be more clear

  12. +++ b/core/modules/layout_builder/src/LayoutEntityHelperTrait.php
    @@ -0,0 +1,109 @@
    +      $components = $section->getComponents();
    +
    +      foreach ($components as $component) {
    

    Simplifying to
    foreach ($section->getComponents() as $component) {

tedbow’s picture

Status: Needs work » Needs review
FileSize
1.82 KB
94.58 KB

Sorry I should run tests again right before creating patch.

tedbow’s picture

Re-roll after #2936358: Layout Builder should be opt-in per display (entity type/bundle/view mode)

Update: forgot that #2936358 actually means we have to update tests. Updating

tedbow’s picture

Status: Needs work » Needs review
FileSize
8.91 KB
94.96 KB

Updated the tests.

+++ b/core/modules/layout_builder/tests/src/FunctionalJavascript/InlineBlockContentBlockTest.php
@@ -0,0 +1,422 @@
+    $field_ui_prefix = 'admin/structure/types/manage/bundle_with_section_field';

This was being set 6 places across 2 file so changed this a constant in the parent class.

tim.plunkett’s picture

The test coverage looks great! This patch is really close.

  1. +++ b/core/modules/layout_builder/src/EventSubscriber/BlockComponentRenderArray.php
    @@ -56,6 +58,24 @@ public function onBuildRender(SectionComponentBuildRenderArrayEvent $event) {
    +          if ($event->inPreview() && !empty($entity->in_preview)) {
    ...
         if ($event->inPreview()) {
    

    I don't think we need the other $entity->in_preview, the $event->inPreview() should cover it, yeah?

  2. +++ b/core/modules/layout_builder/src/EventSubscriber/SetInlineBlockDependency.php
    @@ -0,0 +1,190 @@
    +    if ($revision_table = $entity_type->getRevisionTable()) {
    +      $query = $this->database->select($revision_table);
    +      $query->condition($entity_type->getKey('id'), $entity->id());
    +      $query->fields($revision_table, [$entity_type->getKey('revision')]);
    +      $query->orderBy($entity_type->getKey('revision'), 'DESC');
    +      return $query->execute()->fetchCol();
    

    Is there no direct API for this? It seems weird to use the DB directly.

  3. +++ b/core/modules/layout_builder/src/InlineBlockContentUsage.php
    @@ -0,0 +1,111 @@
    +class InlineBlockContentUsage {
    

    This (and probably a lot of others) should be @internal.

  4. +++ b/core/modules/layout_builder/src/InlineBlockEntityOperations.php
    @@ -0,0 +1,293 @@
    +  protected $database;
    ...
    +   * @param \Drupal\Core\Database\Connection $database
    +   *   The database connection.
    ...
    +    $this->database = $database;
    

    Doesn't seem to be used in this file

  5. +++ b/core/modules/layout_builder/src/InlineBlockEntityOperations.php
    @@ -0,0 +1,293 @@
    +   * @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
    +   * @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
    ...
    +   * @throws \Drupal\Core\Entity\EntityStorageException
    ...
    +   * @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
    +   * @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
    +   * @throws \Drupal\Core\Entity\EntityStorageException
    +   * @throws \Exception
    ...
    +   * @throws \Drupal\Core\Entity\EntityStorageException
    ...
    +   * @throws \Drupal\Core\Entity\EntityStorageException
    ...
    +   * @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
    +   * @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
    +   * @throws \Drupal\Core\Entity\EntityStorageException
    +   * @throws \Exception
    

    Idk that we usually have all these @throws for a non-API method, especially when they're not doing the throwing themselves. And when they are thrown directly, the @throws should explain when they are thrown. In this case I'd probably just drop these

  6. +++ b/core/modules/layout_builder/src/InlineBlockEntityOperations.php
    @@ -0,0 +1,293 @@
    +        // @todo Currently revisions are never created for the parent entity.
    +        // This will be fixed in https://www.drupal.org/node/2937199.
    +        // To work around this always make a revision when the parent entity is
    +        // an instance of RevisionableInterface. After the issue is fixed only
    +        // create a new revision if '$entity->isNewRevision()'.
    

    Nit, the parts of the comment that are part of the @todo should be indented a further two spaces

  7. +++ b/core/modules/layout_builder/src/LayoutEntityHelperTrait.php
    @@ -0,0 +1,107 @@
    +    return ($entity->getEntityTypeId() === 'entity_view_display' && $entity instanceof LayoutBuilderEntityViewDisplay) ||
    ...
    +    if ($entity->getEntityTypeId() === 'entity_view_display' && $entity instanceof LayoutBuilderEntityViewDisplay) {
    

    The entity type ID check should be redundant with the instanceof check.

    Furthermore, the instanceof should check for LayoutEntityDisplayInterface not the class itself

  8. +++ b/core/modules/layout_builder/src/LayoutEntityHelperTrait.php
    @@ -0,0 +1,107 @@
    +        $plugin = $component->getPlugin();
    +        if ($plugin instanceof InlineBlockContentBlock) {
    

    This should use the block plugin ID, not the class

  9. +++ b/core/modules/layout_builder/src/Plugin/Block/InlineBlockContentBlock.php
    @@ -0,0 +1,287 @@
    +   * @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
    +   * @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
    +   * @throws \Drupal\Core\Entity\EntityStorageException
    

    More with these @throws

tedbow’s picture

@tim.plunkett thanks for the review!!!!

1. Yeah that sounds right. fixed
2. There is not as far as I can tell. In the method description I pointed to this issue I created: #2986027: Move NodeStorageInterface::revisionIds on up to RevisionableStorageInterface.
Only NodeStorageInterface has this method but it should be able to added to RevisionableStorageInterface
3. Marking all new classes as internal in this. I don't see of any that should not be. Also not adding the message "Layout Builder is currently experimental and should only be leveraged....." because these classes should still be internal when the module is stable.
4. Sure it is. It is set in the constructor 😜. Just kidding fixed.
5. Fixed.
6. fixed
7. only checking for interface now and not the entity type id.
8. Ok removing the assignment and just checking $component->getPlugin()->getPluginId() === 'inline_block_content'
9. fixed. Also removed all @throws from the tests(I knew I could make this patch smaller 😜)

tim.plunkett’s picture

Thanks!

Two more tiny bits and this is good to go

  1. +++ b/core/modules/layout_builder/src/LayoutEntityHelperTrait.php
    @@ -0,0 +1,108 @@
    +        if ($component->getPlugin()->getPluginId() === 'inline_block_content') {
    

    This can just be if ($component->getPluginId() === 'inline_block_content') {

    No need to instantiate the plugin

  2. +++ b/core/modules/layout_builder/src/LayoutEntityHelperTrait.php
    @@ -22,7 +24,7 @@
    +    return ($entity instanceof LayoutEntityDisplayInterface) ||
           ($this->isEntityUsingFieldOverride($entity));
    

    No need now for the extra parentheses or the newline

tedbow’s picture

tim.plunkett’s picture

Status: Needs review » Reviewed & tested by the community

Great! Thanks @tedbow for the incredible effort on this issue.

tedbow’s picture

+++ b/core/modules/layout_builder/src/LayoutEntityHelperTrait.php
@@ -0,0 +1,107 @@
+        if ($component->getPluginId() === 'inline_block_content') {

Whoops! we need to be checking the base id here.
Fixing

also fixing the 2 codesniffer_fixes.patchissues from #208

tim.plunkett’s picture

Status: Needs review » Reviewed & tested by the community

Ahh, totally makes sense. Thanks!

tedbow’s picture

Issue tags: +beta target

Since Layout Builder is experimental this is still possible for 8.6 beta

alexpott’s picture

Status: Reviewed & tested by the community » Needs work

I wonder if we need #2952390: Off-canvas styles override CKEditor's reset and theme first. Without that using this with the default config in Standard is a pretty messy experience. Plus there are problems with tooltip for text formats. See the screenshot in the issue summary of #2952390: Off-canvas styles override CKEditor's reset and theme. Testing via the UI reveals the patch works as expected. You're able to add blocks to a layout and they don't appear anyway else in the UI. Nice.

Through testing the UI I noticed a PHP notice:

  1. Install standard
  2. Enable layout builder
  3. Goto admin/structure/types/manage/article/display
  4. Enable the layout builder
  5. Click manage layout
  6. Click add block in the main content area
  7. Select the Basic block from the inline custom block section
  8. Add a random title and body
  9. Click add block
  10. Click save layout
  11. See Notice: Only variables should be passed by reference in Drupal\layout_builder\InlineBlockEntityOperations->getPluginBlockId() (line 193 of core/modules/layout_builder/src/InlineBlockEntityOperations.php).

Now going to do some code review...

alexpott’s picture

  1. +++ b/core/modules/layout_builder/layout_builder.services.yml
    @@ -43,3 +43,6 @@ services:
    +  inline_block_content.usage:
    

    If we always envisage this being part of the layout_builder module I think we should prefix the service with layout_builder..

    Also I think we should to make a decision about whether these are inline block or inline block content things. For example, we have InlineBlockEntityOperations and InlineBBlockContentBlockTest and InlineBlockTestBase. There's also inline custom block in the code and UI text. I think we should call them inline blocks everywhere and drop the content and custom as I think these have been used because we're extend the block content entity which using both these words.

  2. +++ b/core/modules/layout_builder/src/InlineBlockEntityOperations.php
    @@ -0,0 +1,266 @@
    +   * Constructs a new  EntityOperations object.
    

    There's a double space here.

  3. +++ b/core/modules/layout_builder/tests/modules/no_transitions_css/css/css_fix.theme.css
    @@ -0,0 +1,23 @@
    +/**
    + * Remove all transitions for testing.
    + */
    

    settings_tray has some pretty similar test code - we could open an issue to add a generic module to system/tests/modules so we don't repeat ourselves and others can benefit easily.

  4. I wonder about REST access to inline blocks. Has that been considered?
alexpott’s picture

  1. +++ b/core/modules/layout_builder/src/LayoutBuilderServiceProvider.php
    @@ -0,0 +1,40 @@
    + * Provides the inline block usage service.
    

    I don't think this provides that service - it registers an event listener that uses this service.

  2. +++ b/core/modules/layout_builder/src/LayoutEntityHelperTrait.php
    @@ -0,0 +1,108 @@
    +      $configuration = $component->getPlugin()->getConfiguration();
    +      if (!empty($configuration['block_revision_id'])) {
    

    Do we need to check the plugin type here - just incase another block plugin type uses the same configuration key? Nope getInlineBlockComponents() does that for us - so this looks good.

tedbow’s picture

Status: Needs work » Needs review
FileSize
20.07 KB
91.54 KB

@alexpott thanks for looking at the issue!
re: #214

  1. #214 fixed Only variables should be passed by reference error
  2. #215.1

    If we always envisage this being part of the layout_builder module I think we should prefix the service with layout_builder..

    I could see this being part of the block module after layout builder is stable if we determine these blocks should be able to added via the Block UI.

    Leaving now for @tim.plunkett's feedback

  3. #215.1 Changed to "inline block" every in text and file names.
  4. #21.2 fixed
  5. <a href="#comment-12723105">#215</a>.3
    +++ b/core/modules/layout_builder/tests/src/FunctionalJavascript/InlineBlockTestBase.php
    @@ -0,0 +1,222 @@
    +    // @todo Remove after https://www.drupal.org/project/drupal/issues/2901792.
    +    'no_transitions_css',
    

    We have a @todo here about this.

  6. #215.4
    Since REST access can't call \Drupal\block_content\Access\RefinableDependentAccessTrait::setAccessDependency it would follow the same logic as the Private file access or any other time access is determined on non-reusable blocks. You would only have access if you had access the entity if is used in. This controlled by \Drupal\layout_builder\EventSubscriber\SetInlineBlockDependency

    The block access handler \Drupal\block_content\BlockContentAccessControlHandler::checkAccess() is written you don't get access unless setAccessDependency() has already been called or an AccessibleInterface is set in the BlockContentGetDependencyEvent. so therefore you would never get accidental access to an non-reusable block.

    For access other than view layout_builder_block_content_access() ensures that you will not able to CREATE UPDATE or DELETE if the none reusable block is used by layout builder and the user doesn't have configure any layout permission

  7. #216.1
    It doesn't register the event listener if actually set the service definition in the container. Because we can't always register this service if block_content module is not installed.

    Changed to
    Sets the definition for inline block usage service.

  8. 👍
alexpott’s picture

+++ b/core/modules/layout_builder/src/LayoutBuilderServiceProvider.php
@@ -0,0 +1,40 @@
+ * Sets the definition for inline block usage service.
...
+      $container->setDefinition('layout_builder.get_block_dependency_subscriber', $definition);

I still think these things are not aligned. The comment makes me think you are setting the definition of the inline_block.usage service.

tedbow’s picture

@alexpott yeah sorry I missed your point before. fixed.

tim.plunkett’s picture

Status: Needs review » Reviewed & tested by the community

I disagree that #2952390: Off-canvas styles override CKEditor's reset and theme is a hard blocker for this. It predates this, is replicable in other ways aside from this, and this doesn't make things any worse.

#219 addresses all of #214/215/216, thanks @tedbow! And thanks @alexpott for the great review.

tedbow credited mglaman.

tedbow’s picture

Crediting everyone who reviewed and help here

Also adding @mglaman because help on #2948064: Layout Builder should support inline creation of Custom Blocks using entity serialization where this code started

Will add other users from related issues per comment

tedbow’s picture

@phenaproxima for help on #2948064 and #2976366: Create generic entity usage tracking service which ended up being specific tracking here

tedbow credited mtodor.

tedbow’s picture

tedbow credited japerry.

tedbow’s picture

@japerry, xjm, and @EclipseGc(already credited) for extensive help on #2948064: Layout Builder should support inline creation of Custom Blocks using entity serialization discussion to determine problems with that approach

tedbow credited xjm.

tedbow’s picture

tedbow’s picture

I think that is everyone from other issues. Thanks for everyone on this. Big thanks to @johndevman for the numerous reviews over last couple months!

alexpott’s picture

Discussed a couple of concerns in the layouts slack channel.

I had a concern that using this on per node layouts might cause problems eg. too much unstructured power, not included in search (turns out they are). @tim.plunkett, @tedbow pointed out that you can already use custom blocks here but this makes things much better because they don't pollute the main block UI and this solves a performance issue (see #2978102: Profile 1000s of custom non-reusable blocks for performance).

My other concern is the entity model - like getting a node via REST api, these blocks are very unconnected. @tim.plunkett pointed out again that this is already the case and @samuel.mortenson pointed me to #2942975: [PP-1] Expose Layout Builder data to REST and JSON:API.

So for me both of these concerns have been addressed.

alexpott’s picture

Status: Reviewed & tested by the community » Fixed

Committed and pushed 500403b458 to 8.7.x and 43187a5476 to 8.6.x. Thanks!

Backporting to 8.6.x as layout builder is experimental and this has been described as the killer feature so it's great to get it in front of people to experiment with in 8.6.x.

  • alexpott committed 500403b on 8.7.x
    Issue #2957425 by tedbow, johndevman, tim.plunkett, hawkeye.twolf,...

  • alexpott committed 43187a5 on 8.6.x
    Issue #2957425 by tedbow, johndevman, tim.plunkett, hawkeye.twolf,...
bkosborne’s picture

This is great, thanks for all the hard work everyone!

yanniboi’s picture

Hi, this is amazing!! :) Great work!

I've just been playing with this and it seems that cron errors when there are no inline blocks that need removing.

I've posted a follow up issue #2992817: Layout builder cron errors when no cleanup required.

Please let me know if I am missing something...

xjm’s picture

Assigned: Unassigned » tedbow
Status: Fixed » Needs work

Unfortunately we had to revert this due to a critical regression. @tedbow has a fix and test coverage for it. We can reroll this and recommit it with the fix once ready.

This will need to wait until after the RC commit freeze ends, but I think it is okay to backport this between RC1 and 8.6.0's commit freeze, since the module is only beta.

tedbow’s picture

Status: Needs work » Needs review
FileSize
92.42 KB
8.92 KB

This commit had an access by-pass vulnerability for access to private files that are used inline blocks but not in the layout for current revision of the entity.

You can see this vulnerability by:

  1. Install the standard profile
  2. Enable the block_content an layout_builder modules
  3. As an admin user on admin/structure/types/manage/article/display check the options Use Layout Builder and Allow each content item to have its layout customized.
  4. On the Basic block type add a file field that uses the private file system.
  5. Create an article node
  6. Logout of admin account
  7. Log in as a user with 'Configure any layout', Use contextual links, and Article: Edit any content permissions
  8. Goto /node/1/layout
  9. Click Add block
  10. In the block list in the dialog click Inline Block > Basic Block
  11. Add a block with a file uploaded to the file field that uses the private file system.(use a .txt file for simplicity)
  12. Save the layout
  13. You should be at node/1
  14. You should see the file name.
  15. click on the file name.
  16. You should see .txt file contents. This is intended since you can see the node and revision the file is attached to.
  17. Save the browser url to access the private file later(or open in another tab)
  18. Edit the node at /node/1/edit, this is only to create a new revision which layout builder currently doesn't do. Articles should be setup to create new revisions by default.
  19. Go back /node/1/layout
  20. Remove the block that was added previous in the previous step by clicking the contextual link and clicking "Remove block"
  21. Save the layout
  22. You should be at node/1
  23. You should NOTsee the file label because it has been removed from the revision.
  24. Using the url of the private file(or the open tab) reload the file.
  25. You can still see the file contents. This is the access by-pass the current user does not have access to see previous revisions fo the node
  26. Logout
  27. Using the url of the private file(or the open tab) reload the file.
  28. You can still the file contents. This is the access by-pass.

The explanation

  1. The block content entity was intentionally not deleted when it was removed from the layout because it was referenced in previous revision and should be available if the node is reverted to that revision.
  2. \Drupal\file\FileAccessControlHandler::checkAccess() will call file_get_file_references() this will return the block_content entity that file was attached to.
  3. file_get_file_references() has a noted limitation in that will only get references for the current revision. But the file is attached to the latest revision of the block. Though the inline block is not in the layout for the latest revision of the node
  4. \Drupal\file\FileAccessControlHandler::checkAccess() will attempt to determine if the user has access to the block_content entity by calling $referencing_entity->access('view', $account, TRUE)
  5. This will in turn call \Drupal\block_content\BlockContentAccessControlHandler::checkAccess().
  6. Since this is non-reusable block and setAccessDependency() has not been called on this block the BlockContentGetDependencyEvent will be called to allow a dependency to be set dynamically.
  7. Layout builder's own \Drupal\layout_builder\EventSubscriber\SetInlineBlockDependency() will respond to this event.
  8. \Drupal\layout_builder\EventSubscriber\SetInlineBlockDependency::getInlineBlockDependency() will find return the first revision of the node that uses the inline block in the layout. This revision is loaded via $revision = $layout_entity_storage->loadRevision($revision_id);
  9. After the event is completed the access to the dependency is will be checked in BlockContentAccessControlHandler::checkAccess() via
    $access = $access->andIf($dependency->access($operation, $account, TRUE));
    
  10. The assumption is if you explicitly load a revision and call access() access would be checked on that revision. That assumption is false. For all revisionable entities in core access checking does not take into account the revision of the object access() is called on.

    Access to revisions is only taken into account on the routing level for instance by nodes in \Drupal\node\Access\NodeRevisionAccessCheck

Suggested solution

Change \Drupal\layout_builder\EventSubscriber\SetInlineBlockDependency::getInlineBlockDependency() to only check to see if the block is used in the latest revision of the entity after it is loaded via $layout_entity = $layout_entity_storage->load($layout_entity_info->layout_entity_id);

This would mean you would not have access to a private file unless it is on the layout for the latest revision of entity. This though is the current situation with file fields: see Private file download returns access denied, when file attached to node revision other than current. So this seems like reasonable behavior.

tedbow’s picture

Assigned: tedbow » Unassigned

Un-assigning myself to get reviews

BTW we found this issue because #2937199: Track Layout override revisions on entities which support revisioning caused InlineBlockPrivateFilesTest to fail.

tedbow’s picture

Here is patch with the same as #239 except without the fix to core/modules/layout_builder/src/EventSubscriber/SetInlineBlockDependency.php

The new assertion at the end of \Drupal\Tests\layout_builder\FunctionalJavascript\InlineBlockPrivateFilesTest::testPrivateFiles() should fail.

tedbow’s picture

And now I will actually upload the file I mentioned in #241

DamienMcKenna’s picture

While it's frustrating that this was reverted, I appreciate that you all jumped on this so quickly. Thank you.

Status: Needs review » Needs work

The last submitted patch, 242: 2957425-241-test-only-FAIL.patch, failed testing. View results

tedbow’s picture

Status: Needs work » Needs review

#242 had the expected failure

re #243 yep it is frustrating, but to good to get it fixed now.

tim.plunkett’s picture

Status: Needs review » Reviewed & tested by the community

That interdiff in #239 is hugely instructive. As is the comment! Kudos @tedbow

(d.o might not like an RTBC issue where the last patch is failing, might want to reup the committable version)

Gábor Hojtsy’s picture

Issue tags: +8.6.0 highlights
mpotter’s picture

Here is a patch that changes update_8001 to 8602 so that it runs after 8.6-rc1. Sorry I wasn't able to post an interdiff.

(Also, this patch is against the 8.6-dev version)

mpotter’s picture

FileSize
72.09 KB

Here is the interdiff. So this was re-rolled against 8.6.x dev so there are some more changes here that I'm not sure about.

Edited: Sorry for the typo in the filename.

tedbow’s picture

Status: Reviewed & tested by the community » Needs review
FileSize
572 bytes
92.42 KB

@mpotter thanks for catching this. I have re-rolled the patch against 8.7.x

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

tim.plunkett’s picture

Status: Needs review » Reviewed & tested by the community

Thanks @mpotter!

tim.plunkett’s picture

To better clarify my comment in #246:

I have not directly worked on this patch. @tedbow and I worked through #239 together, and I can confirm that I:

  • Tested the steps to reproduce, and it is broken
  • Verified the explanation of the security bug
  • Verified that test coverage added to expose the bug is correct, and fails in isolation
  • Verified that the solution provided fixes the failing tests
  • Retested the steps to reproduce with the new patch, and it works!

The follow-up change in #250 was because another issue was committed in the interim that added another numbered hook_update_N().

  • webchick committed 4ed41f4 on 8.7.x
    Issue #2957425 by tedbow, johndevman, mpotter, tim.plunkett, hawkeye....
webchick’s picture

Status: Reviewed & tested by the community » Fixed

Rock, thanks for the clarification, @tim.plunkett. That all looks sane to me. Also thanks to @tedbow for the detailed STR in #239; very helpful!

Since @xjm indicated in #238 that we could still commit this to 8.6 given it's kinda "grandfathered" in, and given this has been given a security-minded review, going to try recommittting this, since we're currently between code freezes.

Committed and pushed to 8.7; cherry-picked to 8.6. Hopefully second time's a charm! :D

  • webchick committed fb03eec on 8.6.x
    Issue #2957425 by tedbow, johndevman, mpotter, tim.plunkett, hawkeye....
webchick’s picture

Status: Reviewed & tested by the community » Fixed

Ahem.

webchick’s picture

Issue tags: +MWDS2018
R_H-L’s picture

Small issue I noted in testing this:

If you have something in your custom block with a file upload (such as an image field), and the filename is long with no spaces, it pushes the right hand part of the file information box off the canvas, which also pushes the remove button off the screen.

Edit: It looks like this remove button is pushed off the screen regardless of filename size

Looks like this is already being looked at in issue 2951547

alesr’s picture

This issue could be related to the fact that the only reference of "block_content_field_data.reusable" I found was in this issue and there were some changes committed to 8.6.x.

https://www.drupal.org/project/drupal/issues/2998096

Status: Fixed » Closed (fixed)

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