Skip to content

Commit

Permalink
Check for allowed blocks recursively in patterns (#30366)
Browse files Browse the repository at this point in the history
Currently, patterns show up in the inserter even if they contain a block that's not on the allowed blocks list. Blocks aren't checked recursively in the patterns, only top level blocks. Even though they show up, we can't insert them since the blocks are disallowed. This PR checks recursively for disallowed blocks.
  • Loading branch information
david-szabo97 authored May 14, 2021
1 parent d21299e commit 6a843df
Show file tree
Hide file tree
Showing 8 changed files with 233 additions and 34 deletions.
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",
"@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 ) {
return false;
}

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

return true;
};

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 -->',
)
);
74 changes: 74 additions & 0 deletions packages/e2e-tests/specs/editor/various/allowed-patterns.test.js
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 );
} );
}
} );
} );

0 comments on commit 6a843df

Please sign in to comment.