2016-08-24 04:52:58 +00:00
|
|
|
<?php
|
|
|
|
|
|
|
|
/**
|
|
|
|
* A version of the abuse filter parser that separates parsing the filter and
|
|
|
|
* evaluating it into different passes, allowing the parse tree to be cached.
|
|
|
|
*
|
|
|
|
* @file
|
2019-10-09 10:37:38 +00:00
|
|
|
* @phan-file-suppress PhanPossiblyInfiniteRecursionSameParams Recursion controlled by class props
|
2016-08-24 04:52:58 +00:00
|
|
|
*/
|
|
|
|
|
2019-08-21 10:04:10 +00:00
|
|
|
use Psr\Log\LoggerInterface;
|
2019-08-21 09:01:50 +00:00
|
|
|
|
2016-08-24 04:52:58 +00:00
|
|
|
/**
|
|
|
|
* A parser that transforms the text of the filter into a parse tree.
|
|
|
|
*/
|
2019-11-30 12:13:07 +00:00
|
|
|
class AFPTreeParser extends AFPTransitionBase {
|
2019-08-21 09:01:50 +00:00
|
|
|
/**
|
|
|
|
* @var array[] Contains the AFPTokens for the code being parsed
|
|
|
|
*/
|
2016-08-24 04:52:58 +00:00
|
|
|
public $mTokens;
|
2019-08-21 09:01:50 +00:00
|
|
|
/**
|
|
|
|
* @var AFPToken The current token
|
|
|
|
*/
|
|
|
|
public $mCur;
|
|
|
|
/**
|
|
|
|
* @var string|null The ID of the filter being parsed, if available. Can also be "global-$ID"
|
|
|
|
*/
|
|
|
|
protected $mFilter;
|
2016-08-24 04:52:58 +00:00
|
|
|
|
2020-01-21 11:13:11 +00:00
|
|
|
public const CACHE_VERSION = 2;
|
2016-08-24 04:52:58 +00:00
|
|
|
|
|
|
|
/**
|
2019-08-21 10:04:10 +00:00
|
|
|
* @var BagOStuff Used to cache tokens
|
2016-08-24 04:52:58 +00:00
|
|
|
*/
|
2019-08-21 10:04:10 +00:00
|
|
|
protected $cache;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @var LoggerInterface Used for debugging
|
|
|
|
*/
|
|
|
|
protected $logger;
|
|
|
|
|
2019-09-16 16:53:36 +00:00
|
|
|
/**
|
|
|
|
* @var IBufferingStatsdDataFactory
|
|
|
|
*/
|
|
|
|
protected $statsd;
|
|
|
|
|
2019-08-21 10:04:10 +00:00
|
|
|
/**
|
|
|
|
* @param BagOStuff $cache
|
|
|
|
* @param LoggerInterface $logger Used for debugging
|
2019-09-16 16:53:36 +00:00
|
|
|
* @param IBufferingStatsdDataFactory $statsd
|
2019-08-21 10:04:10 +00:00
|
|
|
*/
|
2019-09-16 16:53:36 +00:00
|
|
|
public function __construct(
|
|
|
|
BagOStuff $cache,
|
|
|
|
LoggerInterface $logger,
|
|
|
|
IBufferingStatsdDataFactory $statsd
|
|
|
|
) {
|
2019-08-21 10:04:10 +00:00
|
|
|
$this->cache = $cache;
|
|
|
|
$this->logger = $logger;
|
2019-09-16 16:53:36 +00:00
|
|
|
$this->statsd = $statsd;
|
2016-08-24 04:52:58 +00:00
|
|
|
$this->resetState();
|
|
|
|
}
|
|
|
|
|
2019-08-21 09:01:50 +00:00
|
|
|
/**
|
|
|
|
* @param string $filter
|
|
|
|
*/
|
|
|
|
public function setFilter( $filter ) {
|
|
|
|
$this->mFilter = $filter;
|
|
|
|
}
|
|
|
|
|
2018-04-04 21:14:25 +00:00
|
|
|
/**
|
|
|
|
* Resets the state
|
|
|
|
*/
|
2016-08-24 04:52:58 +00:00
|
|
|
public function resetState() {
|
2017-06-15 14:23:34 +00:00
|
|
|
$this->mTokens = [];
|
2016-08-24 04:52:58 +00:00
|
|
|
$this->mPos = 0;
|
2019-08-21 09:01:50 +00:00
|
|
|
$this->mFilter = null;
|
2016-08-24 04:52:58 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Advances the parser to the next token in the filter code.
|
|
|
|
*/
|
|
|
|
protected function move() {
|
|
|
|
list( $this->mCur, $this->mPos ) = $this->mTokens[$this->mPos];
|
|
|
|
}
|
|
|
|
|
2019-08-06 18:59:45 +00:00
|
|
|
/**
|
|
|
|
* Get the next token. This is similar to move() but doesn't change class members,
|
|
|
|
* allowing to look ahead without rolling back the state.
|
|
|
|
*
|
|
|
|
* @return AFPToken
|
|
|
|
*/
|
|
|
|
protected function getNextToken() {
|
|
|
|
return $this->mTokens[$this->mPos][0];
|
|
|
|
}
|
|
|
|
|
2016-08-24 04:52:58 +00:00
|
|
|
/**
|
|
|
|
* getState() function allows parser state to be rollbacked to several tokens
|
|
|
|
* back.
|
|
|
|
*
|
|
|
|
* @return AFPParserState
|
|
|
|
*/
|
|
|
|
protected function getState() {
|
|
|
|
return new AFPParserState( $this->mCur, $this->mPos );
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* setState() function allows parser state to be rollbacked to several tokens
|
|
|
|
* back.
|
|
|
|
*
|
|
|
|
* @param AFPParserState $state
|
|
|
|
*/
|
|
|
|
protected function setState( AFPParserState $state ) {
|
|
|
|
$this->mCur = $state->token;
|
|
|
|
$this->mPos = $state->pos;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Parse the supplied filter source code into a tree.
|
|
|
|
*
|
|
|
|
* @param string $code
|
|
|
|
* @throws AFPUserVisibleException
|
2019-08-24 09:48:20 +00:00
|
|
|
* @return AFPSyntaxTree
|
2016-08-24 04:52:58 +00:00
|
|
|
*/
|
2019-08-24 09:48:20 +00:00
|
|
|
public function parse( $code ) : AFPSyntaxTree {
|
2019-09-02 08:25:56 +00:00
|
|
|
$tokenizer = new AbuseFilterTokenizer( $this->cache, $this->logger );
|
2019-08-21 10:04:10 +00:00
|
|
|
$this->mTokens = $tokenizer->getTokens( $code );
|
2016-08-24 04:52:58 +00:00
|
|
|
$this->mPos = 0;
|
|
|
|
|
2019-08-24 09:48:20 +00:00
|
|
|
return $this->buildSyntaxTree();
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @return AFPSyntaxTree
|
|
|
|
*/
|
|
|
|
public function buildSyntaxTree() : AFPSyntaxTree {
|
2019-09-16 16:53:36 +00:00
|
|
|
$startTime = microtime( true );
|
2019-08-24 09:48:20 +00:00
|
|
|
$root = $this->doLevelEntry();
|
2019-09-16 16:53:36 +00:00
|
|
|
$this->statsd->timing( 'abusefilter_cachingParser_buildtree', microtime( true ) - $startTime );
|
2019-09-09 17:20:47 +00:00
|
|
|
return new AFPSyntaxTree( $root );
|
2016-08-24 04:52:58 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/* Levels */
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Handles unexpected characters after the expression.
|
2019-08-24 09:48:20 +00:00
|
|
|
* @return AFPTreeNode|null Null only if no statements
|
2016-08-24 04:52:58 +00:00
|
|
|
* @throws AFPUserVisibleException
|
|
|
|
*/
|
|
|
|
protected function doLevelEntry() {
|
|
|
|
$result = $this->doLevelSemicolon();
|
|
|
|
|
2018-08-26 08:34:42 +00:00
|
|
|
if ( $this->mCur->type !== AFPToken::TNONE ) {
|
2016-08-24 04:52:58 +00:00
|
|
|
throw new AFPUserVisibleException(
|
|
|
|
'unexpectedatend',
|
|
|
|
$this->mPos, [ $this->mCur->type ]
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
return $result;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Handles the semicolon operator.
|
|
|
|
*
|
|
|
|
* @return AFPTreeNode|null
|
|
|
|
*/
|
|
|
|
protected function doLevelSemicolon() {
|
|
|
|
$statements = [];
|
|
|
|
|
|
|
|
do {
|
|
|
|
$this->move();
|
|
|
|
$position = $this->mPos;
|
|
|
|
|
2019-08-12 16:16:32 +00:00
|
|
|
if (
|
|
|
|
$this->mCur->type === AFPToken::TNONE ||
|
2018-08-22 14:33:35 +00:00
|
|
|
( $this->mCur->type === AFPToken::TBRACE && $this->mCur->value == ')' )
|
|
|
|
) {
|
2019-08-12 16:16:32 +00:00
|
|
|
// Handle special cases which the other parser handled in doLevelAtom
|
2016-08-24 04:52:58 +00:00
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Allow empty statements.
|
2018-08-26 08:34:42 +00:00
|
|
|
if ( $this->mCur->type === AFPToken::TSTATEMENTSEPARATOR ) {
|
2016-08-24 04:52:58 +00:00
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
$statements[] = $this->doLevelSet();
|
|
|
|
$position = $this->mPos;
|
2018-08-26 08:34:42 +00:00
|
|
|
} while ( $this->mCur->type === AFPToken::TSTATEMENTSEPARATOR );
|
2016-08-24 04:52:58 +00:00
|
|
|
|
|
|
|
// Flatten the tree if possible.
|
2018-08-26 08:34:42 +00:00
|
|
|
if ( count( $statements ) === 0 ) {
|
2016-08-24 04:52:58 +00:00
|
|
|
return null;
|
2018-08-26 08:34:42 +00:00
|
|
|
} elseif ( count( $statements ) === 1 ) {
|
2016-08-24 04:52:58 +00:00
|
|
|
return $statements[0];
|
|
|
|
} else {
|
|
|
|
return new AFPTreeNode( AFPTreeNode::SEMICOLON, $statements, $position );
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Handles variable assignment.
|
|
|
|
*
|
|
|
|
* @return AFPTreeNode
|
|
|
|
* @throws AFPUserVisibleException
|
|
|
|
*/
|
|
|
|
protected function doLevelSet() {
|
2018-08-26 08:34:42 +00:00
|
|
|
if ( $this->mCur->type === AFPToken::TID ) {
|
2019-12-07 17:20:10 +00:00
|
|
|
$varname = (string)$this->mCur->value;
|
2016-08-24 04:52:58 +00:00
|
|
|
|
|
|
|
// Speculatively parse the assignment statement assuming it can
|
|
|
|
// potentially be an assignment, but roll back if it isn't.
|
2019-09-09 17:20:47 +00:00
|
|
|
// @todo Use $this->getNextToken for clearer code
|
2016-08-24 04:52:58 +00:00
|
|
|
$initialState = $this->getState();
|
|
|
|
$this->move();
|
|
|
|
|
2018-08-26 08:34:42 +00:00
|
|
|
if ( $this->mCur->type === AFPToken::TOP && $this->mCur->value === ':=' ) {
|
2016-08-24 04:52:58 +00:00
|
|
|
$position = $this->mPos;
|
|
|
|
$this->move();
|
|
|
|
$value = $this->doLevelSet();
|
|
|
|
|
|
|
|
return new AFPTreeNode( AFPTreeNode::ASSIGNMENT, [ $varname, $value ], $position );
|
|
|
|
}
|
|
|
|
|
2018-08-26 08:34:42 +00:00
|
|
|
if ( $this->mCur->type === AFPToken::TSQUAREBRACKET && $this->mCur->value === '[' ) {
|
2016-08-24 04:52:58 +00:00
|
|
|
$this->move();
|
|
|
|
|
2018-08-26 08:34:42 +00:00
|
|
|
if ( $this->mCur->type === AFPToken::TSQUAREBRACKET && $this->mCur->value === ']' ) {
|
2016-08-24 04:52:58 +00:00
|
|
|
$index = 'append';
|
|
|
|
} else {
|
|
|
|
// Parse index offset.
|
|
|
|
$this->setState( $initialState );
|
|
|
|
$this->move();
|
|
|
|
$index = $this->doLevelSemicolon();
|
2018-08-26 08:34:42 +00:00
|
|
|
if ( !( $this->mCur->type === AFPToken::TSQUAREBRACKET && $this->mCur->value === ']' ) ) {
|
2016-08-24 04:52:58 +00:00
|
|
|
throw new AFPUserVisibleException( 'expectednotfound', $this->mPos,
|
|
|
|
[ ']', $this->mCur->type, $this->mCur->value ] );
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
$this->move();
|
2018-08-26 08:34:42 +00:00
|
|
|
if ( $this->mCur->type === AFPToken::TOP && $this->mCur->value === ':=' ) {
|
2016-08-24 04:52:58 +00:00
|
|
|
$position = $this->mPos;
|
|
|
|
$this->move();
|
|
|
|
$value = $this->doLevelSet();
|
|
|
|
if ( $index === 'append' ) {
|
|
|
|
return new AFPTreeNode(
|
2018-04-16 15:37:10 +00:00
|
|
|
AFPTreeNode::ARRAY_APPEND, [ $varname, $value ], $position );
|
2016-08-24 04:52:58 +00:00
|
|
|
} else {
|
|
|
|
return new AFPTreeNode(
|
|
|
|
AFPTreeNode::INDEX_ASSIGNMENT,
|
|
|
|
[ $varname, $index, $value ],
|
|
|
|
$position
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// If we reached this point, we did not find an assignment. Roll back
|
|
|
|
// and assume this was just a literal.
|
|
|
|
$this->setState( $initialState );
|
|
|
|
}
|
|
|
|
|
|
|
|
return $this->doLevelConditions();
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Handles ternary operator and if-then-else-end.
|
|
|
|
*
|
|
|
|
* @return AFPTreeNode
|
|
|
|
* @throws AFPUserVisibleException
|
|
|
|
*/
|
|
|
|
protected function doLevelConditions() {
|
2018-08-26 08:34:42 +00:00
|
|
|
if ( $this->mCur->type === AFPToken::TKEYWORD && $this->mCur->value === 'if' ) {
|
2016-08-24 04:52:58 +00:00
|
|
|
$position = $this->mPos;
|
|
|
|
$this->move();
|
|
|
|
$condition = $this->doLevelBoolOps();
|
|
|
|
|
2018-08-26 08:34:42 +00:00
|
|
|
if ( !( $this->mCur->type === AFPToken::TKEYWORD && $this->mCur->value === 'then' ) ) {
|
2016-08-24 04:52:58 +00:00
|
|
|
throw new AFPUserVisibleException( 'expectednotfound',
|
|
|
|
$this->mPos,
|
|
|
|
[
|
|
|
|
'then',
|
|
|
|
$this->mCur->type,
|
|
|
|
$this->mCur->value
|
|
|
|
]
|
|
|
|
);
|
|
|
|
}
|
|
|
|
$this->move();
|
|
|
|
|
|
|
|
$valueIfTrue = $this->doLevelConditions();
|
|
|
|
|
2019-08-20 16:19:31 +00:00
|
|
|
if ( $this->mCur->type === AFPToken::TKEYWORD && $this->mCur->value === 'else' ) {
|
|
|
|
$this->move();
|
|
|
|
$valueIfFalse = $this->doLevelConditions();
|
|
|
|
} else {
|
|
|
|
$valueIfFalse = null;
|
2016-08-24 04:52:58 +00:00
|
|
|
}
|
|
|
|
|
2018-08-26 08:34:42 +00:00
|
|
|
if ( !( $this->mCur->type === AFPToken::TKEYWORD && $this->mCur->value === 'end' ) ) {
|
2016-08-24 04:52:58 +00:00
|
|
|
throw new AFPUserVisibleException( 'expectednotfound',
|
|
|
|
$this->mPos,
|
|
|
|
[
|
|
|
|
'end',
|
|
|
|
$this->mCur->type,
|
|
|
|
$this->mCur->value
|
|
|
|
]
|
|
|
|
);
|
|
|
|
}
|
|
|
|
$this->move();
|
|
|
|
|
|
|
|
return new AFPTreeNode(
|
|
|
|
AFPTreeNode::CONDITIONAL,
|
|
|
|
[ $condition, $valueIfTrue, $valueIfFalse ],
|
|
|
|
$position
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
$condition = $this->doLevelBoolOps();
|
2018-08-26 08:34:42 +00:00
|
|
|
if ( $this->mCur->type === AFPToken::TOP && $this->mCur->value === '?' ) {
|
2016-08-24 04:52:58 +00:00
|
|
|
$position = $this->mPos;
|
|
|
|
$this->move();
|
|
|
|
|
|
|
|
$valueIfTrue = $this->doLevelConditions();
|
2018-08-26 08:34:42 +00:00
|
|
|
if ( !( $this->mCur->type === AFPToken::TOP && $this->mCur->value === ':' ) ) {
|
2016-08-24 04:52:58 +00:00
|
|
|
throw new AFPUserVisibleException( 'expectednotfound',
|
|
|
|
$this->mPos,
|
|
|
|
[
|
|
|
|
':',
|
|
|
|
$this->mCur->type,
|
|
|
|
$this->mCur->value
|
|
|
|
]
|
|
|
|
);
|
|
|
|
}
|
|
|
|
$this->move();
|
|
|
|
|
|
|
|
$valueIfFalse = $this->doLevelConditions();
|
|
|
|
return new AFPTreeNode(
|
|
|
|
AFPTreeNode::CONDITIONAL,
|
|
|
|
[ $condition, $valueIfTrue, $valueIfFalse ],
|
|
|
|
$position
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
return $condition;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Handles logic operators.
|
|
|
|
*
|
|
|
|
* @return AFPTreeNode
|
|
|
|
*/
|
|
|
|
protected function doLevelBoolOps() {
|
|
|
|
$leftOperand = $this->doLevelCompares();
|
|
|
|
$ops = [ '&', '|', '^' ];
|
2018-08-26 08:34:42 +00:00
|
|
|
while ( $this->mCur->type === AFPToken::TOP && in_array( $this->mCur->value, $ops ) ) {
|
2016-08-24 04:52:58 +00:00
|
|
|
$op = $this->mCur->value;
|
|
|
|
$position = $this->mPos;
|
|
|
|
$this->move();
|
|
|
|
|
2016-11-07 01:28:49 +00:00
|
|
|
$rightOperand = $this->doLevelCompares();
|
2016-08-24 04:52:58 +00:00
|
|
|
|
2016-11-07 01:28:49 +00:00
|
|
|
$leftOperand = new AFPTreeNode(
|
2016-08-24 04:52:58 +00:00
|
|
|
AFPTreeNode::LOGIC,
|
|
|
|
[ $op, $leftOperand, $rightOperand ],
|
|
|
|
$position
|
|
|
|
);
|
|
|
|
}
|
|
|
|
return $leftOperand;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Handles comparison operators.
|
|
|
|
*
|
|
|
|
* @return AFPTreeNode
|
|
|
|
*/
|
|
|
|
protected function doLevelCompares() {
|
|
|
|
$leftOperand = $this->doLevelSumRels();
|
2019-03-21 15:48:35 +00:00
|
|
|
$equalityOps = [ '==', '===', '!=', '!==', '=' ];
|
|
|
|
$orderOps = [ '<', '>', '<=', '>=' ];
|
|
|
|
// Only allow either a single operation, or a combination of a single equalityOps and a single
|
|
|
|
// orderOps. This resembles what PHP does, and allows `a < b == c` while rejecting `a < b < c`
|
|
|
|
$allowedOps = array_merge( $equalityOps, $orderOps );
|
|
|
|
while ( $this->mCur->type === AFPToken::TOP && in_array( $this->mCur->value, $allowedOps ) ) {
|
2016-08-24 04:52:58 +00:00
|
|
|
$op = $this->mCur->value;
|
2019-03-21 15:48:35 +00:00
|
|
|
$allowedOps = in_array( $op, $equalityOps ) ?
|
|
|
|
array_diff( $allowedOps, $equalityOps ) :
|
|
|
|
array_diff( $allowedOps, $orderOps );
|
2016-08-24 04:52:58 +00:00
|
|
|
$position = $this->mPos;
|
|
|
|
$this->move();
|
|
|
|
$rightOperand = $this->doLevelSumRels();
|
|
|
|
$leftOperand = new AFPTreeNode(
|
|
|
|
AFPTreeNode::COMPARE,
|
|
|
|
[ $op, $leftOperand, $rightOperand ],
|
|
|
|
$position
|
|
|
|
);
|
|
|
|
}
|
|
|
|
return $leftOperand;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Handle addition and subtraction.
|
|
|
|
*
|
|
|
|
* @return AFPTreeNode
|
|
|
|
*/
|
|
|
|
protected function doLevelSumRels() {
|
|
|
|
$leftOperand = $this->doLevelMulRels();
|
|
|
|
$ops = [ '+', '-' ];
|
2018-08-26 08:34:42 +00:00
|
|
|
while ( $this->mCur->type === AFPToken::TOP && in_array( $this->mCur->value, $ops ) ) {
|
2016-08-24 04:52:58 +00:00
|
|
|
$op = $this->mCur->value;
|
|
|
|
$position = $this->mPos;
|
|
|
|
$this->move();
|
|
|
|
$rightOperand = $this->doLevelMulRels();
|
|
|
|
$leftOperand = new AFPTreeNode(
|
|
|
|
AFPTreeNode::SUM_REL,
|
|
|
|
[ $op, $leftOperand, $rightOperand ],
|
|
|
|
$position
|
|
|
|
);
|
|
|
|
}
|
|
|
|
return $leftOperand;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Handles multiplication and division.
|
|
|
|
*
|
|
|
|
* @return AFPTreeNode
|
|
|
|
*/
|
|
|
|
protected function doLevelMulRels() {
|
|
|
|
$leftOperand = $this->doLevelPow();
|
|
|
|
$ops = [ '*', '/', '%' ];
|
2018-08-26 08:34:42 +00:00
|
|
|
while ( $this->mCur->type === AFPToken::TOP && in_array( $this->mCur->value, $ops ) ) {
|
2016-08-24 04:52:58 +00:00
|
|
|
$op = $this->mCur->value;
|
|
|
|
$position = $this->mPos;
|
|
|
|
$this->move();
|
|
|
|
$rightOperand = $this->doLevelPow();
|
|
|
|
$leftOperand = new AFPTreeNode(
|
|
|
|
AFPTreeNode::MUL_REL,
|
|
|
|
[ $op, $leftOperand, $rightOperand ],
|
|
|
|
$position
|
|
|
|
);
|
|
|
|
}
|
|
|
|
return $leftOperand;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Handles exponentiation.
|
|
|
|
*
|
|
|
|
* @return AFPTreeNode
|
|
|
|
*/
|
|
|
|
protected function doLevelPow() {
|
|
|
|
$base = $this->doLevelBoolInvert();
|
2018-08-26 08:34:42 +00:00
|
|
|
while ( $this->mCur->type === AFPToken::TOP && $this->mCur->value === '**' ) {
|
2016-08-24 04:52:58 +00:00
|
|
|
$position = $this->mPos;
|
|
|
|
$this->move();
|
|
|
|
$exponent = $this->doLevelBoolInvert();
|
|
|
|
$base = new AFPTreeNode( AFPTreeNode::POW, [ $base, $exponent ], $position );
|
|
|
|
}
|
|
|
|
return $base;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Handles boolean inversion.
|
|
|
|
*
|
|
|
|
* @return AFPTreeNode
|
|
|
|
*/
|
|
|
|
protected function doLevelBoolInvert() {
|
2018-08-26 08:34:42 +00:00
|
|
|
if ( $this->mCur->type === AFPToken::TOP && $this->mCur->value === '!' ) {
|
2016-08-24 04:52:58 +00:00
|
|
|
$position = $this->mPos;
|
|
|
|
$this->move();
|
|
|
|
$argument = $this->doLevelKeywordOperators();
|
|
|
|
return new AFPTreeNode( AFPTreeNode::BOOL_INVERT, [ $argument ], $position );
|
|
|
|
}
|
|
|
|
|
|
|
|
return $this->doLevelKeywordOperators();
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Handles keyword operators.
|
|
|
|
*
|
|
|
|
* @return AFPTreeNode
|
|
|
|
*/
|
|
|
|
protected function doLevelKeywordOperators() {
|
|
|
|
$leftOperand = $this->doLevelUnarys();
|
|
|
|
$keyword = strtolower( $this->mCur->value );
|
2018-08-26 08:34:42 +00:00
|
|
|
if ( $this->mCur->type === AFPToken::TKEYWORD &&
|
2019-11-16 15:32:36 +00:00
|
|
|
isset( AbuseFilterParser::KEYWORDS[$keyword] )
|
2016-08-24 04:52:58 +00:00
|
|
|
) {
|
|
|
|
$position = $this->mPos;
|
|
|
|
$this->move();
|
|
|
|
$rightOperand = $this->doLevelUnarys();
|
|
|
|
|
|
|
|
return new AFPTreeNode(
|
|
|
|
AFPTreeNode::KEYWORD_OPERATOR,
|
|
|
|
[ $keyword, $leftOperand, $rightOperand ],
|
|
|
|
$position
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
return $leftOperand;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Handles unary operators.
|
|
|
|
*
|
|
|
|
* @return AFPTreeNode
|
|
|
|
*/
|
|
|
|
protected function doLevelUnarys() {
|
|
|
|
$op = $this->mCur->value;
|
2018-08-26 08:34:42 +00:00
|
|
|
if ( $this->mCur->type === AFPToken::TOP && ( $op === "+" || $op === "-" ) ) {
|
2016-08-24 04:52:58 +00:00
|
|
|
$position = $this->mPos;
|
|
|
|
$this->move();
|
2018-04-16 15:37:10 +00:00
|
|
|
$argument = $this->doLevelArrayElements();
|
2016-08-24 04:52:58 +00:00
|
|
|
return new AFPTreeNode( AFPTreeNode::UNARY, [ $op, $argument ], $position );
|
|
|
|
}
|
2018-04-16 15:37:10 +00:00
|
|
|
return $this->doLevelArrayElements();
|
2016-08-24 04:52:58 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2018-04-16 15:37:10 +00:00
|
|
|
* Handles accessing an array element by an offset.
|
2016-08-24 04:52:58 +00:00
|
|
|
*
|
|
|
|
* @return AFPTreeNode
|
|
|
|
* @throws AFPUserVisibleException
|
|
|
|
*/
|
2018-04-16 15:37:10 +00:00
|
|
|
protected function doLevelArrayElements() {
|
|
|
|
$array = $this->doLevelParenthesis();
|
2018-08-26 08:34:42 +00:00
|
|
|
while ( $this->mCur->type === AFPToken::TSQUAREBRACKET && $this->mCur->value === '[' ) {
|
2016-08-24 04:52:58 +00:00
|
|
|
$position = $this->mPos;
|
|
|
|
$index = $this->doLevelSemicolon();
|
2018-04-16 15:37:10 +00:00
|
|
|
$array = new AFPTreeNode( AFPTreeNode::ARRAY_INDEX, [ $array, $index ], $position );
|
2016-08-24 04:52:58 +00:00
|
|
|
|
2018-08-26 08:34:42 +00:00
|
|
|
if ( !( $this->mCur->type === AFPToken::TSQUAREBRACKET && $this->mCur->value === ']' ) ) {
|
2016-08-24 04:52:58 +00:00
|
|
|
throw new AFPUserVisibleException( 'expectednotfound', $this->mPos,
|
|
|
|
[ ']', $this->mCur->type, $this->mCur->value ] );
|
|
|
|
}
|
|
|
|
$this->move();
|
|
|
|
}
|
|
|
|
|
2018-04-16 15:37:10 +00:00
|
|
|
return $array;
|
2016-08-24 04:52:58 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Handles parenthesis.
|
|
|
|
*
|
|
|
|
* @return AFPTreeNode
|
|
|
|
* @throws AFPUserVisibleException
|
|
|
|
*/
|
|
|
|
protected function doLevelParenthesis() {
|
2018-08-26 08:34:42 +00:00
|
|
|
if ( $this->mCur->type === AFPToken::TBRACE && $this->mCur->value === '(' ) {
|
2019-09-09 12:12:39 +00:00
|
|
|
$next = $this->getNextToken();
|
|
|
|
if ( $next->type === AFPToken::TBRACE && $next->value === ')' ) {
|
|
|
|
// Empty parentheses are never allowed
|
|
|
|
throw new AFPUserVisibleException(
|
|
|
|
'unexpectedtoken',
|
|
|
|
$this->mPos,
|
|
|
|
[
|
|
|
|
$this->mCur->type,
|
|
|
|
$this->mCur->value
|
|
|
|
]
|
|
|
|
);
|
|
|
|
}
|
2016-08-24 04:52:58 +00:00
|
|
|
$result = $this->doLevelSemicolon();
|
|
|
|
|
2018-08-26 08:34:42 +00:00
|
|
|
if ( !( $this->mCur->type === AFPToken::TBRACE && $this->mCur->value === ')' ) ) {
|
2016-08-24 04:52:58 +00:00
|
|
|
throw new AFPUserVisibleException(
|
|
|
|
'expectednotfound',
|
|
|
|
$this->mPos,
|
|
|
|
[ ')', $this->mCur->type, $this->mCur->value ]
|
|
|
|
);
|
|
|
|
}
|
|
|
|
$this->move();
|
|
|
|
|
|
|
|
return $result;
|
|
|
|
}
|
|
|
|
|
|
|
|
return $this->doLevelFunction();
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Handles function calls.
|
|
|
|
*
|
|
|
|
* @return AFPTreeNode
|
|
|
|
* @throws AFPUserVisibleException
|
|
|
|
*/
|
|
|
|
protected function doLevelFunction() {
|
2018-08-26 08:34:42 +00:00
|
|
|
if ( $this->mCur->type === AFPToken::TID &&
|
2019-11-16 15:32:36 +00:00
|
|
|
isset( AbuseFilterParser::FUNCTIONS[$this->mCur->value] )
|
2016-08-24 04:52:58 +00:00
|
|
|
) {
|
|
|
|
$func = $this->mCur->value;
|
|
|
|
$position = $this->mPos;
|
|
|
|
$this->move();
|
2018-08-26 08:34:42 +00:00
|
|
|
if ( $this->mCur->type !== AFPToken::TBRACE || $this->mCur->value !== '(' ) {
|
2016-08-24 04:52:58 +00:00
|
|
|
throw new AFPUserVisibleException( 'expectednotfound',
|
|
|
|
$this->mPos,
|
|
|
|
[
|
|
|
|
'(',
|
|
|
|
$this->mCur->type,
|
|
|
|
$this->mCur->value
|
|
|
|
]
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2019-08-06 18:59:45 +00:00
|
|
|
if ( ( $func === 'set' || $func === 'set_var' ) ) {
|
|
|
|
$state = $this->getState();
|
|
|
|
$this->move();
|
|
|
|
$next = $this->getNextToken();
|
|
|
|
if (
|
|
|
|
$this->mCur->type !== AFPToken::TSTRING ||
|
|
|
|
(
|
|
|
|
$next->type !== AFPToken::TCOMMA &&
|
|
|
|
// Let this fail later, when checking parameters count
|
|
|
|
!( $next->type === AFPToken::TBRACE && $next->value === ')' )
|
|
|
|
)
|
|
|
|
) {
|
|
|
|
throw new AFPUserVisibleException( 'variablevariable', $this->mPos, [] );
|
|
|
|
} else {
|
|
|
|
$this->setState( $state );
|
|
|
|
}
|
|
|
|
}
|
2016-08-24 04:52:58 +00:00
|
|
|
|
Better handling of function params in CachingParser
This patch includes various fixes to how func arguments are handled in
CachingParser:
- Add a comment about a future improvement of checkSyntax, which we
could limit to try building the AST.
- Having enough args for each function is now also checked when
building the AST. This allows implementing the previous point without
stopping to report notenoughargs at syntaxcheck-time (otherwise it'd be
a runtime error). And it also ensure that we check for the params count
inside skipped branches, e.g. inside if/else: these were already only
discovered at runtime in CachingParser. The old parser is not affected
by this change, because when checking syntax it will always execute
all branches, and at runtime it will skip braces altogether.
- Fix arg count for CachingParser, which previously added a bogus param
in case of a function called without parameters. This was fixed for
the other parser in I484fe2994292970276150d2e417801453339e540, and I
just ported the updated fix. Also note that the CachingParser was
already failing for e.g. `count()`, but instead of complaining about
missing arguments, it failed hard when trying to pass NULL to
evalNode.
- Fixed some tests not to use setExpectedException, which caused the
previous point to remain unnoticed: calling that method prevents the
loop from continuing, and thus only the AbuseFilterParser part was
being executed. The new implementation checks the exception ID and is
thus more future-proof if the i18n message changes.
- Fixed some function names in error reporting for the old parser.
- The arg count is now checked outside of the function handlers, thus
it's no more necessary to call checkEnoughArguments at the beginning
of each handler. This also produces clearer error messages in case of
aliases (e.g. set/set_var).
- Check the args count even if some of the args are DUNDEFINED. This is
much easier now that the check is outside of the handler. This will
make syntax check fail for e.g. `contains_any(added_lines)`.
Bug: T156095
Change-Id: I446a307e5395ea8cc8ec5ca5d5390b074bea2f24
2019-08-20 09:43:37 +00:00
|
|
|
$args = [];
|
|
|
|
$next = $this->getNextToken();
|
|
|
|
if ( $next->type !== AFPToken::TBRACE || $next->value !== ')' ) {
|
|
|
|
do {
|
2019-09-06 16:28:53 +00:00
|
|
|
$thisArg = $this->doLevelSemicolon();
|
|
|
|
if ( $thisArg === null && !$this->functionIsVariadic( $func ) ) {
|
|
|
|
throw new AFPUserVisibleException(
|
|
|
|
'unexpectedtoken',
|
|
|
|
$this->mPos,
|
|
|
|
[
|
|
|
|
$this->mCur->type,
|
|
|
|
$this->mCur->value
|
|
|
|
]
|
|
|
|
);
|
|
|
|
} elseif ( $thisArg !== null ) {
|
|
|
|
$args[] = $thisArg;
|
|
|
|
}
|
Better handling of function params in CachingParser
This patch includes various fixes to how func arguments are handled in
CachingParser:
- Add a comment about a future improvement of checkSyntax, which we
could limit to try building the AST.
- Having enough args for each function is now also checked when
building the AST. This allows implementing the previous point without
stopping to report notenoughargs at syntaxcheck-time (otherwise it'd be
a runtime error). And it also ensure that we check for the params count
inside skipped branches, e.g. inside if/else: these were already only
discovered at runtime in CachingParser. The old parser is not affected
by this change, because when checking syntax it will always execute
all branches, and at runtime it will skip braces altogether.
- Fix arg count for CachingParser, which previously added a bogus param
in case of a function called without parameters. This was fixed for
the other parser in I484fe2994292970276150d2e417801453339e540, and I
just ported the updated fix. Also note that the CachingParser was
already failing for e.g. `count()`, but instead of complaining about
missing arguments, it failed hard when trying to pass NULL to
evalNode.
- Fixed some tests not to use setExpectedException, which caused the
previous point to remain unnoticed: calling that method prevents the
loop from continuing, and thus only the AbuseFilterParser part was
being executed. The new implementation checks the exception ID and is
thus more future-proof if the i18n message changes.
- Fixed some function names in error reporting for the old parser.
- The arg count is now checked outside of the function handlers, thus
it's no more necessary to call checkEnoughArguments at the beginning
of each handler. This also produces clearer error messages in case of
aliases (e.g. set/set_var).
- Check the args count even if some of the args are DUNDEFINED. This is
much easier now that the check is outside of the handler. This will
make syntax check fail for e.g. `contains_any(added_lines)`.
Bug: T156095
Change-Id: I446a307e5395ea8cc8ec5ca5d5390b074bea2f24
2019-08-20 09:43:37 +00:00
|
|
|
} while ( $this->mCur->type === AFPToken::TCOMMA );
|
|
|
|
} else {
|
|
|
|
$this->move();
|
|
|
|
}
|
2019-09-06 16:28:53 +00:00
|
|
|
|
2018-08-26 08:34:42 +00:00
|
|
|
if ( $this->mCur->type !== AFPToken::TBRACE || $this->mCur->value !== ')' ) {
|
2016-08-24 04:52:58 +00:00
|
|
|
throw new AFPUserVisibleException( 'expectednotfound',
|
|
|
|
$this->mPos,
|
|
|
|
[
|
|
|
|
')',
|
|
|
|
$this->mCur->type,
|
|
|
|
$this->mCur->value
|
|
|
|
]
|
|
|
|
);
|
|
|
|
}
|
Better handling of function params in CachingParser
This patch includes various fixes to how func arguments are handled in
CachingParser:
- Add a comment about a future improvement of checkSyntax, which we
could limit to try building the AST.
- Having enough args for each function is now also checked when
building the AST. This allows implementing the previous point without
stopping to report notenoughargs at syntaxcheck-time (otherwise it'd be
a runtime error). And it also ensure that we check for the params count
inside skipped branches, e.g. inside if/else: these were already only
discovered at runtime in CachingParser. The old parser is not affected
by this change, because when checking syntax it will always execute
all branches, and at runtime it will skip braces altogether.
- Fix arg count for CachingParser, which previously added a bogus param
in case of a function called without parameters. This was fixed for
the other parser in I484fe2994292970276150d2e417801453339e540, and I
just ported the updated fix. Also note that the CachingParser was
already failing for e.g. `count()`, but instead of complaining about
missing arguments, it failed hard when trying to pass NULL to
evalNode.
- Fixed some tests not to use setExpectedException, which caused the
previous point to remain unnoticed: calling that method prevents the
loop from continuing, and thus only the AbuseFilterParser part was
being executed. The new implementation checks the exception ID and is
thus more future-proof if the i18n message changes.
- Fixed some function names in error reporting for the old parser.
- The arg count is now checked outside of the function handlers, thus
it's no more necessary to call checkEnoughArguments at the beginning
of each handler. This also produces clearer error messages in case of
aliases (e.g. set/set_var).
- Check the args count even if some of the args are DUNDEFINED. This is
much easier now that the check is outside of the handler. This will
make syntax check fail for e.g. `contains_any(added_lines)`.
Bug: T156095
Change-Id: I446a307e5395ea8cc8ec5ca5d5390b074bea2f24
2019-08-20 09:43:37 +00:00
|
|
|
// Giving too few arguments to a function is a pretty common error. If we check it here
|
|
|
|
// (as well as at runtime, for OCD), we can make checkSyntax only try to build the AST, as
|
|
|
|
// there would be way less runtime errors. Moreover, this check will also be performed inside
|
|
|
|
// skipped branches, e.g. the discarded if/else branch.
|
2019-08-21 09:01:50 +00:00
|
|
|
$this->checkArgCount( $args, $func );
|
2016-08-24 04:52:58 +00:00
|
|
|
$this->move();
|
|
|
|
|
|
|
|
array_unshift( $args, $func );
|
|
|
|
return new AFPTreeNode( AFPTreeNode::FUNCTION_CALL, $args, $position );
|
|
|
|
}
|
|
|
|
|
|
|
|
return $this->doLevelAtom();
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Handle literals.
|
|
|
|
* @return AFPTreeNode
|
|
|
|
* @throws AFPUserVisibleException
|
|
|
|
*/
|
|
|
|
protected function doLevelAtom() {
|
|
|
|
$tok = $this->mCur->value;
|
|
|
|
switch ( $this->mCur->type ) {
|
|
|
|
case AFPToken::TID:
|
2019-10-02 11:24:48 +00:00
|
|
|
$this->checkLogDeprecatedVar( strtolower( $tok ) );
|
|
|
|
// Fallthrough intended
|
2016-08-24 04:52:58 +00:00
|
|
|
case AFPToken::TSTRING:
|
|
|
|
case AFPToken::TFLOAT:
|
|
|
|
case AFPToken::TINT:
|
|
|
|
$result = new AFPTreeNode( AFPTreeNode::ATOM, $this->mCur, $this->mPos );
|
|
|
|
break;
|
|
|
|
case AFPToken::TKEYWORD:
|
|
|
|
if ( in_array( $tok, [ "true", "false", "null" ] ) ) {
|
|
|
|
$result = new AFPTreeNode( AFPTreeNode::ATOM, $this->mCur, $this->mPos );
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
throw new AFPUserVisibleException(
|
|
|
|
'unrecognisedkeyword',
|
|
|
|
$this->mPos,
|
|
|
|
[ $tok ]
|
|
|
|
);
|
|
|
|
/** @noinspection PhpMissingBreakStatementInspection */
|
|
|
|
case AFPToken::TSQUAREBRACKET:
|
2018-08-26 08:34:42 +00:00
|
|
|
if ( $this->mCur->value === '[' ) {
|
2018-04-16 15:37:10 +00:00
|
|
|
$array = [];
|
2016-08-24 04:52:58 +00:00
|
|
|
while ( true ) {
|
|
|
|
$this->move();
|
2018-08-26 08:34:42 +00:00
|
|
|
if ( $this->mCur->type === AFPToken::TSQUAREBRACKET && $this->mCur->value === ']' ) {
|
2016-08-24 04:52:58 +00:00
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
2018-04-16 15:37:10 +00:00
|
|
|
$array[] = $this->doLevelSet();
|
2016-08-24 04:52:58 +00:00
|
|
|
|
2018-08-26 08:34:42 +00:00
|
|
|
if ( $this->mCur->type === AFPToken::TSQUAREBRACKET && $this->mCur->value === ']' ) {
|
2016-08-24 04:52:58 +00:00
|
|
|
break;
|
|
|
|
}
|
2018-08-26 08:34:42 +00:00
|
|
|
if ( $this->mCur->type !== AFPToken::TCOMMA ) {
|
2016-08-24 04:52:58 +00:00
|
|
|
throw new AFPUserVisibleException(
|
|
|
|
'expectednotfound',
|
|
|
|
$this->mPos,
|
|
|
|
[ ', or ]', $this->mCur->type, $this->mCur->value ]
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-04-16 15:37:10 +00:00
|
|
|
$result = new AFPTreeNode( AFPTreeNode::ARRAY_DEFINITION, $array, $this->mPos );
|
2016-08-24 04:52:58 +00:00
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Fallthrough expected
|
|
|
|
default:
|
|
|
|
throw new AFPUserVisibleException(
|
|
|
|
'unexpectedtoken',
|
|
|
|
$this->mPos,
|
|
|
|
[
|
|
|
|
$this->mCur->type,
|
|
|
|
$this->mCur->value
|
|
|
|
]
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
$this->move();
|
2020-01-28 17:43:26 +00:00
|
|
|
// @phan-suppress-next-next-line PhanPossiblyUndeclaredVariable
|
2019-12-07 17:20:10 +00:00
|
|
|
// @phan-suppress-next-line PhanTypeMismatchReturnNullable Until phan can understand the switch
|
2016-08-24 04:52:58 +00:00
|
|
|
return $result;
|
|
|
|
}
|
2019-10-02 11:24:48 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Given a variable name, check if the variable is deprecated. If it is, log the use.
|
|
|
|
* Do that here, and not every time the AST is eval'ed. This means less logging, but more
|
|
|
|
* performance.
|
|
|
|
* @param string $varname
|
|
|
|
*/
|
|
|
|
protected function checkLogDeprecatedVar( $varname ) {
|
|
|
|
if ( array_key_exists( $varname, AbuseFilter::getDeprecatedVariables() ) ) {
|
|
|
|
$this->logger->debug( "Deprecated variable $varname used in filter {$this->mFilter}." );
|
|
|
|
}
|
|
|
|
}
|
2016-08-24 04:52:58 +00:00
|
|
|
}
|