Skip to content

Commit

Permalink
Forbid using certain functions and classes
Browse files Browse the repository at this point in the history
Forbid using functions and classes that are not availble in the Core context.
  • Loading branch information
anton-vlasenko authored Jan 31, 2024
1 parent cd1baa0 commit 7fd9023
Show file tree
Hide file tree
Showing 10 changed files with 502 additions and 5 deletions.
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/';

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

0 comments on commit 7fd9023

Please sign in to comment.