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

Forbid using certain functions and classes #57738

Merged
merged 37 commits into from
Jan 31, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
0005ff9
Add a new property to the PHPCS sniff.
anton-vlasenko Dec 12, 2023
7d473cd
Commit WIP.
anton-vlasenko Dec 12, 2023
efa3918
Commit WIP.
anton-vlasenko Dec 13, 2023
abaad87
Commit WIP.
anton-vlasenko Dec 13, 2023
1a92b2b
Implement processing function calls.
anton-vlasenko Dec 15, 2023
01dc9c9
Implement the ValidBlockLibraryFunctionNameSniff::processFunctionCall…
anton-vlasenko Jan 4, 2024
f176d57
Add the disallowed_function_calls property to phpcs.xml.dist.
anton-vlasenko Jan 4, 2024
4c32ad6
Improve the error message.
anton-vlasenko Jan 5, 2024
0056957
Rename the method to better refelct its purpose.
anton-vlasenko Jan 5, 2024
be64f5c
Move the changes to a new sniff.
anton-vlasenko Jan 8, 2024
c949041
Update the phpcs.xml.dist file.
anton-vlasenko Jan 8, 2024
8b262af
Fix error.
anton-vlasenko Jan 9, 2024
e7e5a1e
Fix property name.
anton-vlasenko Jan 9, 2024
5a0a63d
Fix the sniff.
anton-vlasenko Jan 10, 2024
8e64c08
Fix the config.
anton-vlasenko Jan 10, 2024
3dbd5a8
Remove redundnat code.
anton-vlasenko Jan 10, 2024
66cc48f
Refactor the sniff, add descriptions.
anton-vlasenko Jan 10, 2024
b8e4742
Fix doc blocks.
anton-vlasenko Jan 11, 2024
dde1e6d
Implement the test for the RestrictedFunctionsAndClassesSniff sniff.
anton-vlasenko Jan 11, 2024
5d9b80e
Implement the test for the RestrictedFunctionsAndClassesSniff sniff.
anton-vlasenko Jan 11, 2024
f1cf5bd
Rename Restricted to Forbidden as forbidden is a better description o…
anton-vlasenko Jan 22, 2024
f10aee5
Rename properties.
anton-vlasenko Jan 22, 2024
9ff2764
Rename properties.
anton-vlasenko Jan 22, 2024
829310d
Fix typo.
anton-vlasenko Jan 22, 2024
1047a39
Change error codes.
anton-vlasenko Jan 23, 2024
c0437fc
Teach the sniff not to flag guarded and, therefore, legitimate uses o…
anton-vlasenko Jan 23, 2024
4cd7b18
Improve checks for guarded functions/classes.
anton-vlasenko Jan 23, 2024
539406c
Better explanation + fix code style.
anton-vlasenko Jan 23, 2024
9b590f8
Add phpcs:ignore statements.
anton-vlasenko Jan 23, 2024
c1ebd27
Refactor the code so that the sniff doesn't flag it.
anton-vlasenko Jan 25, 2024
6bfd934
Fix typo.
anton-vlasenko Jan 25, 2024
588a094
Fix code style.
anton-vlasenko Jan 25, 2024
9cd966d
Fix typo.
anton-vlasenko Jan 25, 2024
ae6a161
Add unit tests.
anton-vlasenko Jan 26, 2024
8249699
Fallback to isset().
anton-vlasenko Jan 30, 2024
0c8ef69
Improve the regular expression.
anton-vlasenko Jan 30, 2024
255e3ed
Move the exclude pattern statements to the `phpcs.xml.dist` file to a…
anton-vlasenko Jan 30, 2024
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
6 changes: 5 additions & 1 deletion packages/block-library/src/file/index.php
Original file line number Diff line number Diff line change
Expand Up @@ -63,9 +63,13 @@ function register_block_core_file() {
)
);

if ( defined( 'IS_GUTENBERG_PLUGIN' ) && IS_GUTENBERG_PLUGIN ) {
$module_url = gutenberg_url( '/build/interactivity/file.min.js' );
}

wp_register_script_module(
'@wordpress/block-library/file',
defined( 'IS_GUTENBERG_PLUGIN' ) && IS_GUTENBERG_PLUGIN ? gutenberg_url( '/build/interactivity/file.min.js' ) : includes_url( 'blocks/file/view.min.js' ),
isset( $module_url ) ? $module_url : includes_url( 'blocks/file/view.min.js' ),
array( '@wordpress/interactivity' ),
defined( 'GUTENBERG_VERSION' ) ? GUTENBERG_VERSION : get_bloginfo( 'version' )
);
Expand Down
6 changes: 5 additions & 1 deletion packages/block-library/src/image/index.php
Original file line number Diff line number Diff line change
Expand Up @@ -323,9 +323,13 @@ function register_block_core_image() {
)
);

if ( defined( 'IS_GUTENBERG_PLUGIN' ) && IS_GUTENBERG_PLUGIN ) {
$module_url = gutenberg_url( '/build/interactivity/image.min.js' );
}

wp_register_script_module(
'@wordpress/block-library/image',
defined( 'IS_GUTENBERG_PLUGIN' ) && IS_GUTENBERG_PLUGIN ? gutenberg_url( '/build/interactivity/image.min.js' ) : includes_url( 'blocks/image/view.min.js' ),
isset( $module_url ) ? $module_url : includes_url( 'blocks/image/view.min.js' ),
array( '@wordpress/interactivity' ),
defined( 'GUTENBERG_VERSION' ) ? GUTENBERG_VERSION : get_bloginfo( 'version' )
);
Expand Down
6 changes: 5 additions & 1 deletion packages/block-library/src/navigation/index.php
Original file line number Diff line number Diff line change
Expand Up @@ -1075,9 +1075,13 @@ function register_block_core_navigation() {
)
);

if ( defined( 'IS_GUTENBERG_PLUGIN' ) && IS_GUTENBERG_PLUGIN ) {
$module_url = gutenberg_url( '/build/interactivity/navigation.min.js' );
}

wp_register_script_module(
'@wordpress/block-library/navigation',
defined( 'IS_GUTENBERG_PLUGIN' ) && IS_GUTENBERG_PLUGIN ? gutenberg_url( '/build/interactivity/navigation.min.js' ) : includes_url( 'blocks/navigation/view.min.js' ),
isset( $module_url ) ? $module_url : includes_url( 'blocks/navigation/view.min.js' ),
array( '@wordpress/interactivity' ),
defined( 'GUTENBERG_VERSION' ) ? GUTENBERG_VERSION : get_bloginfo( 'version' )
);
Expand Down
6 changes: 5 additions & 1 deletion packages/block-library/src/query/index.php
Original file line number Diff line number Diff line change
Expand Up @@ -99,9 +99,13 @@ function register_block_core_query() {
)
);

if ( defined( 'IS_GUTENBERG_PLUGIN' ) && IS_GUTENBERG_PLUGIN ) {
$module_url = gutenberg_url( '/build/interactivity/query.min.js' );
}

wp_register_script_module(
'@wordpress/block-library/query',
defined( 'IS_GUTENBERG_PLUGIN' ) && IS_GUTENBERG_PLUGIN ? gutenberg_url( '/build/interactivity/query.min.js' ) : includes_url( 'blocks/query/view.min.js' ),
isset( $module_url ) ? $module_url : includes_url( 'blocks/query/view.min.js' ),
array(
array(
'id' => '@wordpress/interactivity',
Expand Down
6 changes: 5 additions & 1 deletion packages/block-library/src/search/index.php
Original file line number Diff line number Diff line change
Expand Up @@ -197,9 +197,13 @@ function register_block_core_search() {
)
);

if ( defined( 'IS_GUTENBERG_PLUGIN' ) && IS_GUTENBERG_PLUGIN ) {
$module_url = gutenberg_url( '/build/interactivity/search.min.js' );
}

wp_register_script_module(
'@wordpress/block-library/search',
defined( 'IS_GUTENBERG_PLUGIN' ) && IS_GUTENBERG_PLUGIN ? gutenberg_url( '/build/interactivity/search.min.js' ) : includes_url( 'blocks/search/view.min.js' ),
isset( $module_url ) ? $module_url : includes_url( 'blocks/search/view.min.js' ),
array( '@wordpress/interactivity' ),
defined( 'GUTENBERG_VERSION' ) ? GUTENBERG_VERSION : get_bloginfo( 'version' )
);
Expand Down
19 changes: 19 additions & 0 deletions phpcs.xml.dist
Original file line number Diff line number Diff line change
Expand Up @@ -153,4 +153,23 @@
</property>
</properties>
</rule>
<rule ref="Gutenberg.CodeAnalysis.ForbiddenFunctionsAndClasses">
<include-pattern>./packages/block-library/src/*/*.php</include-pattern>
<!--
Refactoring required: the functions and classes prefixed with 'Gutenberg_' in the files that are being excluded
through the 'exclude-pattern' statements below must be guarded, as they are available only in the Gutenberg context,
not in Core.
-->
<exclude-pattern>./packages/block-library/src/form-input/index.php</exclude-pattern>
<exclude-pattern>./packages/block-library/src/form-submission-notification/index.php</exclude-pattern>
<exclude-pattern>./packages/block-library/src/form/index.php</exclude-pattern>
<properties>
<property name="forbidden_functions" type="array">
<element value="[Gg]utenberg.*"/>
</property>
<property name="forbidden_classes" type="array">
<element value="[Gg]utenberg.*"/>
</property>
</properties>
</rule>
</ruleset>
Original file line number Diff line number Diff line change
@@ -0,0 +1,231 @@
<?php
/**
* Gutenberg Coding Standards.
*
* @package gutenberg/gutenberg-coding-standards
* @link https://github.com/WordPress/gutenberg
* @license https://opensource.org/licenses/MIT MIT
*/

namespace GutenbergCS\Gutenberg\Sniffs\CodeAnalysis;

use PHP_CodeSniffer\Files\File;
use PHP_CodeSniffer\Sniffs\Sniff;

/**
* This sniff enforces usage restrictions on specific classes and functions within the Gutenberg project.
* While it permits the declaration of these classes/functions, their actual usage in code is forbidden based on defined regular expressions.
*
* @package gutenberg/gutenberg-coding-standards
*
* @since 1.0.0
*/
final class ForbiddenFunctionsAndClassesSniff implements Sniff {

/**
* Holds regular expressions for classes whose usage is forbidden.
* Note that declaring functions with names matching these regular expressions is still permitted.
* Useful for enforcing security or architectural constraints.
*
* @var array Array of regex patterns for forbidden classes.
*/
public $forbidden_classes = array();

/**
* Holds regular expressions for classes whose usage is forbidden.
* Note that declaring classes with names matching these regular expressions is still permitted.
* Useful for enforcing security or architectural constraints.
*
* @var array Array of regex patterns for forbidden functions.
*/
public $forbidden_functions = array();

/**
* Registers the tokens that this sniff wants to listen for.
*
* @return array
*/
public function register() {
$this->on_register_event();

return array( T_STRING );
}

/**
* Handles processing tokens.
*
* @param File $phpcsFile The file being scanned.
* @param int $stackPtr The position of the current token
* in the stack passed in $tokens.
*/
public function process( File $phpcsFile, $stackPtr ) {
$this->process_string_token( $phpcsFile, $stackPtr );
}

/**
* Detects whether a given string token represents a function call or class usage.
* It then delegates further processing based on the type of usage detected.
*
* @param File $phpcs_file The file being scanned.
* @param int $stack_pointer The position of the current token in the token stack.
*/
private function process_string_token( File $phpcs_file, $stack_pointer ) {
if ( empty( $this->forbidden_functions ) && empty( $this->forbidden_classes ) ) {
// Nothing to process.
return;
}

$tokens = $phpcs_file->getTokens();

$next_token = $phpcs_file->findPrevious( T_WHITESPACE, ( $stack_pointer + 1 ), null, true, null, true );
if ( false !== $next_token && ( T_DOUBLE_COLON === $tokens[ $next_token ]['code'] ) ) {
// Static class method or a class constant.
$this->check_class_usage( $phpcs_file, $stack_pointer );

return;
}

$previous_token = $phpcs_file->findPrevious( T_WHITESPACE, $stack_pointer - 1, null, true, null, true );
if ( false !== $previous_token && ( T_NEW === $tokens[ $previous_token ]['code'] ) ) {
// Static method or a constant usage.
$this->check_class_usage( $phpcs_file, $stack_pointer );

return;
}

if ( false !== $next_token && ( T_OPEN_PARENTHESIS === $tokens[ $next_token ]['code'] ) ) {
// Function.
$this->check_function_usage( $phpcs_file, $stack_pointer );
}
}

/**
* Checks for forbidden class usage.
*
* @param File $phpcs_file File being scanned.
* @param int $stack_pointer Position of the text token in the token stack.
*/
private function check_class_usage( File $phpcs_file, $stack_pointer ) {
if ( empty( $this->forbidden_classes ) ) {
// Nothing to process.
return;
}

if ( $this->check_if_token_guarded( $phpcs_file, $stack_pointer ) ) {
// The token is guarded. Nothing to process.
return;
}

$tokens = $phpcs_file->getTokens();
$class_name = $tokens[ $stack_pointer ]['content'];

foreach ( $this->forbidden_classes as $forbidden_class ) {
$regexp = sprintf( '/^%s$/', $forbidden_class );
if ( 1 !== preg_match( $regexp, $class_name ) ) {
// The class has a valid name; bypassing further checks.
return;
}
}

$error_message = 'It\'s not allowed to use the "' . $class_name . '" class as its name matches the forbidden pattern: "' . $regexp . '".';
$phpcs_file->addError( $error_message, $stack_pointer, 'ForbiddenClassUsage' );
}

/**
* Checks for forbidden function usage.
*
* @param File $phpcs_file File being scanned.
* @param int $stack_pointer Position of the text token in the token stack.
*/
private function check_function_usage( File $phpcs_file, $stack_pointer ) {
if ( empty( $this->forbidden_functions ) ) {
// Nothing to process.
return;
}

if ( $this->check_if_token_guarded( $phpcs_file, $stack_pointer ) ) {
// The token is guarded. Nothing to process.
return;
}

$tokens = $phpcs_file->getTokens();
$function_name = $tokens[ $stack_pointer ]['content'];

foreach ( $this->forbidden_functions as $disallowed_function_call ) {
$regexp = sprintf( '/^%s$/', $disallowed_function_call );
if ( 1 !== preg_match( $regexp, $function_name ) ) {
// The function has a valid name; bypassing further checks.
return;
}
}

$error_message = 'It\'s not allowed to call the "' . $function_name . '()" function as its name matches the forbidden pattern: "' . $regexp . '".';
$phpcs_file->addError( $error_message, $stack_pointer, 'ForbiddenFunctionCall' );
}

/**
* This function looks for a specific if condition that wraps the token
* to determine if it's protected from execution outside of Gutenberg.
*
* Example of a guarded function (works similarly for classes):
* if ( defined( 'IS_GUTENBERG_PLUGIN' ) && IS_GUTENBERG_PLUGIN ) {
* // gutenberg_prefixed_i_am_allowed_to_be_called_since_i_am_guarded_function();
* }
*
* @param File $phpcs_file File being scanned.
* @param int $stack_pointer Position of the text token in the token stack.
*
* @return bool Returns true if the token is guarded.
*/
private function check_if_token_guarded( File $phpcs_file, $stack_pointer ) {
if ( false === $phpcs_file->hasCondition( $stack_pointer, T_IF ) ) {
// No wrapping IF tokens. Stop processing.
return false;
}

$tokens = $phpcs_file->getTokens();

// It has to start from the matched condition closest to the passed token.
$conditions = array_reverse( $tokens[ $stack_pointer ]['conditions'], true );

// Matches the "if ( defined( 'IS_GUTENBERG_PLUGIN' ) && IS_GUTENBERG_PLUGIN" string.
$regexp = '/if\s*\(\s*defined\(\s*(\'|")IS_GUTENBERG_PLUGIN(\'|")\s*\)\s*&&\s*IS_GUTENBERG_PLUGIN/';
Copy link
Contributor

Choose a reason for hiding this comment

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

I'm wondering if there would ever be a reason to make this check configurable? Checking for IS_GUTENBERG_PLUGIN makes sense in block_library or any other PHP files that may be auto-generated in core from the npm packages. I guess this kind of logic wouldn't be needed at all if we were using the sniff in plugin-specific PHP files.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The sniff is currently only checking the /packages/block-library folder: https://github.com/WordPress/gutenberg/pull/57738/files#diff-05ae9cddcaec1e845771a7db224961439f83ef5939ec67d3a48744cb34d7e58bR157
So, it doesn't check files in other folders.
I hope I've understood your question correctly.


foreach ( $conditions as $wrapping_if_token => $condition ) {
if ( T_IF !== $condition ) {
continue;
}

$end_of_wrapping_if_token = $phpcs_file->findEndOfStatement( $wrapping_if_token );
$content = $phpcs_file->getTokensAsString( $wrapping_if_token, $end_of_wrapping_if_token - $wrapping_if_token );

if ( 1 === preg_match( $regexp, $content ) ) {
return true;
}
}

return false;
}

/**
* The purpose of this method is to run callbacks
* after the class properties have been set.
*/
private function on_register_event() {
$this->forbidden_classes = self::sanitize( $this->forbidden_classes );
$this->forbidden_functions = self::sanitize( $this->forbidden_functions );
}

/**
* Sanitize an array of values by trimming each element and removing empty elements.
*
* @param array $values The values being sanitized.
*
* @return array
*/
private static function sanitize( $values ) {
$values = array_map( 'trim', $values );

return array_filter( $values );
}
}
Loading
Loading