Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Check for allowed blocks recursively in patterns #30366

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions packages/block-editor/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
"@babel/runtime": "^7.13.10",
"@wordpress/a11y": "file:../a11y",
"@wordpress/blob": "file:../blob",
"@wordpress/block-serialization-default-parser": "file:../block-serialization-default-parser",
david-szabo97 marked this conversation as resolved.
Show resolved Hide resolved
"@wordpress/blocks": "file:../blocks",
"@wordpress/components": "file:../components",
"@wordpress/compose": "file:../compose",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,17 +14,23 @@ function usePatternsSetup( clientId, blockName, filterPatternsFn ) {
const {
getBlockRootClientId,
__experimentalGetPatternsByBlockTypes,
__experimentalGetParsedPattern,
__experimentalGetAllowedPatterns,
} = select( blockEditorStore );
const rootClientId = getBlockRootClientId( clientId );
let patterns = [];
if ( filterPatternsFn ) {
return __experimentalGetAllowedPatterns( rootClientId ).filter(
filterPatternsFn
patterns = __experimentalGetAllowedPatterns(
rootClientId
).filter( filterPatternsFn );
} else {
patterns = __experimentalGetPatternsByBlockTypes(
blockName,
rootClientId
);
}
return __experimentalGetPatternsByBlockTypes(
blockName,
rootClientId
return patterns.map( ( { name } ) =>
__experimentalGetParsedPattern( name )
);
},
[ clientId, blockName, filterPatternsFn ]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,13 @@
*/
import { useMemo } from '@wordpress/element';
import { cloneBlock } from '@wordpress/blocks';
import { useSelect } from '@wordpress/data';

/**
* Internal dependencies
*/
import { getMatchingBlockByName, getRetainedBlockAttributes } from './utils';
import { store as blockEditorStore } from '../../store';

/**
* Mutate the matched block's attributes by getting
Expand Down Expand Up @@ -94,9 +96,19 @@ export const getPatternTransformedBlocks = (
*/
// TODO tests
const useTransformedPatterns = ( patterns, selectedBlocks ) => {
const parsedPatterns = useSelect(
( select ) =>
patterns.map( ( { name } ) =>
select( blockEditorStore ).__experimentalGetParsedPattern(
name
)
),
[ patterns ]
);

return useMemo(
() =>
patterns.reduce( ( accumulator, _pattern ) => {
parsedPatterns.reduce( ( accumulator, _pattern ) => {
const transformedBlocks = getPatternTransformedBlocks(
selectedBlocks,
_pattern.blocks
Expand All @@ -109,7 +121,7 @@ const useTransformedPatterns = ( patterns, selectedBlocks ) => {
}
return accumulator;
}, [] ),
[ patterns, selectedBlocks ]
[ parsedPatterns, selectedBlocks ]
);
};

Expand Down
103 changes: 76 additions & 27 deletions packages/block-editor/src/store/selectors.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import {
} from '@wordpress/blocks';
import { SVG, Rect, G, Path } from '@wordpress/components';
import { Platform } from '@wordpress/element';
import { parse as parseBlocks } from '@wordpress/block-serialization-default-parser';

/**
* A block selection object.
Expand Down Expand Up @@ -1260,6 +1261,22 @@ export function getTemplateLock( state, rootClientId ) {
return blockListSettings.templateLock;
}

const checkAllowList = ( list, item, defaultResult = null ) => {
if ( isBoolean( list ) ) {
return list;
}
if ( isArray( list ) ) {
// TODO: when there is a canonical way to detect that we are editing a post
// the following check should be changed to something like:
// if ( list.includes( 'core/post-content' ) && getEditorMode() === 'post-content' && item === null )
if ( list.includes( 'core/post-content' ) && item === null ) {
return true;
}
return list.includes( item );
}
return defaultResult;
};

/**
* Determines if the given block type is allowed to be inserted into the block list.
* This function is not exported and not memoized because using a memoized selector
Expand All @@ -1278,22 +1295,6 @@ const canInsertBlockTypeUnmemoized = (
blockName,
rootClientId = null
) => {
const checkAllowList = ( list, item, defaultResult = null ) => {
if ( isBoolean( list ) ) {
return list;
}
if ( isArray( list ) ) {
// TODO: when there is a canonical way to detect that we are editing a post
// the following check should be changed to something like:
// if ( list.includes( 'core/post-content' ) && getEditorMode() === 'post-content' && item === null )
if ( list.includes( 'core/post-content' ) && item === null ) {
return true;
}
return list.includes( item );
}
return defaultResult;
};

let blockType;
if ( blockName && 'object' === typeof blockName ) {
blockType = blockName;
Expand Down Expand Up @@ -1784,6 +1785,32 @@ export const __experimentalGetAllowedBlocks = createSelector(
]
);

const checkAllowListRecursive = ( blocks, allowedBlockTypes ) => {
if ( isBoolean( allowedBlockTypes ) ) {
return allowedBlockTypes;
}

const blocksQueue = [ ...blocks ];
while ( blocksQueue.length > 0 ) {
const block = blocksQueue.shift();

const isAllowed = checkAllowList(
allowedBlockTypes,
block.name || block.blockName,
true
);
if ( ! isAllowed ) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm noting that if we leave off the boolean-blinded true as the default value, we can trap the exact condition in the response of "If this block was explicitly rejected" vs. "if this block wasn't explicitly allowed" (which is what we have at the moment)

if (false === isAllowed) {
    return false;
}

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does that make any difference though? 🤔 This is the same way we use it in canInsertBlockTypeUnmemoized.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it only impacts code readability, not behavior. I brought up the idea because the dangling true confused me, hence "boolean blindness."

return false;
}

block.innerBlocks?.forEach( ( innerBlock ) => {
blocksQueue.push( innerBlock );
} );
}

return true;
};
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When would we not want this to be recursive? Would it make sense to leave the call the same, that is, checkAllowList, instead of creating a new idea/term/function to call?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We use the checkAllowList in canInsertBlockTypeUnmemoized. It was never a recursive function.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sure, I get that we might use this already, but is there a reason they shouldn't be recursive? It sounds like you identified an issue because it wasn't recursive, and do we end up with special rules for different circumstances?

Copy link
Member Author

@david-szabo97 david-szabo97 May 10, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's not an issue. In canInsertBlockTypeUnmemoized only works with block types, so it can't work in a recursive way.

Patterns are special in this case, since they contain multiple block types. We don't have any other way to insert multiple kinds of block types in a single action.

That was my thought, but reusable blocks are pretty similar 🤔 Like saving a Group as reusable block and then inserting it. This needs more testing and we shouldn't do such a huge change to the core in this PR.


export const __experimentalGetParsedPattern = createSelector(
( state, patternName ) => {
const patterns = state.settings.__experimentalBlockPatterns;
Expand All @@ -1799,26 +1826,48 @@ export const __experimentalGetParsedPattern = createSelector(
( state ) => [ state.settings.__experimentalBlockPatterns ]
);

const getAllAllowedPatterns = createSelector(
( state ) => {
const patterns = state.settings.__experimentalBlockPatterns;
const { allowedBlockTypes } = getSettings( state );
const parsedPatterns = patterns.map( ( pattern ) => ( {
...pattern,
// We only need the overall block structure of the pattern. So, for
// performance reasons, we can parse the pattern's content using
// the raw blocks parser, also known as the "stage I" block parser.
// This is about 250x faster than the full parse that the Block API
// offers.
blockNodes: parseBlocks( pattern.content ),
} ) );
const allowedPatterns = parsedPatterns.filter( ( { blockNodes } ) =>
checkAllowListRecursive( blockNodes, allowedBlockTypes )
);
return allowedPatterns;
},
( state ) => [
state.settings.__experimentalBlockPatterns,
state.settings.allowedBlockTypes,
]
);

/**
* Returns the list of allowed patterns for inner blocks children
* Returns the list of allowed patterns for inner blocks children.
*
* @param {Object} state Editor state.
* @param {?string} rootClientId Optional target root client ID.
*
* @return {Array?} The list of allowed block types.
* @return {Array?} The list of allowed patterns.
*/
export const __experimentalGetAllowedPatterns = createSelector(
( state, rootClientId = null ) => {
const patterns = state.settings.__experimentalBlockPatterns;
const parsedPatterns = patterns.map( ( { name } ) =>
__experimentalGetParsedPattern( state, name )
);
const patternsAllowed = filter( parsedPatterns, ( { blocks } ) =>
blocks.every( ( { name } ) =>
canInsertBlockType( state, name, rootClientId )
)
const availableParsedPatterns = getAllAllowedPatterns( state );
const patternsAllowed = filter(
availableParsedPatterns,
( { blockNodes } ) =>
blockNodes.every( ( { blockName } ) =>
canInsertBlockType( state, blockName, rootClientId )
)
);

return patternsAllowed;
},
( state, rootClientId ) => [
Expand Down
24 changes: 24 additions & 0 deletions packages/e2e-tests/plugins/allowed-patterns-disable-blocks.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<?php
/**
* Plugin Name: Gutenberg Test Allowed Patterns Disable Blocks
* Plugin URI: https://github.com/WordPress/gutenberg
* Author: Gutenberg Team
*
* @package gutenberg-test-allowed-patterns-disable-blocks
*/

/**
* Restrict the allowed blocks in the editor.
*
* @param Array $allowed_block_types An array of strings containing the previously allowed blocks.
* @param WP_Post $post The current post object.
* @return Array An array of strings containing the new allowed blocks after the filter is applied.
*/
function my_plugin_allowed_block_types( $allowed_block_types, $post ) {
if ( 'post' !== $post->post_type ) {
return $allowed_block_types;
}
return array( 'core/heading', 'core/columns', 'core/column', 'core/image', 'core/spacer' );
}

add_filter( 'allowed_block_types', 'my_plugin_allowed_block_types', 10, 2 );
32 changes: 32 additions & 0 deletions packages/e2e-tests/plugins/allowed-patterns.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<?php
/**
* Plugin Name: Gutenberg Test Allowed Patterns
* Plugin URI: https://github.com/WordPress/gutenberg
* Author: Gutenberg Team
*
* @package gutenberg-test-allowed-patterns
*/

register_block_pattern(
'test-allowed-patterns/lone-heading',
array(
'title' => 'Test: Single heading',
'content' => '<!-- wp:heading --><h2>Hello!</h2><!-- /wp:heading -->',
)
);

register_block_pattern(
'test-allowed-patterns/lone-paragraph',
array(
'title' => 'Test: Single paragraph',
'content' => '<!-- wp:paragraph --><p>Hello!</p><!-- /wp:paragraph -->',
)
);

register_block_pattern(
'test-allowed-patterns/paragraph-inside-group',
array(
'title' => 'Test: Paragraph inside group',
'content' => '<!-- wp:group --><div class="wp-block-group"><!-- wp:paragraph --><p>Hello!</p><!-- /wp:paragraph --></div><!-- /wp:group -->',
)
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
/**
* WordPress dependencies
*/
import {
activatePlugin,
createNewPost,
deactivatePlugin,
searchForPattern,
toggleGlobalBlockInserter,
} from '@wordpress/e2e-test-utils';

const checkPatternExistence = async ( name, available = true ) => {
await searchForPattern( name );
const patternElement = await page.waitForXPath(
`//div[@role = 'option']//div[contains(text(), '${ name }')]`,
{ timeout: 5000, visible: available, hidden: ! available }
);
const patternExists = !! patternElement;
await toggleGlobalBlockInserter();
return patternExists;
};

const TEST_PATTERNS = [
[ 'Test: Single heading', true ],
[ 'Test: Single paragraph', false ],
[ 'Test: Paragraph inside group', false ],
];

describe( 'Allowed Patterns', () => {
beforeAll( async () => {
await activatePlugin( 'gutenberg-test-allowed-patterns' );
await createNewPost();
} );
afterAll( async () => {
await deactivatePlugin( 'gutenberg-test-allowed-patterns' );
} );

describe( 'Disable blocks plugin disabled', () => {
for ( const [ patternName ] of TEST_PATTERNS ) {
it( `should show test pattern "${ patternName }"`, async () => {
expect( await checkPatternExistence( patternName, true ) ).toBe(
true
);
} );
}
} );

describe( 'Disable blocks plugin enabled', () => {
beforeAll( async () => {
await activatePlugin(
'gutenberg-test-allowed-patterns-disable-blocks'
);
await createNewPost();
} );
afterAll( async () => {
await deactivatePlugin(
'gutenberg-test-allowed-patterns-disable-blocks'
);
} );

for ( const [ patternName, shouldBeAvailable ] of TEST_PATTERNS ) {
it( `should${
shouldBeAvailable ? '' : ' not'
} show test "pattern ${ patternName }"`, async () => {
expect(
await checkPatternExistence(
patternName,
shouldBeAvailable
)
).toBe( shouldBeAvailable );
} );
}
} );
} );