mirror of
https://gerrit.wikimedia.org/r/mediawiki/extensions/AbuseFilter.git
synced 2024-12-03 18:16:15 +00:00
23fe973544
If the number is int there's not need to intval it, and if it's float there's no need to floatval... Just use it to determine the internal data type, like it happens for sum and sub. Change-Id: Ie00c4bb4e67ad1eface0cea3eb42d04aa5fb39cc
495 lines
12 KiB
PHP
495 lines
12 KiB
PHP
<?php
|
|
|
|
class AFPData {
|
|
// Datatypes
|
|
const DINT = 'int';
|
|
const DSTRING = 'string';
|
|
const DNULL = 'null';
|
|
const DBOOL = 'bool';
|
|
const DFLOAT = 'float';
|
|
const DARRAY = 'array';
|
|
|
|
// Translation table mapping shell-style wildcards to PCRE equivalents.
|
|
// Derived from <http://www.php.net/manual/en/function.fnmatch.php#100207>
|
|
private static $wildcardMap = [
|
|
'\*' => '.*',
|
|
'\+' => '\+',
|
|
'\-' => '\-',
|
|
'\.' => '\.',
|
|
'\?' => '.',
|
|
'\[' => '[',
|
|
'\[\!' => '[^',
|
|
'\\' => '\\\\',
|
|
'\]' => ']',
|
|
];
|
|
|
|
/**
|
|
* @var string One of the D* const from this class
|
|
*/
|
|
public $type;
|
|
/**
|
|
* @var mixed|null|AFPData[] The actual data contained in this object
|
|
*/
|
|
public $data;
|
|
|
|
/**
|
|
* @param string $type
|
|
* @param mixed|null $val
|
|
*/
|
|
public function __construct( $type = self::DNULL, $val = null ) {
|
|
$this->type = $type;
|
|
$this->data = $val;
|
|
}
|
|
|
|
/**
|
|
* @param mixed $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::DARRAY, $result );
|
|
} elseif ( is_null( $var ) ) {
|
|
return new AFPData();
|
|
} else {
|
|
throw new AFPException(
|
|
'Data type ' . gettype( $var ) . ' is not supported by AbuseFilter'
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @return AFPData
|
|
*/
|
|
private function dup() {
|
|
return new AFPData( $this->type, $this->data );
|
|
}
|
|
|
|
/**
|
|
* @param AFPData $orig
|
|
* @param string $target
|
|
* @return AFPData
|
|
*/
|
|
public static function castTypes( AFPData $orig, $target ) {
|
|
if ( $orig->type === $target ) {
|
|
return $orig->dup();
|
|
}
|
|
if ( $target === self::DNULL ) {
|
|
// Should never happen. Actually, why is this here?
|
|
// @codeCoverageIgnoreStart
|
|
return new AFPData();
|
|
// @codeCoverageIgnoreEnd
|
|
}
|
|
|
|
if ( $orig->type === self::DARRAY ) {
|
|
if ( $target === self::DBOOL ) {
|
|
return new AFPData( self::DBOOL, (bool)count( $orig->data ) );
|
|
} elseif ( $target === self::DFLOAT ) {
|
|
return new AFPData( self::DFLOAT, floatval( count( $orig->data ) ) );
|
|
} elseif ( $target === self::DINT ) {
|
|
return new AFPData( self::DINT, intval( count( $orig->data ) ) );
|
|
} elseif ( $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 );
|
|
} elseif ( $target === self::DFLOAT ) {
|
|
return new AFPData( self::DFLOAT, floatval( $orig->data ) );
|
|
} elseif ( $target === self::DINT ) {
|
|
return new AFPData( self::DINT, intval( $orig->data ) );
|
|
} elseif ( $target === self::DSTRING ) {
|
|
return new AFPData( self::DSTRING, strval( $orig->data ) );
|
|
// We don't expose any cast-to-array method...
|
|
// @codeCoverageIgnoreStart
|
|
} elseif ( $target === self::DARRAY ) {
|
|
return new AFPData( self::DARRAY, [ $orig ] );
|
|
}
|
|
// @codeCoverageIgnoreEnd
|
|
}
|
|
|
|
/**
|
|
* @param AFPData $value
|
|
* @return AFPData
|
|
*/
|
|
public static function boolInvert( AFPData $value ) {
|
|
return new AFPData( self::DBOOL, !$value->toBool() );
|
|
}
|
|
|
|
/**
|
|
* @param AFPData $base
|
|
* @param AFPData $exponent
|
|
* @return AFPData
|
|
*/
|
|
public static function pow( AFPData $base, AFPData $exponent ) {
|
|
$res = pow( $base->toNumber(), $exponent->toNumber() );
|
|
$type = is_int( $res ) ? self::DINT : self::DFLOAT;
|
|
|
|
return new AFPData( $type, $res );
|
|
}
|
|
|
|
/**
|
|
* Checks if $a contains $b
|
|
*
|
|
* @param AFPData $a
|
|
* @param AFPData $b
|
|
* @return AFPData
|
|
*/
|
|
private static function containmentKeyword( AFPData $a, AFPData $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 AFPData $a
|
|
* @param AFPData $b
|
|
* @return AFPData
|
|
*/
|
|
public static function keywordIn( AFPData $a, AFPData $b ) {
|
|
return self::containmentKeyword( $b, $a );
|
|
}
|
|
|
|
/**
|
|
* @param AFPData $a
|
|
* @param AFPData $b
|
|
* @return AFPData
|
|
*/
|
|
public static function keywordContains( AFPData $a, AFPData $b ) {
|
|
return self::containmentKeyword( $a, $b );
|
|
}
|
|
|
|
/**
|
|
* @param AFPData $d1
|
|
* @param AFPData $d2
|
|
* @param bool $strict whether to also check types
|
|
* @return bool
|
|
*/
|
|
private static function equals( AFPData $d1, AFPData $d2, $strict = false ) {
|
|
if ( $d1->type !== self::DARRAY && $d2->type !== self::DARRAY ) {
|
|
$typecheck = $d1->type === $d2->type || !$strict;
|
|
return $typecheck && $d1->toString() === $d2->toString();
|
|
} elseif ( $d1->type === self::DARRAY && $d2->type === self::DARRAY ) {
|
|
$data1 = $d1->data;
|
|
$data2 = $d2->data;
|
|
if ( count( $data1 ) !== count( $data2 ) ) {
|
|
return false;
|
|
}
|
|
$length = count( $data1 );
|
|
for ( $i = 0; $i < $length; $i++ ) {
|
|
if ( self::equals( $data1[$i], $data2[$i], $strict ) === false ) {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
} else {
|
|
// Trying to compare an array to something else
|
|
if ( $strict ) {
|
|
return false;
|
|
}
|
|
if ( $d1->type === self::DARRAY && count( $d1->data ) === 0 ) {
|
|
return ( $d2->type === self::DBOOL && $d2->toBool() === false ) || $d2->type === self::DNULL;
|
|
} elseif ( $d2->type === self::DARRAY && count( $d2->data ) === 0 ) {
|
|
return ( $d1->type === self::DBOOL && $d1->toBool() === false ) || $d1->type === self::DNULL;
|
|
} else {
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param AFPData $str
|
|
* @param AFPData $pattern
|
|
* @return AFPData
|
|
*/
|
|
public static function keywordLike( AFPData $str, AFPData $pattern ) {
|
|
$str = $str->toString();
|
|
$pattern = '#^' . strtr( preg_quote( $pattern->toString(), '#' ), self::$wildcardMap ) . '$#u';
|
|
Wikimedia\suppressWarnings();
|
|
$result = preg_match( $pattern, $str );
|
|
Wikimedia\restoreWarnings();
|
|
|
|
return new AFPData( self::DBOOL, (bool)$result );
|
|
}
|
|
|
|
/**
|
|
* @param AFPData $str
|
|
* @param AFPData $regex
|
|
* @param int $pos
|
|
* @param bool $insensitive
|
|
* @return AFPData
|
|
* @throws Exception
|
|
*/
|
|
public static function keywordRegex( AFPData $str, AFPData $regex, $pos, $insensitive = false ) {
|
|
$str = $str->toString();
|
|
$pattern = $regex->toString();
|
|
|
|
$pattern = preg_replace( '!(\\\\\\\\)*(\\\\)?/!', '$1\/', $pattern );
|
|
$pattern = "/$pattern/u";
|
|
|
|
if ( $insensitive ) {
|
|
$pattern .= 'i';
|
|
}
|
|
|
|
Wikimedia\suppressWarnings();
|
|
$result = preg_match( $pattern, $str );
|
|
Wikimedia\restoreWarnings();
|
|
if ( $result === false ) {
|
|
throw new AFPUserVisibleException(
|
|
'regexfailure',
|
|
// Coverage bug
|
|
// @codeCoverageIgnoreStart
|
|
$pos,
|
|
// @codeCoverageIgnoreEnd
|
|
[ $pattern ]
|
|
);
|
|
}
|
|
|
|
return new AFPData( self::DBOOL, (bool)$result );
|
|
}
|
|
|
|
/**
|
|
* @param AFPData $str
|
|
* @param AFPData $regex
|
|
* @param int $pos
|
|
* @return AFPData
|
|
*/
|
|
public static function keywordRegexInsensitive( AFPData $str, AFPData $regex, $pos ) {
|
|
return self::keywordRegex( $str, $regex, $pos, true );
|
|
}
|
|
|
|
/**
|
|
* @param AFPData $data
|
|
* @return AFPData
|
|
*/
|
|
public static function unaryMinus( AFPData $data ) {
|
|
if ( $data->type === self::DINT ) {
|
|
return new AFPData( $data->type, -$data->toInt() );
|
|
} else {
|
|
return new AFPData( $data->type, -$data->toFloat() );
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param AFPData $a
|
|
* @param AFPData $b
|
|
* @param string $op
|
|
* @return AFPData
|
|
* @throws AFPException
|
|
*/
|
|
public static function boolOp( AFPData $a, AFPData $b, $op ) {
|
|
$a = $a->toBool();
|
|
$b = $b->toBool();
|
|
if ( $op === '|' ) {
|
|
return new AFPData( self::DBOOL, $a || $b );
|
|
} elseif ( $op === '&' ) {
|
|
return new AFPData( self::DBOOL, $a && $b );
|
|
} elseif ( $op === '^' ) {
|
|
return new AFPData( self::DBOOL, $a xor $b );
|
|
}
|
|
// Should never happen.
|
|
// @codeCoverageIgnoreStart
|
|
throw new AFPException( "Invalid boolean operation: {$op}" );
|
|
// @codeCoverageIgnoreEnd
|
|
}
|
|
|
|
/**
|
|
* @param AFPData $a
|
|
* @param AFPData $b
|
|
* @param string $op
|
|
* @return AFPData
|
|
* @throws AFPException
|
|
*/
|
|
public static function compareOp( AFPData $a, AFPData $b, $op ) {
|
|
if ( $op === '==' || $op === '=' ) {
|
|
return new AFPData( self::DBOOL, self::equals( $a, $b ) );
|
|
} elseif ( $op === '!=' ) {
|
|
return new AFPData( self::DBOOL, !self::equals( $a, $b ) );
|
|
} elseif ( $op === '===' ) {
|
|
return new AFPData( self::DBOOL, self::equals( $a, $b, true ) );
|
|
} elseif ( $op === '!==' ) {
|
|
return new AFPData( self::DBOOL, !self::equals( $a, $b, true ) );
|
|
}
|
|
|
|
$a = $a->toString();
|
|
$b = $b->toString();
|
|
if ( $op === '>' ) {
|
|
return new AFPData( self::DBOOL, $a > $b );
|
|
} elseif ( $op === '<' ) {
|
|
return new AFPData( self::DBOOL, $a < $b );
|
|
} elseif ( $op === '>=' ) {
|
|
return new AFPData( self::DBOOL, $a >= $b );
|
|
} elseif ( $op === '<=' ) {
|
|
return new AFPData( self::DBOOL, $a <= $b );
|
|
}
|
|
// Should never happen
|
|
// @codeCoverageIgnoreStart
|
|
throw new AFPException( "Invalid comparison operation: {$op}" );
|
|
// @codeCoverageIgnoreEnd
|
|
}
|
|
|
|
/**
|
|
* @param AFPData $a
|
|
* @param AFPData $b
|
|
* @param string $op
|
|
* @param int $pos
|
|
* @return AFPData
|
|
* @throws AFPUserVisibleException
|
|
* @throws AFPException
|
|
*/
|
|
public static function mulRel( AFPData $a, AFPData $b, $op, $pos ) {
|
|
$a = $a->toNumber();
|
|
$b = $b->toNumber();
|
|
|
|
if ( $op !== '*' && (float)$b === 0.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
|
|
// @codeCoverageIgnoreStart
|
|
throw new AFPException( "Invalid multiplication-related operation: {$op}" );
|
|
// @codeCoverageIgnoreEnd
|
|
}
|
|
|
|
$type = is_int( $data ) ? self::DINT : self::DFLOAT;
|
|
|
|
return new AFPData( $type, $data );
|
|
}
|
|
|
|
/**
|
|
* @param AFPData $a
|
|
* @param AFPData $b
|
|
* @return AFPData
|
|
*/
|
|
public static function sum( AFPData $a, AFPData $b ) {
|
|
if ( $a->type === self::DSTRING || $b->type === self::DSTRING ) {
|
|
return new AFPData( self::DSTRING, $a->toString() . $b->toString() );
|
|
} elseif ( $a->type === self::DARRAY && $b->type === self::DARRAY ) {
|
|
return new AFPData( self::DARRAY, array_merge( $a->toArray(), $b->toArray() ) );
|
|
} else {
|
|
$res = $a->toNumber() + $b->toNumber();
|
|
$type = is_int( $res ) ? self::DINT : self::DFLOAT;
|
|
|
|
return new AFPData( $type, $res );
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param AFPData $a
|
|
* @param AFPData $b
|
|
* @return AFPData
|
|
*/
|
|
public static function sub( AFPData $a, AFPData $b ) {
|
|
$res = $a->toNumber() - $b->toNumber();
|
|
$type = is_int( $res ) ? self::DINT : self::DFLOAT;
|
|
|
|
return new AFPData( $type, $res );
|
|
}
|
|
|
|
/** 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::DARRAY:
|
|
$input = $this->toArray();
|
|
$output = [];
|
|
foreach ( $input as $item ) {
|
|
$output[] = $item->toNative();
|
|
}
|
|
|
|
return $output;
|
|
case self::DNULL:
|
|
return null;
|
|
default:
|
|
// @codeCoverageIgnoreStart
|
|
throw new MWException( "Unknown type" );
|
|
// @codeCoverageIgnoreEnd
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @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;
|
|
}
|
|
|
|
/**
|
|
* @return int|float
|
|
*/
|
|
public function toNumber() {
|
|
return $this->type === self::DINT ? $this->toInt() : $this->toFloat();
|
|
}
|
|
|
|
/**
|
|
* @return array
|
|
*/
|
|
public function toArray() {
|
|
return self::castTypes( $this, self::DARRAY )->data;
|
|
}
|
|
}
|