mediawiki-extensions-AbuseF.../includes/parser/AbuseFilterParser.php

1465 lines
36 KiB
PHP
Raw Normal View History

<?php
use Wikimedia\Equivset\Equivset;
use MediaWiki\Logger\LoggerFactory;
class AbuseFilterParser {
public $mTokens, $mPos, $mShortCircuit, $mAllowShort;
/** @var AFPToken The current token */
public $mCur;
/**
* @var AbuseFilterVariableHolder
*/
public $mVars;
// length,lcase,ucase,ccnorm,rmdoubles,specialratio,rmspecials,norm,count,get_matches
public static $mFunctions = [
'lcase' => 'funcLc',
'ucase' => 'funcUc',
'length' => 'funcLen',
'string' => 'castString',
'int' => 'castInt',
'float' => 'castFloat',
'bool' => 'castBool',
'norm' => 'funcNorm',
'ccnorm' => 'funcCCNorm',
'ccnorm_contains_any' => 'funcCCNormContainsAny',
'ccnorm_contains_all' => 'funcCCNormContainsAll',
'specialratio' => 'funcSpecialRatio',
'rmspecials' => 'funcRMSpecials',
'rmdoubles' => 'funcRMDoubles',
'rmwhitespace' => 'funcRMWhitespace',
'count' => 'funcCount',
'rcount' => 'funcRCount',
'get_matches' => 'funcGetMatches',
2009-03-09 12:39:52 +00:00
'ip_in_range' => 'funcIPInRange',
'contains_any' => 'funcContainsAny',
'contains_all' => 'funcContainsAll',
'equals_to_any' => 'funcEqualsToAny',
'substr' => 'funcSubstr',
'strlen' => 'funcLen',
'strpos' => 'funcStrPos',
'str_replace' => 'funcStrReplace',
'rescape' => 'funcStrRegexEscape',
'set' => 'funcSetVar',
'set_var' => 'funcSetVar',
'sanitize' => 'funcSanitize',
];
// Functions that affect parser state, and shouldn't be cached.
public static $ActiveFunctions = [
'funcSetVar',
];
public static $mKeywords = [
'in' => 'keywordIn',
'like' => 'keywordLike',
'matches' => 'keywordLike',
'contains' => 'keywordContains',
'rlike' => 'keywordRegex',
'irlike' => 'keywordRegexInsensitive',
'regex' => 'keywordRegex',
];
public static $funcCache = [];
/**
* @var Equivset
*/
protected static $equivset;
AbuseFilter: Change format of database logging/ performance AF is setting several lazy load variables for the currently editing user. To do this it's passing along the user name extracted from a user object and generating a new user object later from that name which is of course pointless. With this patch I'll pass user objects directly to prevent that. On top of that I've deprecated a method in AFComputedVariable::compute which was redundant as there is a more generic one which can solve that task just fine. Furthermore I've changed the logging behaviour from serializing the whole AbuseFilterVariableHolder object to only store the variables. That has two major advantages: * The amount of data that needs to be saved on a filter hit is reduced to about 1/10 of what the old version needed. * This is much more forward compatible as the old way of saving this relied on the class structure to stay the same while this is a simple array containing the vars. On top of that we now only log variables already set by the time a filter is hit. On top of the obvious performance increasement that makes it easier for the user to spot the relevant data. Another thing this change alters is the way the AbuseFilter internally works with AbuseFilterVariableHolder objects. Right now we use one for testing the filter(s) and later we use another one to compute the same data again in case a filter was hit (for logging)! This is not thoroughly tested yet, but way more sane than what we're currently doing! Change-Id: Ib15e7501bff32a54afe2d103ef5aedb950e58ef6
2013-01-07 00:02:41 +00:00
/**
* Create a new instance
*
* @param AbuseFilterVariableHolder|null $vars
AbuseFilter: Change format of database logging/ performance AF is setting several lazy load variables for the currently editing user. To do this it's passing along the user name extracted from a user object and generating a new user object later from that name which is of course pointless. With this patch I'll pass user objects directly to prevent that. On top of that I've deprecated a method in AFComputedVariable::compute which was redundant as there is a more generic one which can solve that task just fine. Furthermore I've changed the logging behaviour from serializing the whole AbuseFilterVariableHolder object to only store the variables. That has two major advantages: * The amount of data that needs to be saved on a filter hit is reduced to about 1/10 of what the old version needed. * This is much more forward compatible as the old way of saving this relied on the class structure to stay the same while this is a simple array containing the vars. On top of that we now only log variables already set by the time a filter is hit. On top of the obvious performance increasement that makes it easier for the user to spot the relevant data. Another thing this change alters is the way the AbuseFilter internally works with AbuseFilterVariableHolder objects. Right now we use one for testing the filter(s) and later we use another one to compute the same data again in case a filter was hit (for logging)! This is not thoroughly tested yet, but way more sane than what we're currently doing! Change-Id: Ib15e7501bff32a54afe2d103ef5aedb950e58ef6
2013-01-07 00:02:41 +00:00
*/
public function __construct( $vars = null ) {
$this->resetState();
AbuseFilter: Change format of database logging/ performance AF is setting several lazy load variables for the currently editing user. To do this it's passing along the user name extracted from a user object and generating a new user object later from that name which is of course pointless. With this patch I'll pass user objects directly to prevent that. On top of that I've deprecated a method in AFComputedVariable::compute which was redundant as there is a more generic one which can solve that task just fine. Furthermore I've changed the logging behaviour from serializing the whole AbuseFilterVariableHolder object to only store the variables. That has two major advantages: * The amount of data that needs to be saved on a filter hit is reduced to about 1/10 of what the old version needed. * This is much more forward compatible as the old way of saving this relied on the class structure to stay the same while this is a simple array containing the vars. On top of that we now only log variables already set by the time a filter is hit. On top of the obvious performance increasement that makes it easier for the user to spot the relevant data. Another thing this change alters is the way the AbuseFilter internally works with AbuseFilterVariableHolder objects. Right now we use one for testing the filter(s) and later we use another one to compute the same data again in case a filter was hit (for logging)! This is not thoroughly tested yet, but way more sane than what we're currently doing! Change-Id: Ib15e7501bff32a54afe2d103ef5aedb950e58ef6
2013-01-07 00:02:41 +00:00
if ( $vars instanceof AbuseFilterVariableHolder ) {
$this->mVars = $vars;
}
}
/**
* Resets the state of the parser.
*/
public function resetState() {
$this->mTokens = [];
$this->mVars = new AbuseFilterVariableHolder;
$this->mPos = 0;
$this->mShortCircuit = false;
$this->mAllowShort = true;
}
/**
* @param string $filter
* @return true|array True when successful, otherwise a two-element array with exception message
* and character position of the syntax error
*/
public function checkSyntax( $filter ) {
$origAS = $this->mAllowShort;
try {
$this->mAllowShort = false;
$this->intEval( $filter );
} catch ( AFPUserVisibleException $excep ) {
$this->mAllowShort = $origAS;
return [ $excep->getMessageObj()->text(), $excep->mPosition ];
}
$this->mAllowShort = $origAS;
return true;
}
/**
* Move to the next token
*/
protected function move() {
list( $this->mCur, $this->mPos ) = $this->mTokens[$this->mPos];
}
/**
* 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;
}
/**
* @throws AFPUserVisibleException
*/
protected function skipOverBraces() {
$braces = 1;
while ( $this->mCur->type != AFPToken::TNONE && $braces > 0 ) {
$this->move();
if ( $this->mCur->type == AFPToken::TBRACE ) {
if ( $this->mCur->value == '(' ) {
$braces++;
} elseif ( $this->mCur->value == ')' ) {
$braces--;
}
}
}
if ( !( $this->mCur->type == AFPToken::TBRACE && $this->mCur->value == ')' ) ) {
throw new AFPUserVisibleException( 'expectednotfound', $this->mCur->pos, [ ')' ] );
}
}
/**
* @param string $code
* @return bool
*/
public function parse( $code ) {
return $this->intEval( $code )->toBool();
}
/**
* @param string $filter
* @return string
*/
public function evaluateExpression( $filter ) {
return $this->intEval( $filter )->toString();
}
/**
* @param string $code
* @return AFPData
*/
public function intEval( $code ) {
// Reset all class members to their default value
$this->mTokens = AbuseFilterTokenizer::tokenize( $code );
$this->mPos = 0;
$this->mShortCircuit = false;
$result = new AFPData();
$this->doLevelEntry( $result );
return $result;
}
/* Levels */
/**
* Handles unexpected characters after the expression
*
* @param AFPData &$result
* @throws AFPUserVisibleException
*/
protected function doLevelEntry( &$result ) {
$this->doLevelSemicolon( $result );
if ( $this->mCur->type != AFPToken::TNONE ) {
throw new AFPUserVisibleException(
'unexpectedatend',
$this->mCur->pos, [ $this->mCur->type ]
);
}
}
/**
* Handles multiple expressions delimited by a semicolon
*
* @param AFPData &$result
*/
protected function doLevelSemicolon( &$result ) {
do {
$this->move();
if ( $this->mCur->type != AFPToken::TSTATEMENTSEPARATOR ) {
$this->doLevelSet( $result );
}
} while ( $this->mCur->type == AFPToken::TSTATEMENTSEPARATOR );
}
/**
* Handles assignments (:=)
*
* @param AFPData &$result
* @throws AFPUserVisibleException
*/
protected function doLevelSet( &$result ) {
if ( $this->mCur->type == AFPToken::TID ) {
$varname = $this->mCur->value;
$prev = $this->getState();
$this->move();
if ( $this->mCur->type == AFPToken::TOP && $this->mCur->value == ':=' ) {
$this->move();
$this->doLevelSet( $result );
$this->setUserVariable( $varname, $result );
return;
} elseif ( $this->mCur->type == AFPToken::TSQUAREBRACKET && $this->mCur->value == '[' ) {
if ( !$this->mVars->varIsSet( $varname ) ) {
throw new AFPUserVisibleException( 'unrecognisedvar',
$this->mCur->pos,
[ $varname ]
);
}
$array = $this->mVars->getVar( $varname );
if ( $array->type != AFPData::DARRAY ) {
throw new AFPUserVisibleException( 'notarray', $this->mCur->pos, [] );
}
$array = $array->toArray();
$this->move();
if ( $this->mCur->type == AFPToken::TSQUAREBRACKET && $this->mCur->value == ']' ) {
$idx = 'new';
} else {
2009-05-22 06:42:10 +00:00
$this->setState( $prev );
$this->move();
$idx = new AFPData();
$this->doLevelSemicolon( $idx );
$idx = $idx->toInt();
if ( !( $this->mCur->type == AFPToken::TSQUAREBRACKET && $this->mCur->value == ']' ) ) {
throw new AFPUserVisibleException( 'expectednotfound', $this->mCur->pos,
[ ']', $this->mCur->type, $this->mCur->value ] );
}
if ( count( $array ) <= $idx ) {
throw new AFPUserVisibleException( 'outofbounds', $this->mCur->pos,
[ $idx, count( $result->data ) ] );
}
}
$this->move();
if ( $this->mCur->type == AFPToken::TOP && $this->mCur->value == ':=' ) {
$this->move();
$this->doLevelSet( $result );
if ( $idx === 'new' ) {
$array[] = $result;
} else {
$array[$idx] = $result;
}
$this->setUserVariable( $varname, new AFPData( AFPData::DARRAY, $array ) );
return;
} else {
$this->setState( $prev );
}
} else {
$this->setState( $prev );
}
}
$this->doLevelConditions( $result );
}
/**
* Handles conditionals: if-then-else and ternary operator
*
* @param AFPData &$result
* @throws AFPUserVisibleException
*/
protected function doLevelConditions( &$result ) {
if ( $this->mCur->type == AFPToken::TKEYWORD && $this->mCur->value == 'if' ) {
$this->move();
$this->doLevelBoolOps( $result );
if ( !( $this->mCur->type == AFPToken::TKEYWORD && $this->mCur->value == 'then' ) ) {
throw new AFPUserVisibleException( 'expectednotfound',
$this->mCur->pos,
[
'then',
$this->mCur->type,
$this->mCur->value
]
);
}
$this->move();
$r1 = new AFPData();
$r2 = new AFPData();
$isTrue = $result->toBool();
if ( !$isTrue ) {
$scOrig = $this->mShortCircuit;
$this->mShortCircuit = $this->mAllowShort;
}
$this->doLevelConditions( $r1 );
if ( !$isTrue ) {
$this->mShortCircuit = $scOrig;
}
if ( !( $this->mCur->type == AFPToken::TKEYWORD && $this->mCur->value == 'else' ) ) {
throw new AFPUserVisibleException( 'expectednotfound',
$this->mCur->pos,
[
'else',
$this->mCur->type,
$this->mCur->value
]
);
}
$this->move();
if ( $isTrue ) {
$scOrig = $this->mShortCircuit;
$this->mShortCircuit = $this->mAllowShort;
}
$this->doLevelConditions( $r2 );
if ( $isTrue ) {
$this->mShortCircuit = $scOrig;
}
if ( !( $this->mCur->type == AFPToken::TKEYWORD && $this->mCur->value == 'end' ) ) {
throw new AFPUserVisibleException( 'expectednotfound',
$this->mCur->pos,
[
'end',
$this->mCur->type,
$this->mCur->value
]
);
}
$this->move();
if ( $result->toBool() ) {
$result = $r1;
} else {
$result = $r2;
}
} else {
$this->doLevelBoolOps( $result );
if ( $this->mCur->type == AFPToken::TOP && $this->mCur->value == '?' ) {
$this->move();
$r1 = new AFPData();
$r2 = new AFPData();
$isTrue = $result->toBool();
if ( !$isTrue ) {
$scOrig = $this->mShortCircuit;
$this->mShortCircuit = $this->mAllowShort;
}
$this->doLevelConditions( $r1 );
if ( !$isTrue ) {
$this->mShortCircuit = $scOrig;
}
if ( !( $this->mCur->type == AFPToken::TOP && $this->mCur->value == ':' ) ) {
throw new AFPUserVisibleException( 'expectednotfound',
$this->mCur->pos,
[
':',
$this->mCur->type,
$this->mCur->value
]
);
}
$this->move();
if ( $isTrue ) {
$scOrig = $this->mShortCircuit;
$this->mShortCircuit = $this->mAllowShort;
}
$this->doLevelConditions( $r2 );
if ( $isTrue ) {
$this->mShortCircuit = $scOrig;
}
if ( $isTrue ) {
$result = $r1;
} else {
$result = $r2;
}
}
}
}
/**
* Handles boolean operators (&, |, ^)
*
* @param AFPData &$result
*/
protected function doLevelBoolOps( &$result ) {
$this->doLevelCompares( $result );
$ops = [ '&', '|', '^' ];
while ( $this->mCur->type == AFPToken::TOP && in_array( $this->mCur->value, $ops ) ) {
$op = $this->mCur->value;
$this->move();
$r2 = new AFPData();
// We can go on quickly as either one statement with | is true or one with & is false
if ( ( $op == '&' && !$result->toBool() ) || ( $op == '|' && $result->toBool() ) ) {
$orig = $this->mShortCircuit;
$this->mShortCircuit = $this->mAllowShort;
$this->doLevelCompares( $r2 );
2009-03-19 00:07:29 +00:00
$this->mShortCircuit = $orig;
$result = new AFPData( AFPData::DBOOL, $result->toBool() );
2009-03-19 00:18:03 +00:00
continue;
}
$this->doLevelCompares( $r2 );
$result = AFPData::boolOp( $result, $r2, $op );
}
}
/**
* Handles comparison operators
*
* @param AFPData &$result
*/
protected function doLevelCompares( &$result ) {
$this->doLevelSumRels( $result );
$ops = [ '==', '===', '!=', '!==', '<', '>', '<=', '>=', '=' ];
while ( $this->mCur->type == AFPToken::TOP && in_array( $this->mCur->value, $ops ) ) {
$op = $this->mCur->value;
$this->move();
$r2 = new AFPData();
$this->doLevelSumRels( $r2 );
if ( $this->mShortCircuit ) {
// The result doesn't matter.
break;
}
AbuseFilter::triggerLimiter();
$result = AFPData::compareOp( $result, $r2, $op );
}
}
/**
* Handles sum-related operations (+ and -)
*
* @param AFPData &$result
*/
protected function doLevelSumRels( &$result ) {
$this->doLevelMulRels( $result );
$ops = [ '+', '-' ];
while ( $this->mCur->type == AFPToken::TOP && in_array( $this->mCur->value, $ops ) ) {
$op = $this->mCur->value;
$this->move();
$r2 = new AFPData();
$this->doLevelMulRels( $r2 );
if ( $this->mShortCircuit ) {
// The result doesn't matter.
break;
}
if ( $op == '+' ) {
$result = AFPData::sum( $result, $r2 );
}
if ( $op == '-' ) {
$result = AFPData::sub( $result, $r2 );
}
}
}
/**
* Handles multiplication-related operations (*, / and %)
*
* @param AFPData &$result
*/
protected function doLevelMulRels( &$result ) {
$this->doLevelPow( $result );
$ops = [ '*', '/', '%' ];
while ( $this->mCur->type == AFPToken::TOP && in_array( $this->mCur->value, $ops ) ) {
$op = $this->mCur->value;
$this->move();
$r2 = new AFPData();
$this->doLevelPow( $r2 );
if ( $this->mShortCircuit ) {
// The result doesn't matter.
break;
}
$result = AFPData::mulRel( $result, $r2, $op, $this->mCur->pos );
}
}
/**
* Handles powers (**)
*
* @param AFPData &$result
*/
protected function doLevelPow( &$result ) {
$this->doLevelBoolInvert( $result );
while ( $this->mCur->type == AFPToken::TOP && $this->mCur->value == '**' ) {
$this->move();
$expanent = new AFPData();
$this->doLevelBoolInvert( $expanent );
if ( $this->mShortCircuit ) {
// The result doesn't matter.
break;
}
$result = AFPData::pow( $result, $expanent );
}
}
/**
* Handles boolean inversion (!)
*
* @param AFPData &$result
*/
protected function doLevelBoolInvert( &$result ) {
if ( $this->mCur->type == AFPToken::TOP && $this->mCur->value == '!' ) {
$this->move();
$this->doLevelSpecialWords( $result );
if ( $this->mShortCircuit ) {
// The result doesn't matter.
return;
}
$result = AFPData::boolInvert( $result );
} else {
$this->doLevelSpecialWords( $result );
}
}
/**
* Handles keywords (in, like, rlike, contains, ...)
*
* @param AFPData &$result
*/
protected function doLevelSpecialWords( &$result ) {
$this->doLevelUnarys( $result );
$keyword = strtolower( $this->mCur->value );
if ( $this->mCur->type == AFPToken::TKEYWORD
&& isset( self::$mKeywords[$keyword] )
) {
$func = self::$mKeywords[$keyword];
$this->move();
$r2 = new AFPData();
$this->doLevelUnarys( $r2 );
if ( $this->mShortCircuit ) {
// The result doesn't matter.
return;
}
AbuseFilter::triggerLimiter();
$result = AFPData::$func( $result, $r2, $this->mCur->pos );
}
}
/**
* Handles unary plus and minus, like in -5 or -(2 * +2)
*
* @param AFPData &$result
*/
protected function doLevelUnarys( &$result ) {
$op = $this->mCur->value;
if ( $this->mCur->type == AFPToken::TOP && ( $op == "+" || $op == "-" ) ) {
$this->move();
$this->doLevelArrayElements( $result );
if ( $this->mShortCircuit ) {
// The result doesn't matter.
return;
}
if ( $op == '-' ) {
$result = AFPData::unaryMinus( $result );
}
} else {
$this->doLevelArrayElements( $result );
}
}
/**
* Handles array elements, parsing expressions like array[number]
*
* @param AFPData &$result
* @throws AFPUserVisibleException
*/
protected function doLevelArrayElements( &$result ) {
$this->doLevelBraces( $result );
while ( $this->mCur->type == AFPToken::TSQUAREBRACKET && $this->mCur->value == '[' ) {
$idx = new AFPData();
$this->doLevelSemicolon( $idx );
if ( !( $this->mCur->type == AFPToken::TSQUAREBRACKET && $this->mCur->value == ']' ) ) {
throw new AFPUserVisibleException( 'expectednotfound', $this->mCur->pos,
[ ']', $this->mCur->type, $this->mCur->value ] );
}
$idx = $idx->toInt();
if ( $result->type == AFPData::DARRAY ) {
if ( count( $result->data ) <= $idx ) {
throw new AFPUserVisibleException( 'outofbounds', $this->mCur->pos,
[ $idx, count( $result->data ) ] );
}
$result = $result->data[$idx];
} else {
throw new AFPUserVisibleException( 'notarray', $this->mCur->pos, [] );
}
$this->move();
}
}
/**
* Handles brackets, only ( and )
*
* @param AFPData &$result
* @throws AFPUserVisibleException
*/
protected function doLevelBraces( &$result ) {
if ( $this->mCur->type == AFPToken::TBRACE && $this->mCur->value == '(' ) {
if ( $this->mShortCircuit ) {
$this->skipOverBraces();
} else {
$this->doLevelSemicolon( $result );
}
if ( !( $this->mCur->type == AFPToken::TBRACE && $this->mCur->value == ')' ) ) {
throw new AFPUserVisibleException(
'expectednotfound',
$this->mCur->pos,
[ ')', $this->mCur->type, $this->mCur->value ]
);
}
$this->move();
} else {
$this->doLevelFunction( $result );
}
}
/**
* Handles functions
*
* @param AFPData &$result
* @throws AFPUserVisibleException
*/
protected function doLevelFunction( &$result ) {
if ( $this->mCur->type == AFPToken::TID && isset( self::$mFunctions[$this->mCur->value] ) ) {
$func = self::$mFunctions[$this->mCur->value];
$this->move();
if ( $this->mCur->type != AFPToken::TBRACE || $this->mCur->value != '(' ) {
throw new AFPUserVisibleException( 'expectednotfound',
$this->mCur->pos,
[
'(',
$this->mCur->type,
$this->mCur->value
]
);
}
if ( $this->mShortCircuit ) {
$this->skipOverBraces();
$this->move();
// The result doesn't matter.
return;
}
$args = [];
$state = $this->getState();
$this->move();
if ( $this->mCur->type != AFPToken::TBRACE || $this->mCur->value != ')' ) {
$this->setState( $state );
do {
$r = new AFPData();
$this->doLevelSemicolon( $r );
$args[] = $r;
} while ( $this->mCur->type == AFPToken::TCOMMA );
}
if ( $this->mCur->type != AFPToken::TBRACE || $this->mCur->value != ')' ) {
throw new AFPUserVisibleException( 'expectednotfound',
$this->mCur->pos,
[
')',
$this->mCur->type,
$this->mCur->value
]
);
}
$this->move();
$funcHash = md5( $func . serialize( $args ) );
if ( isset( self::$funcCache[$funcHash] ) &&
!in_array( $func, self::$ActiveFunctions )
) {
$result = self::$funcCache[$funcHash];
} else {
AbuseFilter::triggerLimiter();
$result = self::$funcCache[$funcHash] = $this->$func( $args );
}
if ( count( self::$funcCache ) > 1000 ) {
self::$funcCache = [];
}
} else {
$this->doLevelAtom( $result );
}
}
/**
* Handles the return value
*
* @param AFPData &$result
* @throws AFPUserVisibleException
*/
protected function doLevelAtom( &$result ) {
$tok = $this->mCur->value;
switch ( $this->mCur->type ) {
case AFPToken::TID:
if ( $this->mShortCircuit ) {
$prev = $this->getState();
$this->move();
if ( $this->mCur->type === AFPToken::TSQUAREBRACKET && $this->mCur->value === '[' ) {
// If the variable represented by $tok is an array, don't break already: $result
// would be null and null[idx] will throw. Instead, skip the whole element (T204841)
$idx = new AFPData();
$this->doLevelSemicolon( $idx );
} else {
$this->setState( $prev );
}
break;
}
$var = strtolower( $tok );
$result = $this->getVarValue( $var );
break;
case AFPToken::TSTRING:
$result = new AFPData( AFPData::DSTRING, $tok );
break;
case AFPToken::TFLOAT:
$result = new AFPData( AFPData::DFLOAT, $tok );
break;
case AFPToken::TINT:
$result = new AFPData( AFPData::DINT, $tok );
break;
case AFPToken::TKEYWORD:
if ( $tok == "true" ) {
$result = new AFPData( AFPData::DBOOL, true );
} elseif ( $tok == "false" ) {
$result = new AFPData( AFPData::DBOOL, false );
} elseif ( $tok == "null" ) {
$result = new AFPData();
} else {
throw new AFPUserVisibleException(
'unrecognisedkeyword',
$this->mCur->pos,
[ $tok ]
);
}
break;
case AFPToken::TNONE:
// Handled at entry level
return;
case AFPToken::TBRACE:
if ( $this->mCur->value == ')' ) {
// Handled at the entry level
return;
}
case AFPToken::TSQUAREBRACKET:
if ( $this->mCur->value == '[' ) {
$array = [];
while ( true ) {
$this->move();
if ( $this->mCur->type == AFPToken::TSQUAREBRACKET && $this->mCur->value == ']' ) {
break;
}
$item = new AFPData();
$this->doLevelSet( $item );
$array[] = $item;
if ( $this->mCur->type == AFPToken::TSQUAREBRACKET && $this->mCur->value == ']' ) {
break;
}
if ( $this->mCur->type != AFPToken::TCOMMA ) {
throw new AFPUserVisibleException(
'expectednotfound',
$this->mCur->pos,
[ ', or ]', $this->mCur->type, $this->mCur->value ]
);
}
}
$result = new AFPData( AFPData::DARRAY, $array );
break;
}
default:
throw new AFPUserVisibleException(
'unexpectedtoken',
$this->mCur->pos,
[
$this->mCur->type,
$this->mCur->value
]
);
}
$this->move();
}
/* End of levels */
/**
* @param string $var
* @return AFPData
* @throws AFPUserVisibleException
*/
protected function getVarValue( $var ) {
$var = strtolower( $var );
$builderValues = AbuseFilter::getBuilderValues();
$deprecatedVars = AbuseFilter::getDeprecatedVariables();
if ( array_key_exists( $var, $deprecatedVars ) ) {
$logger = LoggerFactory::getInstance( 'AbuseFilter' );
$logger->debug( "AbuseFilter: deprecated variable $var used." );
$var = $deprecatedVars[$var];
}
if ( !( array_key_exists( $var, $builderValues['vars'] )
|| $this->mVars->varIsSet( $var ) )
) {
$msg = array_key_exists( $var, AbuseFilter::$disabledVars ) ?
'disabledvar' :
'unrecognisedvar';
// If the variable is invalid, throw an exception
throw new AFPUserVisibleException(
$msg,
$this->mCur->pos,
[ $var ]
);
} else {
return $this->mVars->getVar( $var );
}
}
/**
* @param string $name
* @param mixed $value
* @throws AFPUserVisibleException
*/
protected function setUserVariable( $name, $value ) {
$builderValues = AbuseFilter::getBuilderValues();
$deprecatedVars = AbuseFilter::getDeprecatedVariables();
$blacklistedValues = AbuseFilterVariableHolder::$varBlacklist;
if ( array_key_exists( $name, $builderValues['vars'] ) ||
array_key_exists( $name, AbuseFilter::$disabledVars ) ||
array_key_exists( $name, $deprecatedVars ) ||
in_array( $name, $blacklistedValues ) ) {
throw new AFPUserVisibleException( 'overridebuiltin', $this->mCur->pos, [ $name ] );
}
$this->mVars->setVar( $name, $value );
}
/**
* Check that a built-in function has been provided enough arguments
*
* @param array $args The arguments supplied to the function
* @param string $func The function name
* @param int $count The amount of needed arguments. If this is 1, then the 'noparams' message
* is used. Otherwise, the message is 'notenoughargs'.
* @throws AFPUserVisibleException
*/
protected function checkEnoughArguments( $args, $func, $count ) {
if ( count( $args ) < $count ) {
throw new AFPUserVisibleException(
$count === 1 ? 'noparams' : 'notenoughargs',
$this->mCur->pos,
[ $func, $count, count( $args ) ]
);
}
}
// Built-in functions
/**
* @param array $args
* @return AFPData
*/
protected function funcLc( $args ) {
global $wgContLang;
$this->checkEnoughArguments( $args, 'lc', 1 );
$s = $args[0]->toString();
return new AFPData( AFPData::DSTRING, $wgContLang->lc( $s ) );
}
/**
* @param array $args
* @return AFPData
*/
protected function funcUc( $args ) {
global $wgContLang;
$this->checkEnoughArguments( $args, 'uc', 1 );
$s = $args[0]->toString();
return new AFPData( AFPData::DSTRING, $wgContLang->uc( $s ) );
}
/**
* @param array $args
* @return AFPData
*/
protected function funcLen( $args ) {
$this->checkEnoughArguments( $args, 'len', 1 );
if ( $args[0]->type == AFPData::DARRAY ) {
// Don't use toString on arrays, but count
$val = count( $args[0]->data );
} else {
$val = mb_strlen( $args[0]->toString(), 'utf-8' );
}
return new AFPData( AFPData::DINT, $val );
}
/**
* @param array $args
* @return AFPData
*/
protected function funcSpecialRatio( $args ) {
$this->checkEnoughArguments( $args, 'specialratio', 1 );
$s = $args[0]->toString();
if ( !strlen( $s ) ) {
return new AFPData( AFPData::DFLOAT, 0 );
}
$nospecials = $this->rmspecials( $s );
$val = 1. - ( ( mb_strlen( $nospecials ) / mb_strlen( $s ) ) );
return new AFPData( AFPData::DFLOAT, $val );
}
/**
* @param array $args
* @return AFPData
*/
protected function funcCount( $args ) {
$this->checkEnoughArguments( $args, 'count', 1 );
if ( $args[0]->type == AFPData::DARRAY && count( $args ) == 1 ) {
return new AFPData( AFPData::DINT, count( $args[0]->data ) );
}
if ( count( $args ) == 1 ) {
$count = count( explode( ',', $args[0]->toString() ) );
} else {
$needle = $args[0]->toString();
$haystack = $args[1]->toString();
// T62203: Keep empty parameters from causing PHP warnings
if ( $needle === '' ) {
$count = 0;
} else {
$count = substr_count( $haystack, $needle );
}
}
return new AFPData( AFPData::DINT, $count );
}
/**
* @param array $args
* @return AFPData
* @throws AFPUserVisibleException
*/
protected function funcRCount( $args ) {
$this->checkEnoughArguments( $args, 'rcount', 1 );
if ( count( $args ) == 1 ) {
$count = count( explode( ',', $args[0]->toString() ) );
} else {
$needle = $args[0]->toString();
$haystack = $args[1]->toString();
// Munge the regex
2009-03-25 12:43:53 +00:00
$needle = preg_replace( '!(\\\\\\\\)*(\\\\)?/!', '$1\/', $needle );
$needle = "/$needle/u";
// Suppress and restore needed per T177744
Wikimedia\suppressWarnings();
$count = preg_match_all( $needle, $haystack );
Wikimedia\restoreWarnings();
if ( $count === false ) {
throw new AFPUserVisibleException(
'regexfailure',
$this->mCur->pos,
[ $needle ]
);
}
}
return new AFPData( AFPData::DINT, $count );
}
/**
* Returns an array of matches of needle in the haystack, the first one for the whole regex,
* the other ones for every capturing group.
*
* @param array $args
* @return AFPData An array of matches.
* @throws AFPUserVisibleException
*/
protected function funcGetMatches( $args ) {
$this->checkEnoughArguments( $args, 'get_matches', 2 );
$needle = $args[0]->toString();
$haystack = $args[1]->toString();
// Count the amount of capturing groups in the submitted pattern.
// This way we can return a fixed-dimension array, much easier to manage.
// ToDo: Find a better way to do this.
// First, strip away escaped parentheses
$sanitized = preg_replace( '/(\\\\\\\\)*\\\\\(/', '', $needle );
// Then strip starting parentheses of non-capturing groups, including
// atomics, lookaheads and so on, even if not every of them is supported.
$sanitized = str_replace( '(?', '', $sanitized );
// And also strip "(*", used with backtracking verbs like (*FAIL)
$sanitized = str_replace( '(*', '', $sanitized );
// Finally create an array of falses with dimension = # of capturing groups
$groupscount = substr_count( $sanitized, '(' ) + 1;
$falsy = array_fill( 0, $groupscount, false );
// Munge the regex by escaping slashes
$needle = preg_replace( '!(\\\\\\\\)*(\\\\)?/!', '$1\/', $needle );
$needle = "/$needle/u";
// Suppress and restore are here for the same reason as T177744
Wikimedia\suppressWarnings();
$check = preg_match( $needle, $haystack, $matches );
Wikimedia\restoreWarnings();
if ( $check === false ) {
throw new AFPUserVisibleException(
'regexfailure',
$this->mCur->pos,
[ $needle ]
);
}
// Returned array has non-empty positions identical to the ones returned
// by the third parameter of a standard preg_match call ($matches in this case).
// We want an union with falsy to return a fixed-dimension array.
return AFPData::newFromPHPVar( $matches + $falsy );
}
/**
* @param array $args
* @return AFPData
* @throws AFPUserVisibleException
*/
2009-03-09 12:39:52 +00:00
protected function funcIPInRange( $args ) {
$this->checkEnoughArguments( $args, 'ip_in_range', 2 );
2009-03-09 12:39:52 +00:00
$ip = $args[0]->toString();
$range = $args[1]->toString();
if ( !IP::isValidRange( $range ) ) {
throw new AFPUserVisibleException(
'invalidiprange',
$this->mCur->pos,
[ $range ]
);
}
2009-03-09 12:39:52 +00:00
$result = IP::isInRange( $ip, $range );
return new AFPData( AFPData::DBOOL, $result );
2009-03-09 12:39:52 +00:00
}
/**
* @param array $args
* @return AFPData
*/
protected function funcCCNorm( $args ) {
$this->checkEnoughArguments( $args, 'ccnorm', 1 );
$s = $args[0]->toString();
$s = html_entity_decode( $s, ENT_QUOTES, 'UTF-8' );
$s = $this->ccnorm( $s );
return new AFPData( AFPData::DSTRING, $s );
}
/**
* @param array $args
* @return AFPData
*/
protected function funcSanitize( $args ) {
$this->checkEnoughArguments( $args, 'sanitize', 1 );
$s = $args[0]->toString();
$s = html_entity_decode( $s, ENT_QUOTES, 'UTF-8' );
$s = Sanitizer::decodeCharReferences( $s );
return new AFPData( AFPData::DSTRING, $s );
}
/**
* @param array $args
* @return AFPData
*/
protected function funcContainsAny( $args ) {
$this->checkEnoughArguments( $args, 'contains_any', 2 );
$s = array_shift( $args );
return new AFPData( AFPData::DBOOL, self::contains( $s, $args, true ) );
}
/**
* @param array $args
* @return AFPData
*/
protected function funcContainsAll( $args ) {
$this->checkEnoughArguments( $args, 'contains_all', 2 );
$s = array_shift( $args );
return new AFPData( AFPData::DBOOL, self::contains( $s, $args, false, false ) );
}
/**
* Normalize and search a string for multiple substrings in OR mode
*
* @param array $args
* @return AFPData
*/
protected function funcCCNormContainsAny( $args ) {
$this->checkEnoughArguments( $args, 'ccnorm_contains_any', 2 );
$s = array_shift( $args );
return new AFPData( AFPData::DBOOL, self::contains( $s, $args, true, true ) );
}
/**
* Normalize and search a string for multiple substrings in AND mode
*
* @param array $args
* @return AFPData
*/
protected function funcCCNormContainsAll( $args ) {
$this->checkEnoughArguments( $args, 'ccnorm_contains_all', 2 );
$s = array_shift( $args );
return new AFPData( AFPData::DBOOL, self::contains( $s, $args, false, true ) );
}
/**
* Search for substrings in a string
*
* Use is_any to determine wether to use logic OR (true) or AND (false).
*
* Use normalize = true to make use of ccnorm and
* normalize both sides of the search.
*
* @param AFPData $string
* @param AFPData[] $values
* @param bool $is_any
* @param bool $normalize
*
* @return bool
*/
protected static function contains( $string, $values, $is_any = true, $normalize = false ) {
$string = $string->toString();
if ( $string === '' ) {
return false;
}
if ( $normalize ) {
$string = self::ccnorm( $string );
}
foreach ( $values as $needle ) {
$needle = $needle->toString();
if ( $normalize ) {
$needle = self::ccnorm( $needle );
}
if ( $needle === '' ) {
// T62203: Keep empty parameters from causing PHP warnings
continue;
}
$is_found = strpos( $string, $needle ) !== false;
if ( $is_found === $is_any ) {
// If I'm here and it's ANY (OR) => something is found.
// If I'm here and it's ALL (AND) => nothing is found.
// In both cases, we've had enough.
return $is_found;
}
}
// If I'm here and it's ANY (OR) => nothing was found: return false ($is_any is true)
// If I'm here and it's ALL (AND) => everything was found: return true ($is_any is false)
return !$is_any;
}
/**
* @param array $args
* @return AFPData
*/
protected function funcEqualsToAny( $args ) {
$this->checkEnoughArguments( $args, 'equals_to_any', 2 );
$s = array_shift( $args );
return new AFPData( AFPData::DBOOL, self::equalsToAny( $s, $args ) );
}
/**
* Check if the given string is equals to any of the following strings
*
* @param AFPData $string
* @param AFPData[] $values
*
* @return bool
*/
protected static function equalsToAny( $string, $values ) {
$string = $string->toString();
foreach ( $values as $needle ) {
$needle = $needle->toString();
if ( $string === $needle ) {
return true;
}
}
return false;
}
/**
* @param string $s
* @return mixed
*/
protected static function ccnorm( $s ) {
// Instantiate a single version of the equivset so the data is only loaded once.
if ( !self::$equivset ) {
self::$equivset = new Equivset();
}
return self::$equivset->normalize( $s );
}
/**
* @param string $s
* @return array|string
*/
protected function rmspecials( $s ) {
return preg_replace( '/[^\p{L}\p{N}]/u', '', $s );
}
/**
* @param string $s
* @return array|string
*/
protected function rmdoubles( $s ) {
return preg_replace( '/(.)\1+/us', '\1', $s );
}
/**
* @param string $s
* @return array|string
*/
2009-02-18 19:42:01 +00:00
protected function rmwhitespace( $s ) {
return preg_replace( '/\s+/u', '', $s );
}
/**
* @param array $args
* @return AFPData
*/
protected function funcRMSpecials( $args ) {
$this->checkEnoughArguments( $args, 'rmspecials', 1 );
$s = $args[0]->toString();
return new AFPData( AFPData::DSTRING, $this->rmspecials( $s ) );
}
/**
* @param array $args
* @return AFPData
*/
2009-02-18 19:42:01 +00:00
protected function funcRMWhitespace( $args ) {
$this->checkEnoughArguments( $args, 'rmwhitespace', 1 );
2009-02-18 19:42:01 +00:00
$s = $args[0]->toString();
return new AFPData( AFPData::DSTRING, $this->rmwhitespace( $s ) );
2009-02-18 19:42:01 +00:00
}
/**
* @param array $args
* @return AFPData
*/
protected function funcRMDoubles( $args ) {
$this->checkEnoughArguments( $args, 'rmdoubles', 1 );
$s = $args[0]->toString();
return new AFPData( AFPData::DSTRING, $this->rmdoubles( $s ) );
}
/**
* @param array $args
* @return AFPData
*/
protected function funcNorm( $args ) {
$this->checkEnoughArguments( $args, 'norm', 1 );
$s = $args[0]->toString();
$s = $this->ccnorm( $s );
$s = $this->rmdoubles( $s );
$s = $this->rmspecials( $s );
2009-02-18 19:42:01 +00:00
$s = $this->rmwhitespace( $s );
return new AFPData( AFPData::DSTRING, $s );
}
/**
* @param array $args
* @return AFPData
*/
protected function funcSubstr( $args ) {
$this->checkEnoughArguments( $args, 'substr', 2 );
$s = $args[0]->toString();
$offset = $args[1]->toInt();
$length = isset( $args[2] ) ? $args[2]->toInt() : null;
$result = mb_substr( $s, $offset, $length );
return new AFPData( AFPData::DSTRING, $result );
}
/**
* @param array $args
* @return AFPData
*/
protected function funcStrPos( $args ) {
$this->checkEnoughArguments( $args, 'strpos', 2 );
$haystack = $args[0]->toString();
$needle = $args[1]->toString();
$offset = isset( $args[2] ) ? $args[2]->toInt() : 0;
// T62203: Keep empty parameters from causing PHP warnings
if ( $needle === '' ) {
return new AFPData( AFPData::DINT, -1 );
}
$result = mb_strpos( $haystack, $needle, $offset );
if ( $result === false ) {
$result = -1;
}
return new AFPData( AFPData::DINT, $result );
}
/**
* @param array $args
* @return AFPData
*/
protected function funcStrReplace( $args ) {
$this->checkEnoughArguments( $args, 'str_replace', 3 );
$subject = $args[0]->toString();
$search = $args[1]->toString();
$replace = $args[2]->toString();
return new AFPData( AFPData::DSTRING, str_replace( $search, $replace, $subject ) );
}
/**
* @param array $args
* @return AFPData
*/
protected function funcStrRegexEscape( $args ) {
$this->checkEnoughArguments( $args, 'rescape', 1 );
$string = $args[0]->toString();
// preg_quote does not need the second parameter, since rlike takes
// care of the delimiter symbol itself
return new AFPData( AFPData::DSTRING, preg_quote( $string ) );
}
/**
* @param array $args
* @return mixed
*/
protected function funcSetVar( $args ) {
$this->checkEnoughArguments( $args, 'set_var', 2 );
$varName = $args[0]->toString();
$value = $args[1];
$this->setUserVariable( $varName, $value );
return $value;
}
/**
* @param array $args
* @return AFPData
*/
protected function castString( $args ) {
$this->checkEnoughArguments( $args, 'string', 1 );
return AFPData::castTypes( $args[0], AFPData::DSTRING );
}
/**
* @param array $args
* @return AFPData
*/
protected function castInt( $args ) {
$this->checkEnoughArguments( $args, 'int', 1 );
return AFPData::castTypes( $args[0], AFPData::DINT );
}
/**
* @param array $args
* @return AFPData
*/
protected function castFloat( $args ) {
$this->checkEnoughArguments( $args, 'float', 1 );
return AFPData::castTypes( $args[0], AFPData::DFLOAT );
}
/**
* @param array $args
* @return AFPData
*/
protected function castBool( $args ) {
$this->checkEnoughArguments( $args, 'bool', 1 );
return AFPData::castTypes( $args[0], AFPData::DBOOL );
}
}