Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
use CleantalkSP\Common\Scanner\HeuristicAnalyser\Modules\Tokens;
use CleantalkSP\Common\Scanner\HeuristicAnalyser\Modules\Transformations;
use CleantalkSP\Common\Scanner\HeuristicAnalyser\Modules\Variables;
use CleantalkSP\Common\Scanner\HeuristicAnalyser\Modules\PHPCodeValidator;

/**
* Class Heuristic
Expand Down Expand Up @@ -178,6 +179,11 @@ class HeuristicAnalyser
*/
private $mathematics;

/*
* @var PHPCodeValidator
*/
private $php_code_validator;

/**
* Heuristic constructor.
* Getting common info about file|text and it's content
Expand Down Expand Up @@ -229,6 +235,8 @@ public function __construct($input, $self = null)
$this->includes = new Includes($this->tokens, $this->variables, $this->curr_dir, $this->is_text);
$this->evaluations = new Evaluations($this->tokens, $this->variables, $this->includes, $this->sqls);
$this->code_style = new CodeStyle($this->tokens);
$this->php_code_validator = new PHPCodeValidator($this->tokens);


if ( isset($input['path']) && version_compare(PHP_VERSION, '8.1', '>=') && extension_loaded('mbstring') ) {
// Do not run entropy analysis on included constructs
Expand Down Expand Up @@ -276,12 +284,17 @@ private function checkFileSize($file_size)
*
* @return void
* @psalm-suppress PossiblyUnusedMethod
* @throws HeuristicScannerException
*/
public function processContent()
{
// Skip files does not contain PHP code
if ( $this->extension !== 'php' && ! $this->code_style->hasPHPOpenTags() ) {
return;
if ( $this->extension !== 'php' && !$this->php_code_validator->hasCorrectPHPOpenTags() ) {
throw new HeuristicScannerException('NOT_PHP_CODE');
}

if (!$this->php_code_validator->isValidPHPCode()) {
throw new HeuristicScannerException('NOT_VALID_PHP_CODE');
}

// Analysing code style
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -224,32 +224,6 @@ public function detectBadLines()
return $result;
}

/**
* Check if file contains PHP open tags ("<\?php" or `<\?`).
* @return bool
*/
public function hasPHPOpenTags()
{
foreach ( $this->tokens as $_token => $content ) {
if ( isset($content[0]) && isset($this->tokens->next1[0]) ) {
if ( $content[0] === 'T_OPEN_TAG' ) {
//check if open tag is short
$is_short = isset($content[1]) && $content[1] === '<?';
if (
// should be whitespaces after tag
$is_short && $this->tokens->next1[0] === 'T_WHITESPACE' ||
// should be whitespaces or variable after tag
!$is_short && in_array($this->tokens->next1[0], array('T_WHITESPACE', 'T_VARIABLE'))
) {
return true;
}
}
}
}

return false;
}

/**
* Count special service chars like <>!= etc. and return the proportion to the total chars count.
* Uses $this->tokens as content source.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,243 @@
<?php

namespace CleantalkSP\Common\Scanner\HeuristicAnalyser\Modules;

class PHPCodeValidator
{
/**
* The result of checks
*
* @var array
*/
public $check_list_result;

/**
* The tokens to be validated.
*
* @var Tokens
*/
private $tokens;

/**
* PHPCodeValidator constructor.
*
* @param Tokens $tokens The tokens to be validated.
*/
public function __construct($tokens)
{
$this->tokens = $tokens;
}

/**
* Checks if the PHP code is valid.
*
* @return bool Returns true if the PHP code is valid, false otherwise.
* @psalm-suppress PossiblyUnusedMethod
*/
public function isValidPHPCode()
{
$this->hasCorrectPHPOpenTags();
$this->checkBraces();
$this->checkBrackets();
$this->checkParentheses();
$this->checkSingleQuotes();
$this->checkDoubleQuotes();
$this->checkDigitsStartedVariables();
return empty($this->check_list_result);
}

/**
* Checks if the count of left and right braces and brackets are equal.
*
* @return bool Returns true if the count is equal, false otherwise.
*/
private function checkBraces()
{
$braces_l_count = 0;
$braces_r_count = 0;

foreach ( $this->tokens as $token ) {
if ( $token[0] === '__SERV' ) {
if ( $token[1] === '(' ) {
$braces_l_count++;
}
if ( $token[1] === ')' ) {
$braces_r_count++;
}
}
}

if ( $braces_l_count !== $braces_r_count ) {
$this->check_list_result[__FUNCTION__] = 'Braces () count is not equal ' . $braces_l_count . ' != ' . $braces_r_count;
return false;
}

return true;
}

/**
* Checks if the count of left and right brackets are equal.
*
* @return bool Returns true if the count is equal, false otherwise.
*/
private function checkBrackets()
{
$brackets_l_count = 0;
$brackets_r_count = 0;

foreach ( $this->tokens as $token ) {
if ( $token[0] === '__SERV' ) {
if ( $token[1] === '[' ) {
$brackets_l_count++;
}
if ( $token[1] === ']' ) {
$brackets_r_count++;
}
}
}

if ( $brackets_l_count !== $brackets_r_count ) {
$this->check_list_result[__FUNCTION__] = 'Brackets [] count is not equal';
return false;
}

return true;
}

/**
* Checks if the count of left and right parentheses are equal.
*
* @return bool Returns true if the count is equal, false otherwise.
*/
private function checkParentheses()
{
$parentheses_l_count = 0;
$parentheses_r_count = 0;

/**
* init opening and closing tokens, key is token type, value is string to search
*/
$opening_tokens = array(
'__SERV' => '{',
'T_CURLY_OPEN' => '{',
'T_DOLLAR_OPEN_CURLY_BRACES' => '${',
'T_STRING_VARNAME' => '{'
);

$closing_tokens = array(
'__SERV' => '}',
'T_STRING_VARNAME' => '}'
);

foreach ( $this->tokens as $token ) {
if ( isset($opening_tokens[$token[0]]) &&
$token[1] === $opening_tokens[$token[0]] ) {
$parentheses_l_count++;
}
if ( isset($closing_tokens[$token[0]]) &&
$token[1] === $closing_tokens[$token[0]] ) {
$parentheses_r_count++;
}
}

if ( $parentheses_l_count !== $parentheses_r_count ) {
$this->check_list_result[__FUNCTION__] = 'Parentheses {} count is not equal';
return false;
}

return true;
}

/**
* Checks if the count of single quotes are even.
*
* @return bool Returns true if the count is even, false otherwise.
*/
private function checkSingleQuotes()
{
$single_quotes_count = 0;

foreach ( $this->tokens as $token ) {
if ( $token[0] === '__SERV' ) {
if ( $token[1] === "'" ) {
$single_quotes_count++;
}
}
}

if ( $single_quotes_count % 2 !== 0 ) {
$this->check_list_result[__FUNCTION__] = 'Single quotes count is not even';
return false;
}
return true;
}

/**
* Checks if the count of double quotes are even.
*
* @return bool Returns true if the count is even, false otherwise.
*/
private function checkDoubleQuotes()
{
$double_quotes_count = 0;

foreach ( $this->tokens as $token ) {
if ( $token[0] === '__SERV' ) {
if ( $token[1] === '"' ) {
$double_quotes_count++;
}
}
}

if ( $double_quotes_count % 2 !== 0 ) {
$this->check_list_result[__FUNCTION__] = 'Double quotes count is not even';
return false;
}
return true;
}

/**
* Checks if variables contain digits.
*
* @return bool Returns true if no variables contain digits, false otherwise.
*/
private function checkDigitsStartedVariables()
{
foreach ( $this->tokens as $token ) {
if ( $token[0] === 'T_VARIABLE' ) {
if ( preg_match('/^\$\d.+/', $token[1]) ) {
$this->check_list_result[__FUNCTION__] = 'Variable starts with digits [' . $token[1] . ']';
return false;
}
}
}
return true;
}

/**
* Checks if the file does not contain PHP open tags ("<\?php" or `<\?`).
*
* @return bool Returns true if the file does not contain PHP open tags, false otherwise.
*/
public function hasCorrectPHPOpenTags()
{
foreach ( $this->tokens as $_token => $content ) {
if ( isset($content[0]) && isset($this->tokens->next1[0]) ) {
if ( $content[0] === 'T_OPEN_TAG' ) {
//check if open tag is short
$is_short = isset($content[1]) && $content[1] === '<?';
if ( $is_short ) {
if ( $this->tokens->next1[0] !== 'T_WHITESPACE' ) {
$this->check_list_result[__FUNCTION__] = 'PHP open tags are not valid';
return false;
}
} else {
return true;
}
}
}
}

return false;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,51 @@ class Tokens implements \Iterator, \ArrayAccess, \Countable
*/
private $groups;

/**
* @var Token|null $prev4 The fourth previous token in the iteration.
*/
public $prev4;

/**
* @var Token|null $prev3 The third previous token in the iteration.
*/
public $prev3;

/**
* @var Token|null $prev2 The second previous token in the iteration.
*/
public $prev2;

/**
* @var Token|null $prev1 The first previous token in the iteration.
*/
public $prev1;

/**
* @var Token|null $current The current token in the iteration.
*/
public $current;

/**
* @var Token|null $next1 The first next token in the iteration.
*/
public $next1;

/**
* @var Token|null $next2 The second next token in the iteration.
*/
public $next2;

/**
* @var Token|null $next3 The third next token in the iteration.
*/
public $next3;

/**
* @var Token|null $next4 The fourth next token in the iteration.
*/
public $next4;

/**
* @param $content
* @psalm-suppress PossiblyUnusedMethod
Expand Down Expand Up @@ -646,6 +691,7 @@ public function count()
*
* @return Token|null
* @psalm-suppress PossiblyUnusedReturnValue
* @psalm-suppress PossiblyUnusedMethod
*/
public function __get($name)
{
Expand Down Expand Up @@ -679,6 +725,7 @@ public function __get($name)
/**
* @param $name
* @param $value
* @psalm-suppress PossiblyUnusedMethod
*/
public function __set($name, $value)
{
Expand Down
Loading