mediawiki-extensions-AbuseF.../AbuseFilter.parser.php

2264 lines
54 KiB
PHP
Raw Normal View History

<?php
/**
Abuse filter parser.
Copyright © Victor Vasiliev, 2008. Based on ideas by Andrew Garrett Distributed under GNU GPL v2 terms.
Types of token:
* T_NONE - special-purpose token
* T_BRACE - ( or )
* T_COMMA - ,
* T_OP - operator like + or ^
* T_NUMBER - number
* T_STRING - string, in "" or ''
* T_KEYWORD - keyword
* T_ID - identifier
* T_STATEMENT_SEPARATOR - ;
* T_SQUARE_BRACKETS - [ or ]
Levels of parsing:
* Entry - catches unexpected characters
* Semicolon - ;
* Set - :=
* Conditionls (IF) - if-then-else-end, cond ? a :b
* BoolOps (BO) - &, |, ^
* CompOps (CO) - ==, !=, ===, !==, >, <, >=, <=
* SumRel (SR) - +, -
* MulRel (MR) - *, /, %
* Pow (P) - **
* BoolNeg (BN) - ! operation
* SpecialOperators (SO) - in and like
* Unarys (U) - plus and minus in cases like -5 or -(2 * +2)
* ListElement (LE) - list[number]
* Braces (B) - ( and )
* Functions (F)
* Atom (A) - return value
*/
class AFPToken {
// Types of tken
const TNone = 'T_NONE';
const TID = 'T_ID';
const TKeyword = 'T_KEYWORD';
const TString = 'T_STRING';
const TInt = 'T_INT';
const TFloat = 'T_FLOAT';
const TOp = 'T_OP';
const TBrace = 'T_BRACE';
const TSquareBracket = 'T_SQUARE_BRACKET';
const TComma = 'T_COMMA';
const TStatementSeparator = 'T_STATEMENT_SEPARATOR';
public $type;
public $value;
public $pos;
public function __construct( $type = self::TNone, $value = null, $pos = 0 ) {
$this->type = $type;
$this->value = $value;
$this->pos = $pos;
}
}
class AFPData {
// Datatypes
const DInt = 'int';
const DString = 'string';
const DNull = 'null';
const DBool = 'bool';
const DFloat = 'float';
const DList = 'list';
// Translation table mapping shell-style wildcards to PCRE equivalents.
// Derived from <http://www.php.net/manual/en/function.fnmatch.php#100207>
private static $wildcardMap = array(
'\*' => '.*',
'\.' => '\.',
'\?' => '.',
'\[' => '[',
'\[\!' => '[^',
'\\' => '\\\\',
'\]' => ']',
);
public $type;
public $data;
/**
* @param string $type
* @param null $val
*/
public function __construct( $type = self::DNull, $val = null ) {
$this->type = $type;
$this->data = $val;
}
/**
* @param $var
* @return AFPData
* @throws AFPException
*/
public static function newFromPHPVar( $var ) {
if ( is_string( $var ) ) {
return new AFPData( self::DString, $var );
} elseif ( is_int( $var ) ) {
return new AFPData( self::DInt, $var );
} elseif ( is_float( $var ) ) {
return new AFPData( self::DFloat, $var );
} elseif ( is_bool( $var ) ) {
return new AFPData( self::DBool, $var );
} elseif ( is_array( $var ) ) {
$result = array();
foreach ( $var as $item ) {
$result[] = self::newFromPHPVar( $item );
}
return new AFPData( self::DList, $result );
} elseif ( is_null( $var ) ) {
return new AFPData();
} else {
throw new AFPException(
'Data type ' . gettype( $var ) . ' is not supported by AbuseFilter'
);
}
}
/**
* @return AFPData
*/
public function dup() {
return new AFPData( $this->type, $this->data );
}
/**
* @param $orig AFPData
* @param $target
* @return AFPData
*/
public static function castTypes( $orig, $target ) {
if ( $orig->type == $target ) {
return $orig->dup();
}
if ( $target == self::DNull ) {
return new AFPData();
}
if ( $orig->type == self::DList ) {
if ( $target == self::DBool ) {
return new AFPData( self::DBool, (bool)count( $orig->data ) );
}
if ( $target == self::DFloat ) {
return new AFPData( self::DFloat, floatval( count( $orig->data ) ) );
}
if ( $target == self::DInt ) {
return new AFPData( self::DInt, intval( count( $orig->data ) ) );
}
if ( $target == self::DString ) {
$s = '';
foreach ( $orig->data as $item ) {
$s .= $item->toString() . "\n";
}
return new AFPData( self::DString, $s );
}
}
if ( $target == self::DBool ) {
return new AFPData( self::DBool, (bool)$orig->data );
}
if ( $target == self::DFloat ) {
return new AFPData( self::DFloat, floatval( $orig->data ) );
}
if ( $target == self::DInt ) {
return new AFPData( self::DInt, intval( $orig->data ) );
}
if ( $target == self::DString ) {
return new AFPData( self::DString, strval( $orig->data ) );
}
if ( $target == self::DList ) {
return new AFPData( self::DList, array( $orig ) );
}
}
/**
* @param $value AFPData
* @return AFPData
*/
public static function boolInvert( $value ) {
return new AFPData( self::DBool, !$value->toBool() );
}
/**
* @param $base AFPData
* @param $exponent AFPData
* @return AFPData
*/
public static function pow( $base, $exponent ) {
return new AFPData( self::DFloat, pow( $base->toFloat(), $exponent->toFloat() ) );
}
/**
* @param $a AFPData
* @param $b AFPData
* @return AFPData
*/
public static function keywordIn( $a, $b ) {
$a = $a->toString();
$b = $b->toString();
if ( $a == '' || $b == '' ) {
return new AFPData( self::DBool, false );
}
return new AFPData( self::DBool, strpos( $b, $a ) !== false );
}
/**
* @param $a AFPData
* @param $b AFPData
* @return AFPData
*/
public static function keywordContains( $a, $b ) {
$a = $a->toString();
$b = $b->toString();
if ( $a == '' || $b == '' ) {
return new AFPData( self::DBool, false );
}
return new AFPData( self::DBool, strpos( $a, $b ) !== false );
}
/**
* @param $value
* @param $list
* @return bool
*/
public static function listContains( $value, $list ) {
// Should use built-in PHP function somehow
foreach ( $list->data as $item ) {
if ( self::equals( $value, $item ) ) {
return true;
}
}
return false;
}
/**
* @param $d1 AFPData
* @param $d2 AFPData
* @return bool
*/
public static function equals( $d1, $d2 ) {
return $d1->type != self::DList && $d2->type != self::DList &&
$d1->toString() === $d2->toString();
}
/**
* @param $str AFPData
* @param $pattern AFPData
* @return AFPData
*/
public static function keywordLike( $str, $pattern ) {
$str = $str->toString();
$pattern = '#^' . strtr( preg_quote( $pattern->toString(), '#' ), self::$wildcardMap ) . '$#u';
wfSuppressWarnings();
$result = preg_match( $pattern, $str );
wfRestoreWarnings();
return new AFPData( self::DBool, (bool)$result );
}
/**
* @param $str AFPData
* @param $regex AFPData
* @param $pos
* @param $insensitive bool
* @return AFPData
* @throws Exception
*/
public static function keywordRegex( $str, $regex, $pos, $insensitive = false ) {
$str = $str->toString();
$pattern = $regex->toString();
2009-03-25 12:43:53 +00:00
$pattern = preg_replace( '!(\\\\\\\\)*(\\\\)?/!', '$1\/', $pattern );
2009-03-22 10:34:54 +00:00
$pattern = "/$pattern/u";
if( $insensitive ) {
$pattern .= 'i';
}
$handler = new AFPRegexErrorHandler( $pattern, $pos );
try {
$handler->install();
$result = preg_match( $pattern, $str );
$handler->restore();
} catch ( Exception $e ) {
$handler->restore();
throw $e;
}
return new AFPData( self::DBool, (bool)$result );
}
/**
* @param $str
* @param $regex
* @param $pos
* @return AFPData
*/
public static function keywordRegexInsensitive( $str, $regex, $pos ) {
return self::keywordRegex( $str, $regex, $pos, true );
}
/**
* @param $data AFPData
* @return AFPData
*/
public static function unaryMinus( $data ) {
if ( $data->type == self::DInt ) {
return new AFPData( $data->type, - $data->toInt() );
} else {
return new AFPData( $data->type, - $data->toFloat() );
}
}
/**
* @param $a AFPData
* @param $b AFPData
* @param $op string
* @return AFPData
* @throws AFPException
*/
public static function boolOp( $a, $b, $op ) {
$a = $a->toBool();
$b = $b->toBool();
if ( $op == '|' ) {
return new AFPData( self::DBool, $a || $b );
}
if ( $op == '&' ) {
return new AFPData( self::DBool, $a && $b );
}
if ( $op == '^' ) {
return new AFPData( self::DBool, $a xor $b );
}
2009-01-26 23:32:46 +00:00
throw new AFPException( "Invalid boolean operation: {$op}" ); // Should never happen.
}
/**
* @param $a AFPData
* @param $b AFPData
* @param $op string
* @return AFPData
* @throws AFPException
*/
public static function compareOp( $a, $b, $op ) {
if ( $op == '==' || $op == '=' ) {
return new AFPData( self::DBool, self::equals( $a, $b ) );
}
if ( $op == '!=' ) {
return new AFPData( self::DBool, !self::equals( $a, $b ) );
}
if ( $op == '===' ) {
return new AFPData( self::DBool, $a->type == $b->type && self::equals( $a, $b ) );
}
if ( $op == '!==' ) {
return new AFPData( self::DBool, $a->type != $b->type || !self::equals( $a, $b ) );
}
$a = $a->toString();
$b = $b->toString();
if ( $op == '>' ) {
return new AFPData( self::DBool, $a > $b );
}
if ( $op == '<' ) {
return new AFPData( self::DBool, $a < $b );
}
if ( $op == '>=' ) {
return new AFPData( self::DBool, $a >= $b );
}
if ( $op == '<=' ) {
return new AFPData( self::DBool, $a <= $b );
}
2009-01-26 23:32:46 +00:00
throw new AFPException( "Invalid comparison operation: {$op}" ); // Should never happen
}
/**
* @param $a AFPData
* @param $b AFPData
* @param $op string
* @param $pos
* @return AFPData
* @throws AFPUserVisibleException
* @throws AFPException
*/
public static function mulRel( $a, $b, $op, $pos ) {
// Figure out the type.
if ( $a->type == self::DFloat || $b->type == self::DFloat ||
$a->toFloat() != $a->toString() || $b->toFloat() != $b->toString() ) {
$type = self::DFloat;
$a = $a->toFloat();
$b = $b->toFloat();
} else {
$type = self::DInt;
$a = $a->toInt();
$b = $b->toInt();
}
if ( $op != '*' && $b == 0 ) {
throw new AFPUserVisibleException( 'dividebyzero', $pos, array( $a ) );
}
if ( $op == '*' ) {
$data = $a * $b;
} elseif ( $op == '/' ) {
$data = $a / $b;
} elseif ( $op == '%' ) {
$data = $a % $b;
} else {
2009-01-26 23:32:46 +00:00
throw new AFPException( "Invalid multiplication-related operation: {$op}" ); // Should never happen
}
if ( $type == self::DInt ) {
$data = intval( $data );
} else {
$data = floatval( $data );
}
return new AFPData( $type, $data );
}
/**
* @param $a AFPData
* @param $b AFPData
* @return AFPData
*/
public static function sum( $a, $b ) {
if ( $a->type == self::DString || $b->type == self::DString ) {
return new AFPData( self::DString, $a->toString() . $b->toString() );
} elseif ( $a->type == self::DList && $b->type == self::DList ) {
return new AFPData( self::DList, array_merge( $a->toList(), $b->toList() ) );
} else {
return new AFPData( self::DFloat, $a->toFloat() + $b->toFloat() );
}
}
/**
* @param $a AFPData
* @param $b AFPData
* @return AFPData
*/
public static function sub( $a, $b ) {
return new AFPData( self::DFloat, $a->toFloat() - $b->toFloat() );
}
/** Convert shorteners */
/**
* @throws MWException
* @return mixed
*/
public function toNative() {
switch( $this->type ) {
case self::DBool:
return $this->toBool();
case self::DString:
return $this->toString();
case self::DFloat:
return $this->toFloat();
case self::DInt:
return $this->toInt();
case self::DList:
$input = $this->toList();
$output = array();
foreach( $input as $item ) {
$output[] = $item->toNative();
}
return $output;
case self::DNull:
return null;
default:
throw new MWException( "Unknown type" );
}
}
/**
* @return bool
*/
public function toBool() {
return self::castTypes( $this, self::DBool )->data;
}
/**
* @return string
*/
public function toString() {
return self::castTypes( $this, self::DString )->data;
}
/**
* @return float
*/
public function toFloat() {
return self::castTypes( $this, self::DFloat )->data;
}
/**
* @return int
*/
public function toInt() {
return self::castTypes( $this, self::DInt )->data;
}
public function toList() {
return self::castTypes( $this, self::DList )->data;
}
}
class AFPParserState {
public $pos, $token, $lastInput;
public function __construct( $token, $pos ) {
$this->token = $token;
$this->pos = $pos;
$this->lastInput = AbuseFilterParser::$lastHandledToken;
}
}
class AFPException extends MWException { }
2009-01-26 23:32:46 +00:00
// Exceptions that we might conceivably want to report to ordinary users
// (i.e. exceptions that don't represent bugs in the extension itself)
class AFPUserVisibleException extends AFPException {
/**
* @param string $exception_id
* @param int $position
* @param array $params
*/
function __construct( $exception_id, $position, $params ) {
// Give grep a chance to find the usages:
// abusefilter-exception-unexpectedatend, abusefilter-exception-expectednotfound
// abusefilter-exception-unrecognisedkeyword, abusefilter-exception-unexpectedtoken
// abusefilter-exception-unclosedstring, abusefilter-exception-invalidoperator
// abusefilter-exception-unrecognisedtoken, abusefilter-exception-noparams
// abusefilter-exception-dividebyzero, abusefilter-exception-unrecognisedvar
// abusefilter-exception-notenoughargs, abusefilter-exception-regexfailure
// abusefilter-exception-overridebuiltin, abusefilter-exception-outofbounds
// abusefilter-exception-notlist
$msg = wfMessage(
'abusefilter-exception-' . $exception_id,
array_merge( array( $position ), $params )
)->text();
2009-01-26 23:32:46 +00:00
parent::__construct( $msg );
$this->mExceptionID = $exception_id;
$this->mPosition = $position;
$this->mParams = $params;
}
}
class AFPRegexErrorHandler {
function __construct( $regex, $pos ) {
$this->regex = $regex;
$this->pos = $pos;
}
/**
* @param $errno
* @param $errstr
* @param $errfile
* @param $errline
* @param $context
* @return bool
* @throws AFPUserVisibleException
*/
function handleError( $errno, $errstr, $errfile, $errline, $context ) {
if ( error_reporting() == 0 ) {
return true;
}
throw new AFPUserVisibleException(
'regexfailure',
$this->pos,
array( $errstr, $this->regex )
);
}
function install() {
set_error_handler( array( $this, 'handleError' ) );
}
function restore() {
restore_error_handler();
}
}
class AbuseFilterParser {
public $mParams, $mCode, $mTokens, $mPos, $mCur, $mShortCircuit, $mAllowShort, $mLen;
/**
* @var AbuseFilterVariableHolder
*/
public $mVars;
// length,lcase,ucase,ccnorm,rmdoubles,specialratio,rmspecials,norm,count
static $mFunctions = array(
'lcase' => 'funcLc',
'ucase' => 'funcUc',
'length' => 'funcLen',
'string' => 'castString',
'int' => 'castInt',
'float' => 'castFloat',
'bool' => 'castBool',
'norm' => 'funcNorm',
'ccnorm' => 'funcCCNorm',
'specialratio' => 'funcSpecialRatio',
'rmspecials' => 'funcRMSpecials',
'rmdoubles' => 'funcRMDoubles',
'rmwhitespace' => 'funcRMWhitespace',
'count' => 'funcCount',
'rcount' => 'funcRCount',
2009-03-09 12:39:52 +00:00
'ip_in_range' => 'funcIPInRange',
'contains_any' => 'funcContainsAny',
'substr' => 'funcSubstr',
'strlen' => 'funcLen',
'strpos' => 'funcStrPos',
'str_replace' => 'funcStrReplace',
'rescape' => 'funcStrRegexEscape',
'set' => 'funcSetVar',
'set_var' => 'funcSetVar',
);
// Functions that affect parser state, and shouldn't be cached.
static $ActiveFunctions = array(
'funcSetVar',
);
// Order is important. The punctuation-matching regex requires that
// ** comes before *, etc. They are sorted to make it easy to spot
// such errors.
static $mOps = array(
'!==', '!=', '!', // Inequality
'**', '*', // Multiplication/exponentiation
'/', '+', '-', '%', // Other arithmetic
'&', '|', '^', // Logic
':=', // Setting
'?', ':', // Ternery
'<=', '<', // Less than
'>=', '>', // Greater than
'===', '==', '=', // Equality
);
static $mKeywords = array(
'in', 'like', 'true', 'false', 'null', 'contains', 'matches',
'rlike', 'irlike', 'regex', 'if', 'then', 'else', 'end',
);
static $parserCache = array();
static $funcCache = array();
static $lastHandledToken = array();
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 $vars AbuseFilterVariableHolder
*/
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;
}
}
public function resetState() {
$this->mParams = array();
$this->mCode = '';
$this->mTokens = array();
$this->mVars = new AbuseFilterVariableHolder;
$this->mPos = 0;
$this->mShortCircuit = false;
$this->mAllowShort = true;
}
/**
* @param $filter
* @return array|bool
*/
public function checkSyntax( $filter ) {
try {
$origAS = $this->mAllowShort;
$this->mAllowShort = false;
$this->parse( $filter );
} catch ( AFPUserVisibleException $excep ) {
$this->mAllowShort = $origAS;
return array( $excep->getMessage(), $excep->mPosition );
}
$this->mAllowShort = $origAS;
return true;
}
/**
* @param $name
* @param $value
*/
public function setVar( $name, $value ) {
$this->mVars->setVar( $name, $value );
}
/**
* @param $vars
*/
public function setVars( $vars ) {
if ( is_array( $vars ) ) {
foreach ( $vars as $name => $var ) {
$this->setVar( $name, $var );
}
} elseif ( $vars instanceof AbuseFilterVariableHolder ) {
$this->mVars->addHolders( $vars );
}
}
/**
* @return AFPToken
*/
protected function move( ) {
wfProfileIn( __METHOD__ );
list( $val, $type, , $offset ) = self::nextToken( $this->mCode, $this->mPos );
$token = new AFPToken( $type, $val, $this->mPos );
$this->mPos = $offset;
wfProfileOut( __METHOD__ );
return $this->mCur = $token;
}
/**
* 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;
self::$lastHandledToken = $state->lastInput;
}
/**
* @return mixed
* @throws AFPUserVisibleException
*/
protected function skipOverBraces() {
if ( !( $this->mCur->type == AFPToken::TBrace && $this->mCur->value == '(' ) || !$this->mShortCircuit ) {
return;
}
$braces = 1;
wfProfileIn( __METHOD__ );
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--;
}
}
}
wfProfileOut( __METHOD__ );
if ( !( $this->mCur->type == AFPToken::TBrace && $this->mCur->value == ')' ) )
throw new AFPUserVisibleException( 'expectednotfound', $this->mCur->pos, array( ')' ) );
}
/**
* @param $code
* @return bool
*/
public function parse( $code ) {
return $this->intEval( $code )->toBool();
}
/**
* @param $filter
* @return string
*/
public function evaluateExpression( $filter ) {
return $this->intEval( $filter )->toString();
}
/**
* @param $code
* @return AFPData
*/
function intEval( $code ) {
// Setup, resetting
$this->mCode = $code;
$this->mPos = 0;
$this->mLen = strlen( $code );
$this->mShortCircuit = false;
$result = new AFPData();
$this->doLevelEntry( $result );
return $result;
}
/**
* @param $a
* @param $b
* @return int
*/
static function lengthCompare( $a, $b ) {
if ( strlen( $a ) == strlen( $b ) ) {
return 0;
}
return ( strlen( $a ) < strlen( $b ) ) ? - 1 : 1;
}
/* Levels */
/**
* Handles unexpected characters after the expression
*
* @param $result AFPData
* @throws AFPUserVisibleException
*/
protected function doLevelEntry( &$result ) {
$this->doLevelSemicolon( $result );
if ( $this->mCur->type != AFPToken::TNone ) {
throw new AFPUserVisibleException( 'unexpectedatend', $this->mCur->pos, array( $this->mCur->type ) );
}
}
/**
* Handles multiple expressions
* @param $result AFPData
*/
protected function doLevelSemicolon( &$result ) {
do {
$this->move();
if ( $this->mCur->type != AFPToken::TStatementSeparator ) {
$this->doLevelSet( $result );
}
} while ( $this->mCur->type == AFPToken::TStatementSeparator );
}
/**
* Handles multiple expressions
*
* @param $result AFPData
* @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,
2011-02-10 17:32:57 +00:00
array( $varname )
);
}
$list = $this->mVars->getVar( $varname );
if ( $list->type != AFPData::DList ) {
2009-05-22 06:42:10 +00:00
throw new AFPUserVisibleException( 'notlist', $this->mCur->pos, array() );
}
$list = $list->toList();
$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,
array( ']', $this->mCur->type, $this->mCur->value ) );
}
if ( count( $list ) <= $idx ) {
throw new AFPUserVisibleException( 'outofbounds', $this->mCur->pos,
array( $idx, count( $result->data ) ) );
}
}
$this->move();
if ( $this->mCur->type == AFPToken::TOp && $this->mCur->value == ':=' ) {
$this->move();
$this->doLevelSet( $result );
if ( $idx === 'new' ) {
$list[] = $result;
} else {
$list[$idx] = $result;
}
$this->setUserVariable( $varname, new AFPData( AFPData::DList, $list ) );
return;
} else {
$this->setState( $prev );
}
} else {
$this->setState( $prev );
}
}
$this->doLevelConditions( $result );
}
/**
* @param $result AFPData
* @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,
array(
'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,
array(
'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,
array(
'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,
array(
':',
$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;
}
}
}
}
/**
* @param $result AFPData
*/
protected function doLevelBoolOps( &$result ) {
$this->doLevelCompares( $result );
$ops = array( '&', '|', '^' );
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 on with & is false
if ( ( $op == '&' && !$result->toBool() ) || ( $op == '|' && $result->toBool() ) ) {
wfProfileIn( __METHOD__ . '-shortcircuit' );
$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() );
wfProfileOut( __METHOD__ . '-shortcircuit' );
2009-03-19 00:18:03 +00:00
continue;
}
$this->doLevelCompares( $r2 );
wfProfileIn( __METHOD__ );
$result = AFPData::boolOp( $result, $r2, $op );
wfProfileOut( __METHOD__ );
}
}
/**
* @param $result
*/
protected function doLevelCompares( &$result ) {
AbuseFilter::triggerLimiter();
$this->doLevelSumRels( $result );
$ops = array( '==', '===', '!=', '!==', '<', '>', '<=', '>=', '=' );
while ( $this->mCur->type == AFPToken::TOp && in_array( $this->mCur->value, $ops ) ) {
$op = $this->mCur->value;
$this->move();
$r2 = new AFPData();
$this->doLevelSumRels( $r2 );
wfProfileIn( __METHOD__ );
$result = AFPData::compareOp( $result, $r2, $op );
wfProfileOut( __METHOD__ );
}
}
/**
* @param $result
*/
protected function doLevelSumRels( &$result ) {
$this->doLevelMulRels( $result );
wfProfileIn( __METHOD__ );
$ops = array( '+', '-' );
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 ( $op == '+' ) {
$result = AFPData::sum( $result, $r2 );
}
if ( $op == '-' ) {
$result = AFPData::sub( $result, $r2 );
}
}
wfProfileOut( __METHOD__ );
}
/**
* @param $result
*/
protected function doLevelMulRels( &$result ) {
$this->doLevelPow( $result );
wfProfileIn( __METHOD__ );
$ops = array( '*', '/', '%' );
while ( $this->mCur->type == AFPToken::TOp && in_array( $this->mCur->value, $ops ) ) {
$op = $this->mCur->value;
$this->move();
$r2 = new AFPData();
$this->doLevelPow( $r2 );
$result = AFPData::mulRel( $result, $r2, $op, $this->mCur->pos );
}
wfProfileOut( __METHOD__ );
}
/**
* @param $result
*/
protected function doLevelPow( &$result ) {
$this->doLevelBoolInvert( $result );
wfProfileIn( __METHOD__ );
while ( $this->mCur->type == AFPToken::TOp && $this->mCur->value == '**' ) {
$this->move();
$expanent = new AFPData();
$this->doLevelBoolInvert( $expanent );
$result = AFPData::pow( $result, $expanent );
}
wfProfileOut( __METHOD__ );
}
/**
* @param $result
*/
protected function doLevelBoolInvert( &$result ) {
if ( $this->mCur->type == AFPToken::TOp && $this->mCur->value == '!' ) {
$this->move();
$this->doLevelSpecialWords( $result );
wfProfileIn( __METHOD__ );
$result = AFPData::boolInvert( $result );
wfProfileOut( __METHOD__ );
} else {
$this->doLevelSpecialWords( $result );
}
}
/**
* @param $result
*/
protected function doLevelSpecialWords( &$result ) {
$this->doLevelUnarys( $result );
$keyword = strtolower( $this->mCur->value );
$specwords = array(
'in' => 'keywordIn',
'like' => 'keywordLike',
'matches' => 'keywordLike',
'contains' => 'keywordContains',
'rlike' => 'keywordRegex',
'irlike' => 'keywordRegexInsensitive',
'regex' => 'keywordRegex'
);
if ( $this->mCur->type == AFPToken::TKeyword && in_array( $keyword, array_keys( $specwords ) ) ) {
$func = $specwords[$keyword];
$this->move();
$r2 = new AFPData();
$this->doLevelUnarys( $r2 );
if ( $this->mShortCircuit ) {
return; // The result doesn't matter.
}
wfProfileIn( __METHOD__ );
wfProfileIn( __METHOD__ . "-$func" );
$result = AFPData::$func( $result, $r2, $this->mCur->pos );
wfProfileOut( __METHOD__ . "-$func" );
wfProfileOut( __METHOD__ );
}
}
/**
* @param $result
*/
protected function doLevelUnarys( &$result ) {
$op = $this->mCur->value;
if ( $this->mCur->type == AFPToken::TOp && ( $op == "+" || $op == "-" ) ) {
$this->move();
$this->doLevelListElements( $result );
wfProfileIn( __METHOD__ );
if ( $op == '-' ) {
$result = AFPData::unaryMinus( $result );
}
wfProfileOut( __METHOD__ );
} else {
$this->doLevelListElements( $result );
}
}
/**
* @param $result
* @throws AFPUserVisibleException
*/
protected function doLevelListElements( &$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,
array( ']', $this->mCur->type, $this->mCur->value ) );
}
$idx = $idx->toInt();
if ( $result->type == AFPData::DList ) {
if ( count( $result->data ) <= $idx ) {
throw new AFPUserVisibleException( 'outofbounds', $this->mCur->pos,
array( $idx, count( $result->data ) ) );
}
$result = $result->data[$idx];
} else {
throw new AFPUserVisibleException( 'notlist', $this->mCur->pos, array() );
}
$this->move();
}
}
/**
* @param $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,
array( ')', $this->mCur->type, $this->mCur->value )
);
$this->move();
} else {
$this->doLevelFunction( $result );
}
}
/**
* @param $result
* @throws AFPUserVisibleException
*/
protected function doLevelFunction( &$result ) {
if ( $this->mCur->type == AFPToken::TID && isset( self::$mFunctions[$this->mCur->value] ) ) {
wfProfileIn( __METHOD__ );
$func = self::$mFunctions[$this->mCur->value];
$this->move();
if ( $this->mCur->type != AFPToken::TBrace || $this->mCur->value != '(' ) {
throw new AFPUserVisibleException( 'expectednotfound',
$this->mCur->pos,
array(
'(',
$this->mCur->type,
$this->mCur->value
)
);
}
if ( $this->mShortCircuit ) {
$this->skipOverBraces();
$this->move();
wfProfileOut( __METHOD__ );
return; // The result doesn't matter.
}
wfProfileIn( __METHOD__ . '-loadargs' );
$args = array();
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,
array(
')',
$this->mCur->type,
$this->mCur->value
)
);
}
$this->move();
wfProfileOut( __METHOD__ . '-loadargs' );
wfProfileIn( __METHOD__ . "-$func" );
$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 = array();
}
wfProfileOut( __METHOD__ . "-$func" );
wfProfileOut( __METHOD__ );
} else {
$this->doLevelAtom( $result );
}
}
/**
* @param $result
* @throws AFPUserVisibleException
* @return AFPData
*/
protected function doLevelAtom( &$result ) {
wfProfileIn( __METHOD__ );
$tok = $this->mCur->value;
switch( $this->mCur->type ) {
case AFPToken::TID:
if ( $this->mShortCircuit ) {
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 {
wfProfileOut( __METHOD__ );
throw new AFPUserVisibleException(
'unrecognisedkeyword',
$this->mCur->pos,
array( $tok )
);
}
break;
case AFPToken::TNone:
wfProfileOut( __METHOD__ );
return; // Handled at entry level
case AFPToken::TBrace:
if ( $this->mCur->value == ')' ) {
wfProfileOut( __METHOD__ );
return; // Handled at the entry level
}
case AFPToken::TSquareBracket:
if ( $this->mCur->value == '[' ) {
$list = array();
for ( ; ; ) {
$this->move();
if ( $this->mCur->type == AFPToken::TSquareBracket && $this->mCur->value == ']' ) {
break;
}
$item = new AFPData();
$this->doLevelSet( $item );
$list[] = $item;
if ( $this->mCur->type == AFPToken::TSquareBracket && $this->mCur->value == ']' ) {
break;
}
if ( $this->mCur->type != AFPToken::TComma ) {
throw new AFPUserVisibleException(
'expectednotfound',
$this->mCur->pos,
array( ', or ]', $this->mCur->type, $this->mCur->value )
);
}
}
$result = new AFPData( AFPData::DList, $list );
break;
}
default:
throw new AFPUserVisibleException(
'unexpectedtoken',
$this->mCur->pos,
array(
$this->mCur->type,
$this->mCur->value
)
);
}
$this->move();
wfProfileOut( __METHOD__ );
}
/* End of levels */
/**
* @param $var
* @return AFPData
* @throws AFPUserVisibleException
*/
protected function getVarValue( $var ) {
wfProfileIn( __METHOD__ );
$var = strtolower( $var );
$builderValues = AbuseFilter::getBuilderValues();
if ( !( array_key_exists( $var, $builderValues['vars'] )
|| $this->mVars->varIsSet( $var ) ) ) {
// If the variable is invalid, throw an exception
wfProfileOut( __METHOD__ );
throw new AFPUserVisibleException(
'unrecognisedvar',
$this->mCur->pos,
array( $var )
);
} else {
$val = $this->mVars->getVar( $var );
2009-03-19 00:07:29 +00:00
wfProfileOut( __METHOD__ );
return $val;
}
}
/**
* @param $name
* @param $value
* @throws AFPUserVisibleException
*/
protected function setUserVariable( $name, $value ) {
$builderValues = AbuseFilter::getBuilderValues();
if ( array_key_exists( $name, $builderValues['vars'] ) ) {
throw new AFPUserVisibleException( 'overridebuiltin', $this->mCur->pos, array( $name ) );
}
$this->mVars->setVar( $name, $value );
}
/**
* @param $code
* @param $offset
* @return array
* @throws AFPException
* @throws AFPUserVisibleException
*/
static function nextToken( $code, $offset ) {
$tok = '';
// Check for infinite loops
if ( self::$lastHandledToken == array( $code, $offset ) ) {
// Should never happen
throw new AFPException( "Entered infinite loop. Offset $offset of $code" );
}
self::$lastHandledToken = array( $code, $offset );
// Spaces
$matches = array();
if ( preg_match( '/\s+/uA', $code, $matches, 0, $offset ) ) {
$offset += strlen( $matches[0] );
}
if ( $offset >= strlen( $code ) ) {
return array( '', AFPToken::TNone, $code, $offset );
}
// Comments
if ( substr( $code, $offset, 2 ) == '/*' ) {
$end = strpos( $code, '*/', $offset );
return self::nextToken( $code, $end + 2 );
}
// Commas
if ( $code[$offset] == ',' ) {
return array( ',', AFPToken::TComma, $code, $offset + 1 );
}
// Braces
if ( $code[$offset] == '(' or $code[$offset] == ')' ) {
return array( $code[$offset], AFPToken::TBrace, $code, $offset + 1 );
}
// Square brackets
if ( $code[$offset] == '[' or $code[$offset] == ']' ) {
return array( $code[$offset], AFPToken::TSquareBracket, $code, $offset + 1 );
}
// Semicolons
if ( $code[$offset] == ';' ) {
return array( ';', AFPToken::TStatementSeparator, $code, $offset + 1 );
}
// Strings
if ( $code[$offset] == '"' || $code[$offset] == "'" ) {
$type = $code[$offset];
$offset++;
$strLen = strlen( $code );
while ( $offset < $strLen ) {
if ( $code[$offset] == $type ) {
$offset++;
return array( $tok, AFPToken::TString, $code, $offset );
}
// Performance: Use a PHP function (implemented in C)
// to scan ahead.
$addLength = strcspn( $code, $type . "\\", $offset );
if ( $addLength ) {
$tok .= substr( $code, $offset, $addLength );
$offset += $addLength;
} elseif ( $code[$offset] == '\\' ) {
switch( $code[$offset + 1] ) {
case '\\':
$tok .= '\\';
break;
case $type:
$tok .= $type;
break;
case 'n';
$tok .= "\n";
break;
case 'r':
$tok .= "\r";
break;
case 't':
$tok .= "\t";
break;
case 'x':
$chr = substr( $code, $offset + 2, 2 );
if ( preg_match( '/^[0-9A-Fa-f]{2}$/', $chr ) ) {
$chr = base_convert( $chr, 16, 10 );
$tok .= chr( $chr );
$offset += 2; # \xXX -- 2 done later
} else {
$tok .= 'x';
}
break;
default:
$tok .= "\\" . $code[$offset + 1];
}
$offset += 2;
} else {
$tok .= $code[$offset];
$offset++;
}
}
throw new AFPUserVisibleException( 'unclosedstring', $offset, array() );
}
// Find operators
static $operator_regex = null;
// Match using a regex. Regexes are faster than PHP
if ( !$operator_regex ) {
$quoted_operators = array();
foreach ( self::$mOps as $op ) {
$quoted_operators[] = preg_quote( $op, '/' );
}
$operator_regex = '/(' . implode( '|', $quoted_operators ) . ')/A';
}
$matches = array();
preg_match( $operator_regex, $code, $matches, 0, $offset );
if ( count( $matches ) ) {
$tok = $matches[0];
$offset += strlen( $tok );
return array( $tok, AFPToken::TOp, $code, $offset );
}
// Find bare numbers
$bases = array(
'b' => 2,
'x' => 16,
'o' => 8
);
$baseChars = array(
2 => '[01]',
16 => '[0-9A-Fa-f]',
8 => '[0-8]',
10 => '[0-9.]',
);
$baseClass = '[' . implode( '', array_keys( $bases ) ) . ']';
$radixRegex = "/([0-9A-Fa-f]+(?:\.\d*)?|\.\d+)($baseClass)?/Au";
$matches = array();
if ( preg_match( $radixRegex, $code, $matches, 0, $offset ) ) {
$input = $matches[1];
$baseChar = @$matches[2];
// Sometimes the base char gets mixed in with the rest of it because
// the regex targets hex, too.
// This mostly happens with binary
if ( !$baseChar && !empty( $bases[ substr( $input, - 1 ) ] ) ) {
$baseChar = substr( $input, - 1, 1 );
$input = substr( $input, 0, - 1 );
}
if ( $baseChar ) {
$base = $bases[$baseChar];
} else {
$base = 10;
}
// Check against the appropriate character class for input validation
$baseRegex = "/^" . $baseChars[$base] . "+$/";
if ( preg_match( $baseRegex, $input ) ) {
if ( $base != 10 ) {
$num = base_convert( $input, $base, 10 );
} else {
$num = $input;
}
$offset += strlen( $matches[0] );
$float = strpos( $input, '.' ) !== false;
return array(
$float
? floatval( $num )
: intval( $num ),
$float
? AFPToken::TFloat
: AFPToken::TInt,
$code,
$offset
);
}
}
// The rest are considered IDs
// Regex match > PHP
$idSymbolRegex = '/[0-9A-Za-z_]+/A';
$matches = array();
if ( preg_match( $idSymbolRegex, $code, $matches, 0, $offset ) ) {
$tok = $matches[0];
$type = in_array( $tok, self::$mKeywords )
? AFPToken::TKeyword
: AFPToken::TID;
return array( $tok, $type, $code, $offset + strlen( $tok ) );
}
throw new AFPUserVisibleException(
'unrecognisedtoken', $offset, array( substr( $code, $offset ) ) );
}
// Built-in functions
/**
* @param $args
* @return AFPData
* @throws AFPUserVisibleException
*/
protected function funcLc( $args ) {
global $wgContLang;
if ( count( $args ) < 1 ) {
throw new AFPUserVisibleException(
'notenoughargs',
$this->mCur->pos,
array( 'lc', 2, count( $args ) )
);
}
$s = $args[0]->toString();
return new AFPData( AFPData::DString, $wgContLang->lc( $s ) );
}
/**
* @param $args
* @return AFPData
* @throws AFPUserVisibleException
*/
protected function funcUc( $args ) {
global $wgContLang;
if ( count( $args ) < 1 ) {
throw new AFPUserVisibleException(
'notenoughargs',
$this->mCur->pos,
array( 'uc', 2, count( $args ) )
);
}
$s = $args[0]->toString();
return new AFPData( AFPData::DString, $wgContLang->uc( $s ) );
}
/**
* @param $args
* @return AFPData
* @throws AFPUserVisibleException
*/
protected function funcLen( $args ) {
if ( count( $args ) < 1 ) {
throw new AFPUserVisibleException(
'notenoughargs',
$this->mCur->pos,
array( 'len', 2, count( $args ) )
);
}
if ( $args[0]->type == AFPData::DList ) {
// Don't use toString on lists, but count
return new AFPData( AFPData::DInt, count( $args[0]->data ) );
}
$s = $args[0]->toString();
return new AFPData( AFPData::DInt, mb_strlen( $s, 'utf-8' ) );
}
/**
* @param $args
* @return AFPData
* @throws AFPUserVisibleException
*/
protected function funcSimpleNorm( $args ) {
if ( count( $args ) < 1 ) {
throw new AFPUserVisibleException(
'notenoughargs',
$this->mCur->pos,
array( 'simplenorm', 2, count( $args ) )
);
}
$s = $args[0]->toString();
$s = preg_replace( '/[\d\W]+/', '', $s );
$s = strtolower( $s );
return new AFPData( AFPData::DString, $s );
}
/**
* @param $args
* @return AFPData
* @throws AFPUserVisibleException
*/
protected function funcSpecialRatio( $args ) {
if ( count( $args ) < 1 ) {
throw new AFPUserVisibleException(
'notenoughargs',
$this->mCur->pos,
array( 'specialratio', 1, count( $args ) )
);
}
$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 $args
* @return AFPData
* @throws AFPUserVisibleException
*/
protected function funcCount( $args ) {
if ( count( $args ) < 1 ) {
throw new AFPUserVisibleException(
'notenoughargs',
$this->mCur->pos,
array( 'count', 1, count( $args ) )
);
}
if ( $args[0]->type == AFPData::DList && count( $args ) == 1 ) {
return new AFPData( AFPData::DInt, count( $args[0]->data ) );
}
$offset = - 1;
if ( count( $args ) == 1 ) {
$count = count( explode( ',', $args[0]->toString() ) );
} else {
$needle = $args[0]->toString();
$haystack = $args[1]->toString();
$count = 0;
// Bug #60203: Keep empty parameters from causing PHP warnings
if ( $needle !== '' ) {
while ( ( $offset = strpos( $haystack, $needle, $offset + 1 ) ) !== false ) {
$count++;
}
}
}
return new AFPData( AFPData::DInt, $count );
}
/**
* @param $args
* @return AFPData
* @throws AFPUserVisibleException
* @throws Exception
*/
protected function funcRCount( $args ) {
if ( count( $args ) < 1 ) {
throw new AFPUserVisibleException(
'notenoughargs',
$this->mCur->pos,
array( 'rcount', 1, count( $args ) )
);
}
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";
$matches = array();
$handler = new AFPRegexErrorHandler( $needle, $this->mCur->pos );
try {
$handler->install();
$count = preg_match_all( $needle, $haystack, $matches );
$handler->restore();
} catch ( Exception $e ) {
$handler->restore();
throw $e;
}
}
return new AFPData( AFPData::DInt, $count );
}
/**
* @param $args
* @return AFPData
* @throws AFPUserVisibleException
*/
2009-03-09 12:39:52 +00:00
protected function funcIPInRange( $args ) {
if ( count( $args ) < 2 ) {
throw new AFPUserVisibleException(
'notenoughargs',
$this->mCur->pos,
array( 'ip_in_range', 2, count( $args ) )
);
}
2009-03-09 12:39:52 +00:00
$ip = $args[0]->toString();
$range = $args[1]->toString();
2009-03-09 12:39:52 +00:00
$result = IP::isInRange( $ip, $range );
2009-03-09 12:39:52 +00:00
return new AFPData( AFPData::DBool, $result );
}
/**
* @param $args
* @return AFPData
* @throws AFPUserVisibleException
*/
protected function funcCCNorm( $args ) {
if ( count( $args ) < 1 ) {
throw new AFPUserVisibleException(
'notenoughargs',
$this->mCur->pos,
array( 'ccnorm', 1, count( $args ) )
);
}
$s = $args[0]->toString();
$s = html_entity_decode( $s, ENT_QUOTES, 'UTF-8' );
$s = $this->ccnorm( $s );
return new AFPData( AFPData::DString, $s );
}
/**
* @param $args array
* @return AFPData
* @throws AFPUserVisibleException
*/
protected function funcContainsAny( $args ) {
if ( count( $args ) < 2 ) {
throw new AFPUserVisibleException(
'notenoughargs',
$this->mCur->pos,
array( 'contains_any', 2, count( $args ) )
);
}
$s = array_shift( $args );
$s = $s->toString();
$searchStrings = array();
foreach ( $args as $arg ) {
$searchStrings[] = $arg->toString();
}
if ( function_exists( 'fss_prep_search' ) ) {
$fss = fss_prep_search( $searchStrings );
$result = fss_exec_search( $fss, $s );
$ok = is_array( $result );
} else {
$ok = false;
foreach ( $searchStrings as $needle ) {
// Bug #60203: Keep empty parameters from causing PHP warnings
if ( $needle !== '' && strpos( $s, $needle ) !== false ) {
$ok = true;
break;
}
}
}
return new AFPData( AFPData::DBool, $ok );
}
/**
* @param $s
* @return mixed
*/
protected function ccnorm( $s ) {
static $replacementArray = null;
if ( is_null( $replacementArray ) ) {
global $IP;
if ( is_readable( "$IP/extensions/AntiSpoof/equivset.php" ) ) {
// Satisfy analyzer.
$equivset = null;
// Contains a map of characters in $equivset.
require "$IP/extensions/AntiSpoof/equivset.php";
// strtr in ReplacementArray->replace() doesn't like this.
if ( isset( $equivset[''] ) ) {
unset( $equivset[''] );
}
$replacementArray = new ReplacementArray( $equivset );
} else {
// AntiSpoof isn't available, so just create a dummy
wfDebugLog(
'AbuseFilter',
"Can't compute normalized string (ccnorm) as the AntiSpoof Extension isn't installed."
);
$replacementArray = new ReplacementArray( array() );
}
}
return $replacementArray->replace( $s );
}
/**
* @param $s string
* @return array|string
*/
protected function rmspecials( $s ) {
return preg_replace( '/[^\p{L}\p{N}]/u', '', $s );
}
/**
* @param $s string
* @return array|string
*/
protected function rmdoubles( $s ) {
return preg_replace( '/(.)\1+/us', '\1', $s );
}
/**
* @param $s string
* @return array|string
*/
2009-02-18 19:42:01 +00:00
protected function rmwhitespace( $s ) {
return preg_replace( '/\s+/u', '', $s );
}
/**
* @param $args array
* @return AFPData
* @throws AFPUserVisibleException
*/
protected function funcRMSpecials( $args ) {
if ( count( $args ) < 1 ) {
throw new AFPUserVisibleException(
'notenoughargs',
$this->mCur->pos,
array( 'rmspecials', 1, count( $args ) )
);
}
$s = $args[0]->toString();
$s = $this->rmspecials( $s );
return new AFPData( AFPData::DString, $s );
}
/**
* @param $args array
* @return AFPData
* @throws AFPUserVisibleException
*/
2009-02-18 19:42:01 +00:00
protected function funcRMWhitespace( $args ) {
if ( count( $args ) < 1 ) {
throw new AFPUserVisibleException(
'notenoughargs',
$this->mCur->pos,
array( 'rmwhitespace', 1, count( $args ) )
);
}
2009-02-18 19:42:01 +00:00
$s = $args[0]->toString();
2009-02-18 19:42:01 +00:00
$s = $this->rmwhitespace( $s );
2009-02-18 19:42:01 +00:00
return new AFPData( AFPData::DString, $s );
}
/**
* @param $args array
* @return AFPData
* @throws AFPUserVisibleException
*/
protected function funcRMDoubles( $args ) {
if ( count( $args ) < 1 ) {
throw new AFPUserVisibleException(
'notenoughargs',
$this->mCur->pos,
array( 'rmdoubles', 1, count( $args ) )
);
}
$s = $args[0]->toString();
$s = $this->rmdoubles( $s );
return new AFPData( AFPData::DString, $s );
}
/**
* @param $args array
* @return AFPData
* @throws AFPUserVisibleException
*/
protected function funcNorm( $args ) {
if ( count( $args ) < 1 ) {
throw new AFPUserVisibleException(
'notenoughargs',
$this->mCur->pos,
array( 'norm', 1, count( $args ) )
);
}
$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 $args array
* @return AFPData
* @throws AFPUserVisibleException
*/
protected function funcSubstr( $args ) {
if ( count( $args ) < 2 ) {
throw new AFPUserVisibleException(
'notenoughargs',
$this->mCur->pos,
array( 'substr', 2, count( $args ) )
);
}
$s = $args[0]->toString();
$offset = $args[1]->toInt();
if ( isset( $args[2] ) ) {
$length = $args[2]->toInt();
$result = mb_substr( $s, $offset, $length );
} else {
$result = mb_substr( $s, $offset );
}
return new AFPData( AFPData::DString, $result );
}
/**
* @param $args array
* @return AFPData
* @throws AFPUserVisibleException
*/
protected function funcStrPos( $args ) {
if ( count( $args ) < 2 ) {
throw new AFPUserVisibleException(
'notenoughargs',
$this->mCur->pos,
array( 'strpos', 2, count( $args ) )
);
}
$haystack = $args[0]->toString();
$needle = $args[1]->toString();
// Bug #60203: Keep empty parameters from causing PHP warnings
if ( $needle === '' ) {
return new AFPData( AFPData::DInt, -1 );
}
if ( isset( $args[2] ) ) {
$offset = $args[2]->toInt();
$result = mb_strpos( $haystack, $needle, $offset );
} else {
$result = mb_strpos( $haystack, $needle );
}
if ( $result === false )
$result = - 1;
return new AFPData( AFPData::DInt, $result );
}
/**
* @param $args array
* @return AFPData
* @throws AFPUserVisibleException
*/
protected function funcStrReplace( $args ) {
if ( count( $args ) < 3 ) {
throw new AFPUserVisibleException(
'notenoughargs',
$this->mCur->pos,
array( 'str_replace', 3, count( $args ) )
);
}
$subject = $args[0]->toString();
$search = $args[1]->toString();
$replace = $args[2]->toString();
return new AFPData( AFPData::DString, str_replace( $search, $replace, $subject ) );
}
/**
* @param $args array
* @return AFPData
* @throws AFPUserVisibleException
*/
protected function funcStrRegexEscape( $args ) {
if ( count( $args ) < 1 ) {
throw new AFPUserVisibleException( 'notenoughargs', $this->mCur->pos,
array( 'rescape', 1, count( $args ) ) );
}
$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 $args array
* @return mixed
* @throws AFPUserVisibleException
*/
protected function funcSetVar( $args ) {
if ( count( $args ) < 2 ) {
throw new AFPUserVisibleException(
'notenoughargs',
$this->mCur->pos,
array( 'set_var', 2, count( $args ) )
);
}
$varName = $args[0]->toString();
$value = $args[1];
$this->setUserVariable( $varName, $value );
return $value;
}
/**
* @param $args array
* @return AFPData
* @throws AFPUserVisibleException
*/
protected function castString( $args ) {
if ( count( $args ) < 1 ) {
throw new AFPUserVisibleException( 'noparams', $this->mCur->pos, array( __METHOD__ ) );
}
$val = $args[0];
return AFPData::castTypes( $val, AFPData::DString );
}
/**
* @param $args array
* @return AFPData
* @throws AFPUserVisibleException
*/
protected function castInt( $args ) {
if ( count( $args ) < 1 ) {
throw new AFPUserVisibleException( 'noparams', $this->mCur->pos, array( __METHOD__ ) );
}
$val = $args[0];
return AFPData::castTypes( $val, AFPData::DInt );
}
/**
* @param $args array
* @return AFPData
* @throws AFPUserVisibleException
*/
protected function castFloat( $args ) {
if ( count( $args ) < 1 ) {
throw new AFPUserVisibleException( 'noparams', $this->mCur->pos, array( __METHOD__ ) );
}
$val = $args[0];
return AFPData::castTypes( $val, AFPData::DFloat );
}
/**
* @param $args array
* @return AFPData
* @throws AFPUserVisibleException
*/
protected function castBool( $args ) {
if ( count( $args ) < 1 ) {
throw new AFPUserVisibleException( 'noparams', $this->mCur->pos, array( __METHOD__ ) );
}
$val = $args[0];
return AFPData::castTypes( $val, AFPData::DBool );
}
}