mirror of
https://gerrit.wikimedia.org/r/mediawiki/extensions/AbuseFilter.git
synced 2024-11-15 18:19:38 +00:00
b67cb42c09
Change-Id: I544cdfa75c7472f2d98b2561bc6f6f9c2d2ad639
461 lines
10 KiB
PHP
461 lines
10 KiB
PHP
<?php
|
|
|
|
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 = [
|
|
'\*' => '.*',
|
|
'\+' => '\+',
|
|
'\-' => '\-',
|
|
'\.' => '\.',
|
|
'\?' => '.',
|
|
'\[' => '[',
|
|
'\[\!' => '[^',
|
|
'\\' => '\\\\',
|
|
'\]' => ']',
|
|
];
|
|
|
|
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 = [];
|
|
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, [ $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';
|
|
MediaWiki\suppressWarnings();
|
|
$result = preg_match( $pattern, $str );
|
|
MediaWiki\restoreWarnings();
|
|
|
|
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();
|
|
|
|
$pattern = preg_replace( '!(\\\\\\\\)*(\\\\)?/!', '$1\/', $pattern );
|
|
$pattern = "/$pattern/u";
|
|
|
|
if ( $insensitive ) {
|
|
$pattern .= 'i';
|
|
}
|
|
|
|
$result = preg_match( $pattern, $str );
|
|
if ( $result === false ) {
|
|
throw new AFPUserVisibleException(
|
|
'regexfailure',
|
|
$pos,
|
|
[ 'unspecified error in preg_match()', $pattern ]
|
|
);
|
|
}
|
|
|
|
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 );
|
|
}
|
|
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 );
|
|
}
|
|
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, [ $a ] );
|
|
}
|
|
|
|
if ( $op == '*' ) {
|
|
$data = $a * $b;
|
|
} elseif ( $op == '/' ) {
|
|
$data = $a / $b;
|
|
} elseif ( $op == '%' ) {
|
|
$data = $a % $b;
|
|
} else {
|
|
// Should never happen
|
|
throw new AFPException( "Invalid multiplication-related operation: {$op}" );
|
|
}
|
|
|
|
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 = [];
|
|
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;
|
|
}
|
|
}
|