diff --git a/includes/Checker/Checks/Plugin_Repo/Direct_File_Access_Check.php b/includes/Checker/Checks/Plugin_Repo/Direct_File_Access_Check.php index 36b03936c..a8b82e9ee 100644 --- a/includes/Checker/Checks/Plugin_Repo/Direct_File_Access_Check.php +++ b/includes/Checker/Checks/Plugin_Repo/Direct_File_Access_Check.php @@ -7,6 +7,11 @@ namespace WordPress\Plugin_Check\Checker\Checks\Plugin_Repo; +use PhpParser\Error; +use PhpParser\Node; +use PhpParser\Node\Expr; +use PhpParser\Node\Stmt; +use PhpParser\ParserFactory; use WordPress\Plugin_Check\Checker\Check_Categories; use WordPress\Plugin_Check\Checker\Check_Result; use WordPress\Plugin_Check\Checker\Checks\Abstract_File_Check; @@ -25,6 +30,7 @@ * using checks like: if ( ! defined( 'ABSPATH' ) ) exit; * * @since 1.8.0 + * @SuppressWarnings(PHPMD.ExcessiveClassComplexity) */ class Direct_File_Access_Check extends Abstract_File_Check { @@ -124,6 +130,181 @@ private function has_direct_access_protection( $file ) { return false; } + // Try AST-based detection first for more accurate results. + $parser = ( new ParserFactory() )->create( ParserFactory::PREFER_PHP7 ); + try { + $ast = $parser->parse( $contents ); + if ( null !== $ast ) { + if ( $this->has_direct_access_protection_ast( $ast ) ) { + return true; + } + } + } catch ( Error $e ) { + // Fall through to regex-based detection. + } + + // Fallback to regex-based detection. + return $this->has_direct_access_protection_regex( $contents ); + } + + /** + * Checks if AST contains proper direct access protection using AST parsing. + * + * @since 1.9.0 + * + * @param array $ast The parsed AST nodes. + * @return bool True if protection found, false otherwise. + */ + private function has_direct_access_protection_ast( array $ast ) { + $protected_vars = array( 'ABSPATH', 'WPINC' ); + + foreach ( $ast as $node ) { + $class = get_class( $node ); + if ( 'PhpParser\Node\Stmt\Expression' === $class ) { + if ( $this->is_protection_expression( $node->expr, $protected_vars ) ) { + return true; + } + } + + if ( 'PhpParser\Node\Stmt\If_' === $class ) { + if ( $this->is_protection_if_statement( $node, $protected_vars ) ) { + return true; + } + } + } + + return false; + } + + /** + * Checks if an expression is a protection pattern (defined() || exit). + * Matches the internal scanner's approach. + * + * @since 1.9.0 + * + * @param Expr $expr The expression to check. + * @param array $protected_vars Array of protected variable names. + * @return bool True if protection pattern, false otherwise. + */ + private function is_protection_expression( $expr, array $protected_vars ) { + $class = get_class( $expr ); + if ( 'PhpParser\Node\Expr\BinaryOp\BooleanOr' === $class ) { + // @phpstan-ignore-next-line Access to property $left on PhpParser\Node\Expr\BinaryOp\BooleanOr. + if ( ! empty( $expr->left ) && ! empty( $expr->right ) ) { + // @phpstan-ignore-next-line Access to property $right on PhpParser\Node\Expr\BinaryOp\BooleanOr. + if ( $this->check_defined_expr( $expr->left, $protected_vars ) ) { + // @phpstan-ignore-next-line Access to property $right on PhpParser\Node\Expr\BinaryOp\BooleanOr. + if ( $this->is_this_an_exit( $expr->right ) ) { + return true; + } + } + } + } + + return false; + } + + /** + * Checks if an If statement is a protection pattern (if ( ! defined() ) exit). + * Matches the internal scanner's approach. + * + * @since 1.9.0 + * + * @param Stmt\If_ $node The If statement node. + * @param array $protected_vars Array of protected variable names. + * @return bool True if protection pattern, false otherwise. + */ + private function is_protection_if_statement( Stmt\If_ $node, array $protected_vars ) { + $class = get_class( $node->cond ); + if ( 'PhpParser\Node\Expr\BooleanNot' === $class ) { + // @phpstan-ignore-next-line Access to property $expr on PhpParser\Node\Expr\BooleanNot. + if ( $this->check_defined_expr( $node->cond->expr, $protected_vars ) ) { + if ( ! empty( $node->stmts ) ) { + $continue = false; + foreach ( $node->stmts as $stmt ) { + // @phpstan-ignore-next-line Access to property $expr on statement. + if ( ! empty( $stmt->expr ) && $this->is_this_an_exit( $stmt->expr ) ) { + $continue = true; + } + } + if ( $continue ) { + return true; + } + } + } + } + + return false; + } + + /** + * Checks if an expression is a defined() call with protected variable. + * Matches the internal scanner's check_defined_expr() method exactly. + * Works with both regular and named arguments (PHP 8+). + * + * @since 1.9.0 + * + * @param Expr $expr The expression to check. + * @param array $protected_vars Array of protected variable names. + * @return bool True if defined() check, false otherwise. + */ + private function check_defined_expr( $expr, array $protected_vars ) { + $class = get_class( $expr ); + if ( 'PhpParser\Node\Expr\FuncCall' === $class ) { + // @phpstan-ignore-next-line Access to property $name on PhpParser\Node\Expr\FuncCall. + if ( ! empty( $expr->name ) && $expr->name instanceof Node\Name && 'defined' === $expr->name->toString() ) { + // @phpstan-ignore-next-line Access to property $args on PhpParser\Node\Expr\FuncCall. + if ( ! empty( $expr->args[0]->value ) && 'PhpParser\Node\Scalar\String_' === get_class( $expr->args[0]->value ) ) { + // @phpstan-ignore-next-line Access to property $value on PhpParser\Node\Scalar\String_. + if ( ! empty( $expr->args[0]->value->value ) ) { + // @phpstan-ignore-next-line Access to property $value on PhpParser\Node\Scalar\String_. + if ( in_array( $expr->args[0]->value->value, $protected_vars, true ) ) { + return true; + } + } + } + } + } + + return false; + } + + /** + * Checks if an expression is an exit/die call. + * Matches the internal scanner's is_this_an_exit() method. + * + * @since 1.9.0 + * + * @param Expr $expr The expression to check. + * @return bool True if exit/die call, false otherwise. + */ + private function is_this_an_exit( $expr ) { + $class = get_class( $expr ); + if ( 'PhpParser\Node\Expr\Exit_' === $class ) { + return true; + } + if ( 'PhpParser\Node\Expr\FuncCall' === $class ) { + // @phpstan-ignore-next-line Access to property $name on PhpParser\Node\Expr\FuncCall. + if ( ! empty( $expr->name ) && $expr->name instanceof Node\Name ) { + $name = $expr->name->toString(); + if ( 'exit' === $name || 'die' === $name ) { + return true; + } + } + } + + return false; + } + + /** + * Checks if a file has proper direct access protection using regex. + * + * @since 1.9.0 + * + * @param string $contents The file contents. + * @return bool True if protection found, false otherwise. + */ + private function has_direct_access_protection_regex( $contents ) { // Remove the opening PHP tag if present. $contents = preg_replace( '/^<\?php\s*/i', '', $contents ); @@ -139,28 +320,28 @@ private function has_direct_access_protection( $file ) { $without_comments = preg_replace( '/\n\s*\n\s*\n/', "\n\n", $without_comments ); $without_comments = trim( $without_comments ); - // Pattern 1: defined( 'ABSPATH' ) || exit; or, exit; . - if ( preg_match( "/defined\s*\(\s*['\"]ABSPATH['\"]\s*\)\s*(?:\|\||or)\s*(?:exit|die)\s*(?:\([^)]*\))?\s*;/i", $without_comments ) ) { + // Pattern 1: defined( 'ABSPATH' ) || exit; or defined( constant_name: 'ABSPATH' ) || exit;. + if ( preg_match( "/defined\s*\(\s*(?:constant_name\s*:\s*)?['\"]ABSPATH['\"]\s*\)\s*(?:\|\||or)\s*(?:exit|die)\s*(?:\([^)]*\))?\s*;/i", $without_comments ) ) { return true; } - // Pattern 2: defined( 'WPINC' ) || exit; or, die();. - if ( preg_match( "/defined\s*\(\s*['\"]WPINC['\"]\s*\)\s*(?:\|\||or)\s*(?:exit|die)\s*(?:\([^)]*\))?\s*;/i", $without_comments ) ) { + // Pattern 2: defined( 'WPINC' ) || exit; or defined( constant_name: 'WPINC' ) || exit;. + if ( preg_match( "/defined\s*\(\s*(?:constant_name\s*:\s*)?['\"]WPINC['\"]\s*\)\s*(?:\|\||or)\s*(?:exit|die)\s*(?:\([^)]*\))?\s*;/i", $without_comments ) ) { return true; } - // Pattern 3: if ( ! defined( 'ABSPATH' ) ) exit; or, exit;. - if ( preg_match( "/if\s*\(\s*!\s*defined\s*\(\s*['\"]ABSPATH['\"]\s*\)\s*\)\s*(?:\{|exit|die)/i", $without_comments ) ) { + // Pattern 3: if ( ! defined( 'ABSPATH' ) ) exit; or if ( ! defined( constant_name: 'ABSPATH' ) ) exit;. + if ( preg_match( "/if\s*\(\s*!\s*defined\s*\(\s*(?:constant_name\s*:\s*)?['\"]ABSPATH['\"]\s*\)\s*\)\s*(?:\{|exit|die)/i", $without_comments ) ) { return true; } - // Pattern 4: if ( ! defined( 'WPINC' ) ) exit; {exit; or, die();}. - if ( preg_match( "/if\s*\(\s*!\s*defined\s*\(\s*['\"]WPINC['\"]\s*\)\s*\)\s*(?:\{|exit|die)/i", $without_comments ) ) { + // Pattern 4: if ( ! defined( 'WPINC' ) ) exit; or if ( ! defined( constant_name: 'WPINC' ) ) exit;. + if ( preg_match( "/if\s*\(\s*!\s*defined\s*\(\s*(?:constant_name\s*:\s*)?['\"]WPINC['\"]\s*\)\s*\)\s*(?:\{|exit|die)/i", $without_comments ) ) { return true; } - // Pattern 5: if ( ! defined( 'ABSPATH' ) ) { die(); }, WPINC. - if ( preg_match( "/if\s*\(\s*!\s*defined\s*\(\s*['\"](?:ABSPATH|WPINC)['\"]\s*\)\s*\)\s*\{[^}]*die\s*\(/i", $without_comments ) ) { + // Pattern 5: if ( ! defined( 'ABSPATH' ) ) { die(); } or if ( ! defined( constant_name: 'ABSPATH' ) ) { die(); }. + if ( preg_match( "/if\s*\(\s*!\s*defined\s*\(\s*(?:constant_name\s*:\s*)?['\"](?:ABSPATH|WPINC)['\"]\s*\)\s*\)\s*\{[^}]*die\s*\(/i", $without_comments ) ) { return true; } @@ -184,6 +365,462 @@ private function is_valid_for_direct_access( $file ) { return false; } + $parser = ( new ParserFactory() )->create( ParserFactory::PREFER_PHP7 ); + try { + $ast = $parser->parse( $contents ); + if ( null === $ast ) { + return $this->is_valid_for_direct_access_regex( $contents ); + } + + return $this->is_ast_valid_for_direct_access( $ast ); + } catch ( Error $e ) { + return $this->is_valid_for_direct_access_regex( $contents ); + } + } + + /** + * Checks if AST only contains structural code (safe for direct access). + * + * @since 1.9.0 + * + * @param array $ast The parsed AST nodes. + * @return bool True if the AST only contains structural code, false otherwise. + * @SuppressWarnings(PHPMD.NPathComplexity) + */ + private function is_ast_valid_for_direct_access( array $ast ) { + $safe_node_types = array( + Stmt\Nop::class, + Stmt\Namespace_::class, + Stmt\Use_::class, + Stmt\GroupUse::class, + Stmt\Class_::class, + Stmt\Interface_::class, + Stmt\Trait_::class, + Stmt\Enum_::class, + ); + + $has_assignments = false; + $has_returns = false; + + foreach ( $ast as $node ) { + $node_class = get_class( $node ); + + if ( in_array( $node_class, $safe_node_types, true ) ) { + if ( $node instanceof Stmt\Namespace_ && ! empty( $node->stmts ) ) { + if ( ! $this->is_ast_valid_for_direct_access( $node->stmts ) ) { + return false; + } + } + continue; + } + + if ( $node instanceof Stmt\Function_ ) { + return false; + } + + if ( $node instanceof Stmt\Return_ ) { + if ( $this->is_safe_return_expression( $node->expr ) ) { + $has_returns = true; + continue; + } + return false; + } + + if ( $node instanceof Stmt\Expression ) { + if ( $this->is_safe_expression( $node->expr ) ) { + continue; + } + if ( $this->is_asset_assignment( $node->expr ) ) { + $has_assignments = true; + continue; + } + return false; + } + + if ( $node instanceof Stmt\If_ ) { + if ( $this->is_safe_if_statement( $node ) ) { + continue; + } + return false; + } + + return false; + } + + if ( $has_assignments && $has_returns ) { + return true; + } + + return true; + } + + /** + * Checks if an expression is an asset file assignment (variable = array/string). + * + * @since 1.9.0 + * + * @param Expr $expr The expression to check. + * @return bool True if asset assignment, false otherwise. + */ + private function is_asset_assignment( $expr ) { + if ( ! ( $expr instanceof Expr\Assign ) ) { + return false; + } + + return $this->is_safe_expression( $expr->expr ); + } + + /** + * Checks if an expression is safe (doesn't execute code). + * + * @since 1.9.0 + * + * @param Expr|null $expr The expression to check. + * @return bool True if the expression is safe, false otherwise. + * @SuppressWarnings(PHPMD.NPathComplexity) + */ + private function is_safe_expression( $expr ) { + if ( null === $expr ) { + return true; + } + + if ( $this->is_safe_scalar( $expr ) ) { + return true; + } + + if ( $this->is_safe_encapsed_string( $expr ) ) { + return true; + } + + if ( $expr instanceof Expr\ConstFetch ) { + return true; + } + + if ( $this->is_safe_array( $expr ) ) { + return true; + } + + if ( $this->is_safe_function_call( $expr ) ) { + return true; + } + + if ( $this->is_safe_concat( $expr ) ) { + return true; + } + + if ( $this->is_unsafe_expression( $expr ) ) { + return false; + } + + return false; + } + + /** + * Checks if expression is a safe scalar value. + * + * @since 1.9.0 + * + * @param Expr $expr The expression to check. + * @return bool True if safe scalar, false otherwise. + */ + private function is_safe_scalar( $expr ) { + $class = get_class( $expr ); + return 'PhpParser\Node\Scalar\String_' === $class + || 'PhpParser\Node\Scalar\LNumber' === $class + || 'PhpParser\Node\Scalar\DNumber' === $class + || 'PhpParser\Node\Scalar\EncapsedStringPart' === $class; + } + + /** + * Checks if expression is a safe encapsed string. + * + * @since 1.9.0 + * + * @param Expr $expr The expression to check. + * @return bool True if safe encapsed string, false otherwise. + */ + private function is_safe_encapsed_string( $expr ) { + if ( 'PhpParser\Node\Scalar\Encapsed' !== get_class( $expr ) ) { + return false; + } + + // Type assertion: $expr is PhpParser\Node\Scalar\Encapsed which has a $parts property. + // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase,Generic.Commenting.DocComment.MissingShort + if ( ! isset( $expr->parts ) || ! is_array( $expr->parts ) ) { + return false; + } + + // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase + foreach ( $expr->parts as $part ) { + if ( ! $this->is_safe_expression( $part ) ) { + return false; + } + } + return true; + } + + /** + * Checks if expression is a safe array. + * + * @since 1.9.0 + * + * @param Expr $expr The expression to check. + * @return bool True if safe array, false otherwise. + */ + private function is_safe_array( $expr ) { + if ( ! ( $expr instanceof Expr\Array_ ) ) { + return false; + } + + foreach ( $expr->items as $item ) { + if ( null !== $item && null !== $item->value && ! $this->is_safe_expression( $item->value ) ) { + return false; + } + } + return true; + } + + /** + * Checks if expression is a safe function call. + * + * @since 1.9.0 + * + * @param Expr $expr The expression to check. + * @return bool True if safe function call, false otherwise. + */ + private function is_safe_function_call( $expr ) { + if ( ! ( $expr instanceof Expr\FuncCall ) ) { + return false; + } + + $function_name = $this->get_function_name( $expr ); + if ( null === $function_name || ! in_array( $function_name, $this->get_allowed_functions(), true ) ) { + return false; + } + + foreach ( $expr->args as $arg ) { + if ( null !== $arg->value && ! $this->is_safe_expression( $arg->value ) ) { + return false; + } + } + return true; + } + + /** + * Checks if expression is a safe concatenation. + * + * @since 1.9.0 + * + * @param Expr $expr The expression to check. + * @return bool True if safe concatenation, false otherwise. + */ + private function is_safe_concat( $expr ) { + if ( ! ( $expr instanceof Expr\BinaryOp\Concat ) ) { + return false; + } + + return $this->is_safe_expression( $expr->left ) && $this->is_safe_expression( $expr->right ); + } + + /** + * Checks if expression is unsafe (executes code). + * + * @since 1.9.0 + * + * @param Expr $expr The expression to check. + * @return bool True if unsafe, false otherwise. + */ + private function is_unsafe_expression( $expr ) { + return $expr instanceof Expr\Assign + || $expr instanceof Expr\AssignOp + || $expr instanceof Expr\MethodCall + || $expr instanceof Expr\StaticCall + || $expr instanceof Expr\Exit_; + } + + /** + * Checks if an If statement is safe (only contains safe function calls and returns). + * + * @since 1.9.0 + * + * @param Stmt\If_ $node The If statement node. + * @return bool True if safe, false otherwise. + * @SuppressWarnings(PHPMD.NPathComplexity) + */ + private function is_safe_if_statement( Stmt\If_ $node ) { + if ( ! $this->is_safe_condition( $node->cond ) ) { + return false; + } + + if ( empty( $node->stmts ) ) { + return true; + } + + foreach ( $node->stmts as $stmt ) { + if ( $stmt instanceof Stmt\Return_ ) { + if ( ! $this->is_safe_return_expression( $stmt->expr ) ) { + return false; + } + continue; + } + + if ( $stmt instanceof Stmt\If_ ) { + if ( ! $this->is_safe_if_statement( $stmt ) ) { + return false; + } + continue; + } + + return false; + } + + if ( ! empty( $node->elseifs ) ) { + foreach ( $node->elseifs as $elseif ) { + if ( ! $this->is_safe_elseif_statement( $elseif ) ) { + return false; + } + } + } + + if ( null !== $node->else ) { + foreach ( $node->else->stmts as $stmt ) { + if ( $stmt instanceof Stmt\Return_ ) { + if ( ! $this->is_safe_return_expression( $stmt->expr ) ) { + return false; + } + continue; + } + + if ( $stmt instanceof Stmt\If_ ) { + if ( ! $this->is_safe_if_statement( $stmt ) ) { + return false; + } + continue; + } + + return false; + } + } + + return true; + } + + /** + * Checks if an ElseIf statement is safe. + * + * @since 1.9.0 + * + * @param Stmt\ElseIf_ $node The ElseIf statement node. + * @return bool True if safe, false otherwise. + */ + private function is_safe_elseif_statement( Stmt\ElseIf_ $node ) { + if ( ! $this->is_safe_condition( $node->cond ) ) { + return false; + } + + if ( empty( $node->stmts ) ) { + return true; + } + + foreach ( $node->stmts as $stmt ) { + if ( $stmt instanceof Stmt\Return_ ) { + if ( ! $this->is_safe_return_expression( $stmt->expr ) ) { + return false; + } + continue; + } + + if ( $stmt instanceof Stmt\If_ ) { + if ( ! $this->is_safe_if_statement( $stmt ) ) { + return false; + } + continue; + } + + return false; + } + + return true; + } + + /** + * Checks if a condition expression is safe (only safe function calls). + * + * @since 1.9.0 + * + * @param Expr $cond The condition expression. + * @return bool True if safe, false otherwise. + */ + private function is_safe_condition( $cond ) { + if ( $cond instanceof Expr\BooleanNot ) { + return $this->is_safe_condition( $cond->expr ); + } + + if ( $cond instanceof Expr\BinaryOp\BooleanAnd || $cond instanceof Expr\BinaryOp\BooleanOr ) { + return $this->is_safe_condition( $cond->left ) && $this->is_safe_condition( $cond->right ); + } + + if ( $cond instanceof Expr\FuncCall ) { + $function_name = $this->get_function_name( $cond ); + return null !== $function_name && in_array( $function_name, $this->get_allowed_functions(), true ); + } + + return false; + } + + /** + * Checks if a return expression is safe. + * + * @since 1.9.0 + * + * @param Expr|null $expr The expression to check. + * @return bool True if the return expression is safe, false otherwise. + */ + private function is_safe_return_expression( $expr ) { + return $this->is_safe_expression( $expr ); + } + + /** + * Gets the function name from a function call expression. + * + * @since 1.9.0 + * + * @param Expr\FuncCall $node The function call node. + * @return string|null The function name, or null if not found. + */ + private function get_function_name( Expr\FuncCall $node ) { + if ( $node->name instanceof Node\Name ) { + return $node->name->toString(); + } + return null; + } + + /** + * Gets the list of allowed functions that don't require guards. + * + * @since 1.9.0 + * + * @return array List of allowed function names. + */ + private function get_allowed_functions() { + return array( + 'class_exists', + 'function_exists', + 'interface_exists', + 'trait_exists', + 'defined', + ); + } + + /** + * Fallback method using regex for files that can't be parsed. + * + * @since 1.9.0 + * + * @param string $contents The file contents. + * @return bool True if the file is safe for direct access, false otherwise. + */ + private function is_valid_for_direct_access_regex( $contents ) { $contents = $this->clean_file_contents( $contents ); if ( $this->is_asset_file( $contents ) ) { diff --git a/phpstan.neon.dist b/phpstan.neon.dist index a6885d2fb..bbf7bf8bf 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -50,3 +50,51 @@ parameters: - message: '/^Instanceof between PhpParser\\Node\\Expr\\AssignOp\\Concat and PhpParser\\Node\\Expr\\AssignOp will always evaluate to true\.$/' path: includes/Scanner/PHP_Parser.php + - + message: '/^Access to property \$parts on .*\.$/' + path: includes/Checker/Checks/Plugin_Repo/Direct_File_Access_Check.php + - + message: '/^Undefined property.*\$parts.*\.$/' + path: includes/Checker/Checks/Plugin_Repo/Direct_File_Access_Check.php + - + message: '/^Access to property \$value on PhpParser\\Node\\Arg\.$/' + path: includes/Checker/Checks/Plugin_Repo/Direct_File_Access_Check.php + - + message: '/^Access to property \$value on PhpParser\\Node\\Scalar\\String_\.$/' + path: includes/Checker/Checks/Plugin_Repo/Direct_File_Access_Check.php + - + message: '/^Undefined property.*\$value.*\.$/' + path: includes/Checker/Checks/Plugin_Repo/Direct_File_Access_Check.php + - + message: '/^Access to property \$left on .*\.$/' + path: includes/Checker/Checks/Plugin_Repo/Direct_File_Access_Check.php + - + message: '/^Access to property \$right on .*\.$/' + path: includes/Checker/Checks/Plugin_Repo/Direct_File_Access_Check.php + - + message: '/^Access to property \$expr on .*\.$/' + path: includes/Checker/Checks/Plugin_Repo/Direct_File_Access_Check.php + - + message: '/^Access to property \$name on .*\.$/' + path: includes/Checker/Checks/Plugin_Repo/Direct_File_Access_Check.php + - + message: '/^Access to property \$args on .*\.$/' + path: includes/Checker/Checks/Plugin_Repo/Direct_File_Access_Check.php + - + message: '/^Undefined property.*\$left.*\.$/' + path: includes/Checker/Checks/Plugin_Repo/Direct_File_Access_Check.php + - + message: '/^Undefined property.*\$right.*\.$/' + path: includes/Checker/Checks/Plugin_Repo/Direct_File_Access_Check.php + - + message: '/^Undefined property.*\$expr.*\.$/' + path: includes/Checker/Checks/Plugin_Repo/Direct_File_Access_Check.php + - + message: '/^Undefined property.*\$name.*\.$/' + path: includes/Checker/Checks/Plugin_Repo/Direct_File_Access_Check.php + - + message: '/^Undefined property.*\$args.*\.$/' + path: includes/Checker/Checks/Plugin_Repo/Direct_File_Access_Check.php + - + message: '/^Property PhpParser\\Node\\Scalar\\Encapsed::\$parts.*is not nullable\.$/' + path: includes/Checker/Checks/Plugin_Repo/Direct_File_Access_Check.php