mirror of
https://gerrit.wikimedia.org/r/mediawiki/extensions/AbuseFilter.git
synced 2024-12-04 10:28:32 +00:00
3e0c30ff92
This is achieved by creating a new ParserStatus class. Aside from the result of parse(), it contains whether the cache was warm. This can be used to differentiate profiling data as part of T231112. Another use case is returning non-fatal warnings (T269770). Change-Id: Ifcbda861ce1a44bbe9bffba5b83cd9ef338a8dba
503 lines
13 KiB
PHP
503 lines
13 KiB
PHP
<?php
|
|
|
|
namespace MediaWiki\Extension\AbuseFilter\Parser;
|
|
|
|
use InvalidArgumentException;
|
|
use RuntimeException;
|
|
|
|
class AFPData {
|
|
// Datatypes
|
|
public const DINT = 'int';
|
|
public const DSTRING = 'string';
|
|
public const DNULL = 'null';
|
|
public const DBOOL = 'bool';
|
|
public const DFLOAT = 'float';
|
|
public const DARRAY = 'array';
|
|
// Special purpose type for non-initialized stuff
|
|
public const DUNDEFINED = 'undefined';
|
|
// Special purpose for creating instances that will be populated later
|
|
public const DEMPTY = 'empty';
|
|
|
|
/**
|
|
* Translation table mapping shell-style wildcards to PCRE equivalents.
|
|
* Derived from <http://www.php.net/manual/en/function.fnmatch.php#100207>
|
|
* @internal
|
|
*/
|
|
public const WILDCARD_MAP = [
|
|
'\*' => '.*',
|
|
'\+' => '\+',
|
|
'\-' => '\-',
|
|
'\.' => '\.',
|
|
'\?' => '.',
|
|
'\[' => '[',
|
|
'\[\!' => '[^',
|
|
'\\' => '\\\\',
|
|
'\]' => ']',
|
|
];
|
|
|
|
/**
|
|
* @var string One of the D* const from this class
|
|
* @private Use $this->getType()
|
|
*/
|
|
public $type;
|
|
/**
|
|
* @var mixed|null|AFPData[] The actual data contained in this object
|
|
* @private Use $this->getData()
|
|
*/
|
|
public $data;
|
|
|
|
/**
|
|
* @return string
|
|
*/
|
|
public function getType() {
|
|
return $this->type;
|
|
}
|
|
|
|
/**
|
|
* @return AFPData[]|mixed|null
|
|
*/
|
|
public function getData() {
|
|
return $this->data;
|
|
}
|
|
|
|
/**
|
|
* @param string $type
|
|
* @param AFPData[]|mixed|null $val
|
|
*/
|
|
public function __construct( $type, $val = null ) {
|
|
if ( ( $type === self::DUNDEFINED || $type === self::DEMPTY ) && $val !== null ) {
|
|
// Sanity
|
|
throw new InvalidArgumentException( 'DUNDEFINED and DEMPTY cannot have a non-null value' );
|
|
}
|
|
$this->type = $type;
|
|
$this->data = $val;
|
|
}
|
|
|
|
/**
|
|
* @param mixed $var
|
|
* @return AFPData
|
|
* @throws AFPException
|
|
*/
|
|
public static function newFromPHPVar( $var ) {
|
|
switch ( gettype( $var ) ) {
|
|
case 'string':
|
|
return new AFPData( self::DSTRING, $var );
|
|
case 'integer':
|
|
return new AFPData( self::DINT, $var );
|
|
case 'double':
|
|
return new AFPData( self::DFLOAT, $var );
|
|
case 'boolean':
|
|
return new AFPData( self::DBOOL, $var );
|
|
case 'array':
|
|
$result = [];
|
|
foreach ( $var as $item ) {
|
|
$result[] = self::newFromPHPVar( $item );
|
|
}
|
|
return new AFPData( self::DARRAY, $result );
|
|
case 'NULL':
|
|
return new AFPData( self::DNULL );
|
|
default:
|
|
throw new AFPException(
|
|
'Data type ' . gettype( $var ) . ' is not supported by AbuseFilter'
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param AFPData $orig
|
|
* @param string $target
|
|
* @return AFPData
|
|
*/
|
|
public static function castTypes( AFPData $orig, $target ) {
|
|
if ( $orig->type === $target ) {
|
|
return $orig;
|
|
}
|
|
if ( $orig->type === self::DUNDEFINED ) {
|
|
// This case should be handled at a higher level, to avoid implicitly relying on what
|
|
// this method will do for the specific case.
|
|
throw new AFPException( 'Refusing to cast DUNDEFINED to something else' );
|
|
}
|
|
if ( $target === self::DNULL ) {
|
|
// We don't expose any method to cast to null. And, actually, should we?
|
|
return new AFPData( self::DNULL );
|
|
}
|
|
|
|
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, 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 ) );
|
|
} elseif ( $target === self::DARRAY ) {
|
|
// We don't expose any method to cast to array
|
|
return new AFPData( self::DARRAY, [ $orig ] );
|
|
}
|
|
throw new AFPException( 'Cannot cast ' . $orig->type . " to $target." );
|
|
}
|
|
|
|
/**
|
|
* @return AFPData
|
|
*/
|
|
public function boolInvert() {
|
|
if ( $this->type === self::DUNDEFINED ) {
|
|
return new AFPData( self::DUNDEFINED );
|
|
}
|
|
return new AFPData( self::DBOOL, !$this->toBool() );
|
|
}
|
|
|
|
/**
|
|
* @param AFPData $exponent
|
|
* @return AFPData
|
|
*/
|
|
public function pow( AFPData $exponent ) {
|
|
if ( $this->type === self::DUNDEFINED || $exponent->type === self::DUNDEFINED ) {
|
|
return new AFPData( self::DUNDEFINED );
|
|
}
|
|
$res = pow( $this->toNumber(), $exponent->toNumber() );
|
|
$type = is_int( $res ) ? self::DINT : self::DFLOAT;
|
|
|
|
return new AFPData( $type, $res );
|
|
}
|
|
|
|
/**
|
|
* @param AFPData $d2
|
|
* @param bool $strict whether to also check types
|
|
* @return bool
|
|
* @throws AFPException if $this or $d2 is a DUNDEFINED. This shouldn't happen, because this method
|
|
* only returns a boolean, and thus the type of the result has already been decided and cannot
|
|
* be changed to be a DUNDEFINED from here.
|
|
* @internal
|
|
*/
|
|
public function equals( AFPData $d2, $strict = false ) {
|
|
if ( $this->type === self::DUNDEFINED || $d2->type === self::DUNDEFINED ) {
|
|
throw new AFPException(
|
|
__METHOD__ . " got a DUNDEFINED. This should be handled at a higher level"
|
|
);
|
|
} elseif ( $this->type !== self::DARRAY && $d2->type !== self::DARRAY ) {
|
|
$typecheck = $this->type === $d2->type || !$strict;
|
|
return $typecheck && $this->toString() === $d2->toString();
|
|
} elseif ( $this->type === self::DARRAY && $d2->type === self::DARRAY ) {
|
|
$data1 = $this->data;
|
|
$data2 = $d2->data;
|
|
if ( count( $data1 ) !== count( $data2 ) ) {
|
|
return false;
|
|
}
|
|
$length = count( $data1 );
|
|
for ( $i = 0; $i < $length; $i++ ) {
|
|
// @phan-suppress-next-line PhanTypeArraySuspiciousNullable Array type
|
|
if ( $data1[$i]->equals( $data2[$i], $strict ) === false ) {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
} else {
|
|
// Trying to compare an array to something else
|
|
if ( $strict ) {
|
|
return false;
|
|
}
|
|
if ( $this->type === self::DARRAY && count( $this->data ) === 0 ) {
|
|
return ( $d2->type === self::DBOOL && $d2->toBool() === false ) || $d2->type === self::DNULL;
|
|
} elseif ( $d2->type === self::DARRAY && count( $d2->data ) === 0 ) {
|
|
return ( $this->type === self::DBOOL && $this->toBool() === false ) ||
|
|
$this->type === self::DNULL;
|
|
} else {
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @return AFPData
|
|
*/
|
|
public function unaryMinus() {
|
|
if ( $this->type === self::DUNDEFINED ) {
|
|
return new AFPData( self::DUNDEFINED );
|
|
} elseif ( $this->type === self::DINT ) {
|
|
return new AFPData( $this->type, -$this->toInt() );
|
|
} else {
|
|
$type = $this->type === self::DEMPTY ? self::DNULL : $this->type;
|
|
return new AFPData( $type, -$this->toFloat() );
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param AFPData $b
|
|
* @param string $op
|
|
* @return AFPData
|
|
* @throws AFPException
|
|
*/
|
|
public function boolOp( AFPData $b, $op ) {
|
|
$a = $this->type === self::DUNDEFINED ? false : $this->toBool();
|
|
$b = $b->type === self::DUNDEFINED ? false : $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 $b
|
|
* @param string $op
|
|
* @return AFPData
|
|
* @throws AFPException
|
|
*/
|
|
public function compareOp( AFPData $b, $op ) {
|
|
if ( $this->type === self::DUNDEFINED || $b->type === self::DUNDEFINED ) {
|
|
return new AFPData( self::DUNDEFINED );
|
|
}
|
|
if ( $op === '==' || $op === '=' ) {
|
|
return new AFPData( self::DBOOL, $this->equals( $b ) );
|
|
} elseif ( $op === '!=' ) {
|
|
return new AFPData( self::DBOOL, !$this->equals( $b ) );
|
|
} elseif ( $op === '===' ) {
|
|
return new AFPData( self::DBOOL, $this->equals( $b, true ) );
|
|
} elseif ( $op === '!==' ) {
|
|
return new AFPData( self::DBOOL, !$this->equals( $b, true ) );
|
|
}
|
|
|
|
$a = $this->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 $b
|
|
* @param string $op
|
|
* @param int $pos
|
|
* @return AFPData
|
|
* @throws AFPUserVisibleException
|
|
* @throws AFPException
|
|
*/
|
|
public function mulRel( AFPData $b, $op, $pos ) {
|
|
if ( $b->type === self::DUNDEFINED ) {
|
|
// The LHS type is checked later, because we first need to ensure we're not
|
|
// dividing or taking modulo by 0 (and that should throw regardless of whether
|
|
// the LHS is undefined).
|
|
return new AFPData( self::DUNDEFINED );
|
|
}
|
|
|
|
$b = $b->toNumber();
|
|
|
|
if (
|
|
( $op === '/' && (float)$b === 0.0 ) ||
|
|
( $op === '%' && (int)$b === 0 )
|
|
) {
|
|
$lhs = $this->type === self::DUNDEFINED ? 0 : $this->toNumber();
|
|
throw new AFPUserVisibleException( 'dividebyzero', $pos, [ $lhs ] );
|
|
}
|
|
|
|
if ( $this->type === self::DUNDEFINED ) {
|
|
return new AFPData( self::DUNDEFINED );
|
|
}
|
|
$a = $this->toNumber();
|
|
|
|
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 $b
|
|
* @return AFPData
|
|
*/
|
|
public function sum( AFPData $b ) {
|
|
if ( $this->type === self::DUNDEFINED || $b->type === self::DUNDEFINED ) {
|
|
return new AFPData( self::DUNDEFINED );
|
|
} elseif ( $this->type === self::DSTRING || $b->type === self::DSTRING ) {
|
|
return new AFPData( self::DSTRING, $this->toString() . $b->toString() );
|
|
} elseif ( $this->type === self::DARRAY && $b->type === self::DARRAY ) {
|
|
return new AFPData( self::DARRAY, array_merge( $this->toArray(), $b->toArray() ) );
|
|
} else {
|
|
$res = $this->toNumber() + $b->toNumber();
|
|
$type = is_int( $res ) ? self::DINT : self::DFLOAT;
|
|
|
|
return new AFPData( $type, $res );
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param AFPData $b
|
|
* @return AFPData
|
|
*/
|
|
public function sub( AFPData $b ) {
|
|
if ( $this->type === self::DUNDEFINED || $b->type === self::DUNDEFINED ) {
|
|
return new AFPData( self::DUNDEFINED );
|
|
}
|
|
$res = $this->toNumber() - $b->toNumber();
|
|
$type = is_int( $res ) ? self::DINT : self::DFLOAT;
|
|
|
|
return new AFPData( $type, $res );
|
|
}
|
|
|
|
/**
|
|
* Check whether this instance contains the DUNDEFINED type, recursively
|
|
* @return bool
|
|
*/
|
|
public function hasUndefined() : bool {
|
|
if ( $this->type === self::DUNDEFINED ) {
|
|
return true;
|
|
}
|
|
if ( $this->type === self::DARRAY ) {
|
|
foreach ( $this->data as $el ) {
|
|
if ( $el->hasUndefined() ) {
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Return a clone of this instance where DUNDEFINED is replaced with DNULL
|
|
* @return $this
|
|
*/
|
|
public function cloneAsUndefinedReplacedWithNull() : self {
|
|
if ( $this->type === self::DUNDEFINED ) {
|
|
return new self( self::DNULL );
|
|
}
|
|
if ( $this->type === self::DARRAY ) {
|
|
$data = [];
|
|
foreach ( $this->data as $el ) {
|
|
$data[] = $el->cloneAsUndefinedReplacedWithNull();
|
|
}
|
|
return new self( self::DARRAY, $data );
|
|
}
|
|
return clone $this;
|
|
}
|
|
|
|
/** Convert shorteners */
|
|
|
|
/**
|
|
* @throws RuntimeException
|
|
* @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:
|
|
case self::DUNDEFINED:
|
|
case self::DEMPTY:
|
|
return null;
|
|
default:
|
|
// @codeCoverageIgnoreStart
|
|
throw new RuntimeException( "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() {
|
|
// Types that can be cast to int
|
|
$intLikeTypes = [
|
|
self::DINT,
|
|
self::DBOOL,
|
|
self::DNULL
|
|
];
|
|
return in_array( $this->type, $intLikeTypes, true ) ? $this->toInt() : $this->toFloat();
|
|
}
|
|
|
|
/**
|
|
* @return array
|
|
*/
|
|
public function toArray() {
|
|
return self::castTypes( $this, self::DARRAY )->data;
|
|
}
|
|
}
|