2016-12-17 17:52:36 +00:00
|
|
|
<?php
|
|
|
|
|
2020-01-15 16:08:53 +00:00
|
|
|
use MediaWiki\Extension\AbuseFilter\AbuseFilterServices;
|
|
|
|
use MediaWiki\Extension\AbuseFilter\KeywordsManager;
|
2019-08-11 13:11:20 +00:00
|
|
|
use Psr\Log\LoggerInterface;
|
|
|
|
|
2016-12-17 17:52:36 +00:00
|
|
|
class AbuseFilterVariableHolder {
|
2019-08-11 13:11:20 +00:00
|
|
|
/**
|
2020-09-18 13:05:58 +00:00
|
|
|
* Used in self::getVar() to determine what to do if the requested variable is missing. See
|
|
|
|
* the docs of that method for an explanation.
|
2019-08-11 13:11:20 +00:00
|
|
|
*/
|
2020-01-21 11:13:11 +00:00
|
|
|
public const GET_LAX = 0;
|
|
|
|
public const GET_STRICT = 1;
|
2020-09-18 13:05:58 +00:00
|
|
|
public const GET_BC = 2;
|
2019-08-11 13:11:20 +00:00
|
|
|
|
2020-01-15 16:08:53 +00:00
|
|
|
/** @var KeywordsManager */
|
|
|
|
private $keywordsManager;
|
|
|
|
|
2019-08-11 13:11:20 +00:00
|
|
|
/** @var LoggerInterface */
|
|
|
|
private $logger;
|
|
|
|
|
2019-08-29 16:50:58 +00:00
|
|
|
/**
|
|
|
|
* @var (AFPData|AFComputedVariable)[]
|
|
|
|
* @fixme This should be private, but it isn't because of T231542: there are serialized instances
|
|
|
|
* stored in the DB, and mVars wouldn't be available in HHVM after deserializing them (T213006)
|
|
|
|
*/
|
|
|
|
public $mVars = [];
|
2016-12-17 17:52:36 +00:00
|
|
|
|
2019-01-05 17:30:37 +00:00
|
|
|
/** @var bool Whether this object is being used for an ongoing action being filtered */
|
|
|
|
public $forFilter = false;
|
2016-12-17 17:52:36 +00:00
|
|
|
|
2019-08-11 13:11:20 +00:00
|
|
|
/**
|
2020-01-15 16:08:53 +00:00
|
|
|
* @param KeywordsManager|null $keywordsManager Optional for BC
|
2019-08-11 13:11:20 +00:00
|
|
|
*/
|
2020-01-15 16:08:53 +00:00
|
|
|
public function __construct( KeywordsManager $keywordsManager = null ) {
|
|
|
|
$this->keywordsManager = $keywordsManager ?? AbuseFilterServices::getKeywordsManager();
|
|
|
|
// Avoid injecting a Logger, as it's just temporary
|
2019-08-11 13:11:20 +00:00
|
|
|
$this->logger = new Psr\Log\NullLogger();
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @param LoggerInterface $logger
|
|
|
|
*/
|
|
|
|
public function setLogger( LoggerInterface $logger ) {
|
|
|
|
$this->logger = $logger;
|
|
|
|
}
|
|
|
|
|
2019-02-24 14:55:19 +00:00
|
|
|
/**
|
|
|
|
* Utility function to translate an array with shape [ varname => value ] into a self instance
|
|
|
|
*
|
|
|
|
* @param array $vars
|
2020-01-15 16:08:53 +00:00
|
|
|
* @param KeywordsManager|null $keywordsManager Optional for BC
|
2019-02-24 14:55:19 +00:00
|
|
|
* @return AbuseFilterVariableHolder
|
|
|
|
*/
|
2020-01-15 16:08:53 +00:00
|
|
|
public static function newFromArray(
|
|
|
|
array $vars,
|
|
|
|
KeywordsManager $keywordsManager = null
|
|
|
|
) : AbuseFilterVariableHolder {
|
|
|
|
$ret = new self( $keywordsManager );
|
2019-02-24 14:55:19 +00:00
|
|
|
foreach ( $vars as $var => $value ) {
|
|
|
|
$ret->setVar( $var, $value );
|
|
|
|
}
|
|
|
|
return $ret;
|
|
|
|
}
|
|
|
|
|
Rewrite the VariableHolder code to translate deprecated variables
The current code was more of a subpar, temporary solution. However, we
need a stable solution in case more variables will be deprecated in the
future (T213006 fixes the problem for the past deprecation round). So,
instead of setting a hacky property, directly translate all variables
when loading the var dump. This is not only stable, but has a couple
micro-performance advantages:
- Calling getDeprecatedVariables happens only once when loading the
dump, and not every time a variable is accessed
- No checks are needed when retrieving a variable,
because names can always assumed to be new
Some simple benchmarks reveals a runtime reduction of 8-15% compared to
the old code (8% when it had varsVersion = 2, 15% for varsVersion = 1),
which comes at no cost together with increased readability and
stability. It ain't much, but it's honest work.
Change-Id: Ib32a92c4ad939790633aa63eb3ef8d4629488bea
2020-09-21 11:54:23 +00:00
|
|
|
/**
|
|
|
|
* Checks whether any deprecated variable is stored with the old name, and replaces it with
|
|
|
|
* the new name. This should normally only happen when a DB dump is retrieved from the DB.
|
|
|
|
*/
|
|
|
|
public function translateDeprecatedVars() : void {
|
|
|
|
$deprecatedVars = $this->keywordsManager->getDeprecatedVariables();
|
|
|
|
foreach ( $this->mVars as $name => $value ) {
|
|
|
|
if ( array_key_exists( $name, $deprecatedVars ) ) {
|
|
|
|
$this->mVars[ $deprecatedVars[$name] ] = $value;
|
|
|
|
unset( $this->mVars[$name] );
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2016-12-17 17:52:36 +00:00
|
|
|
/**
|
2017-10-06 18:52:31 +00:00
|
|
|
* @param string $variable
|
|
|
|
* @param mixed $datum
|
2016-12-17 17:52:36 +00:00
|
|
|
*/
|
2018-04-04 21:14:25 +00:00
|
|
|
public function setVar( $variable, $datum ) {
|
2016-12-17 17:52:36 +00:00
|
|
|
$variable = strtolower( $variable );
|
|
|
|
if ( !( $datum instanceof AFPData || $datum instanceof AFComputedVariable ) ) {
|
|
|
|
$datum = AFPData::newFromPHPVar( $datum );
|
|
|
|
}
|
|
|
|
|
|
|
|
$this->mVars[$variable] = $datum;
|
|
|
|
}
|
|
|
|
|
2018-12-27 17:06:56 +00:00
|
|
|
/**
|
|
|
|
* Get all variables stored in this object
|
|
|
|
*
|
|
|
|
* @return (AFPData|AFComputedVariable)[]
|
|
|
|
*/
|
|
|
|
public function getVars() {
|
|
|
|
return $this->mVars;
|
|
|
|
}
|
|
|
|
|
2019-08-26 13:01:09 +00:00
|
|
|
/**
|
|
|
|
* Get a lazy loader for a variable. This method is here for testing ease
|
|
|
|
* @param string $method
|
|
|
|
* @param array $parameters
|
|
|
|
* @return AFComputedVariable
|
|
|
|
*/
|
|
|
|
public function getLazyLoader( $method, $parameters ) {
|
|
|
|
return new AFComputedVariable( $method, $parameters );
|
|
|
|
}
|
|
|
|
|
2016-12-17 17:52:36 +00:00
|
|
|
/**
|
2017-10-06 18:52:31 +00:00
|
|
|
* @param string $variable
|
|
|
|
* @param string $method
|
|
|
|
* @param array $parameters
|
2016-12-17 17:52:36 +00:00
|
|
|
*/
|
2018-04-04 21:14:25 +00:00
|
|
|
public function setLazyLoadVar( $variable, $method, $parameters ) {
|
2019-08-26 13:01:09 +00:00
|
|
|
$placeholder = $this->getLazyLoader( $method, $parameters );
|
2016-12-17 17:52:36 +00:00
|
|
|
$this->setVar( $variable, $placeholder );
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get a variable from the current object
|
|
|
|
*
|
2019-01-27 12:52:41 +00:00
|
|
|
* @param string $varName The variable name
|
2020-09-18 13:05:58 +00:00
|
|
|
* @param int $mode One of the self::GET_* constants, determines how to behave when the variable is unset:
|
|
|
|
* - GET_STRICT -> In the future, this will throw an exception. For now it returns a DUNDEFINED and logs a warning
|
|
|
|
* - GET_LAX -> Return a DUNDEFINED AFPData
|
|
|
|
* - GET_BC -> Return a DNULL AFPData (this should only be used for BC, see T230256)
|
2019-09-12 11:47:51 +00:00
|
|
|
* @param string|null $tempFilter Filter ID, if available; only used for debugging (temporarily)
|
2016-12-17 17:52:36 +00:00
|
|
|
* @return AFPData
|
|
|
|
*/
|
2020-09-18 13:05:58 +00:00
|
|
|
public function getVar( $varName, $mode = self::GET_STRICT, $tempFilter = null ) : AFPData {
|
2019-01-27 12:52:41 +00:00
|
|
|
$varName = strtolower( $varName );
|
2019-08-11 13:11:20 +00:00
|
|
|
if ( $this->varIsSet( $varName ) ) {
|
2019-01-27 12:52:41 +00:00
|
|
|
/** @var $variable AFComputedVariable|AFPData */
|
|
|
|
$variable = $this->mVars[$varName];
|
|
|
|
if ( $variable instanceof AFComputedVariable ) {
|
|
|
|
$value = $variable->compute( $this );
|
|
|
|
$this->setVar( $varName, $value );
|
2016-12-17 17:52:36 +00:00
|
|
|
return $value;
|
2019-01-27 12:52:41 +00:00
|
|
|
} elseif ( $variable instanceof AFPData ) {
|
|
|
|
return $variable;
|
2019-08-11 13:11:20 +00:00
|
|
|
} else {
|
|
|
|
throw new UnexpectedValueException(
|
|
|
|
"Variable $varName has unexpected type " . gettype( $variable )
|
|
|
|
);
|
2016-12-17 17:52:36 +00:00
|
|
|
}
|
2020-09-18 13:05:58 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// The variable is not set.
|
|
|
|
switch ( $mode ) {
|
|
|
|
case self::GET_STRICT:
|
|
|
|
$this->logger->warning(
|
|
|
|
__METHOD__ . ": requested unset variable {varname} in strict mode, filter: {filter}",
|
|
|
|
[
|
|
|
|
'varname' => $varName,
|
|
|
|
'exception' => new RuntimeException(),
|
|
|
|
'filter' => $tempFilter ?? 'unavailable'
|
|
|
|
]
|
|
|
|
);
|
|
|
|
// @todo change the line below to throw an exception in a future MW version
|
|
|
|
return new AFPData( AFPData::DUNDEFINED );
|
|
|
|
case self::GET_LAX:
|
|
|
|
return new AFPData( AFPData::DUNDEFINED );
|
|
|
|
case self::GET_BC:
|
|
|
|
// Old behaviour, which can sometimes lead to unexpected results (e.g.
|
|
|
|
// `edit_delta < -5000` will match any non-edit action).
|
|
|
|
return new AFPData( AFPData::DNULL );
|
|
|
|
default:
|
|
|
|
throw new LogicException( "Mode '$mode' not recognized." );
|
2016-12-17 17:52:36 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Merge any number of holders given as arguments into this holder.
|
|
|
|
*
|
2019-04-16 10:01:36 +00:00
|
|
|
* @param AbuseFilterVariableHolder ...$holders
|
2016-12-17 17:52:36 +00:00
|
|
|
*/
|
2019-04-16 10:01:36 +00:00
|
|
|
public function addHolders( AbuseFilterVariableHolder ...$holders ) {
|
2016-12-17 17:52:36 +00:00
|
|
|
foreach ( $holders as $addHolder ) {
|
|
|
|
$this->mVars = array_merge( $this->mVars, $addHolder->mVars );
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2018-03-26 18:41:20 +00:00
|
|
|
* Export all variables stored in this object with their native (PHP) types.
|
2016-12-17 17:52:36 +00:00
|
|
|
*
|
2019-08-26 13:01:09 +00:00
|
|
|
* @return array
|
2016-12-17 17:52:36 +00:00
|
|
|
*/
|
2018-03-26 18:41:20 +00:00
|
|
|
public function exportAllVars() {
|
2016-12-17 17:52:36 +00:00
|
|
|
$exported = [];
|
|
|
|
foreach ( array_keys( $this->mVars ) as $varName ) {
|
2018-03-26 18:41:20 +00:00
|
|
|
$exported[ $varName ] = $this->getVar( $varName )->toNative();
|
2016-12-17 17:52:36 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return $exported;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Export all non-lazy variables stored in this object as string
|
|
|
|
*
|
|
|
|
* @return string[]
|
|
|
|
*/
|
2018-04-04 21:14:25 +00:00
|
|
|
public function exportNonLazyVars() {
|
2016-12-17 17:52:36 +00:00
|
|
|
$exported = [];
|
|
|
|
foreach ( $this->mVars as $varName => $data ) {
|
2019-01-05 18:28:03 +00:00
|
|
|
if ( !( $data instanceof AFComputedVariable ) ) {
|
2016-12-17 17:52:36 +00:00
|
|
|
$exported[$varName] = $this->getVar( $varName )->toString();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return $exported;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Dump all variables stored in this object in their native types.
|
|
|
|
* If you want a not yet set variable to be included in the results you can
|
|
|
|
* either set $compute to an array with the name of the variable or set
|
|
|
|
* $compute to true to compute all not yet set variables.
|
|
|
|
*
|
2017-10-06 18:52:31 +00:00
|
|
|
* @param array|bool $compute Variables we should copute if not yet set
|
|
|
|
* @param bool $includeUserVars Include user set variables
|
2016-12-17 17:52:36 +00:00
|
|
|
* @return array
|
|
|
|
*/
|
2017-06-15 14:23:34 +00:00
|
|
|
public function dumpAllVars( $compute = [], $includeUserVars = false ) {
|
|
|
|
$coreVariables = [];
|
2016-12-17 17:52:36 +00:00
|
|
|
|
|
|
|
if ( !$includeUserVars ) {
|
|
|
|
// Compile a list of all variables set by the extension to be able
|
|
|
|
// to filter user set ones by name
|
2020-01-15 16:08:53 +00:00
|
|
|
$activeVariables = array_keys( $this->keywordsManager->getVarsMappings() );
|
|
|
|
$deprecatedVariables = array_keys( $this->keywordsManager->getDeprecatedVariables() );
|
|
|
|
$disabledVariables = array_keys( $this->keywordsManager->getDisabledVariables() );
|
2018-12-28 16:01:19 +00:00
|
|
|
$coreVariables = array_merge( $activeVariables, $deprecatedVariables, $disabledVariables );
|
2016-12-17 17:52:36 +00:00
|
|
|
$coreVariables = array_map( 'strtolower', $coreVariables );
|
|
|
|
}
|
|
|
|
|
2018-12-29 17:32:12 +00:00
|
|
|
$exported = [];
|
|
|
|
foreach ( array_keys( $this->mVars ) as $varName ) {
|
2019-01-30 19:30:59 +00:00
|
|
|
$computeThis = ( is_array( $compute ) && in_array( $varName, $compute ) ) || $compute === true;
|
2016-12-17 17:52:36 +00:00
|
|
|
if (
|
|
|
|
( $includeUserVars || in_array( strtolower( $varName ), $coreVariables ) ) &&
|
|
|
|
// Only include variables set in the extension in case $includeUserVars is false
|
2019-01-30 19:30:59 +00:00
|
|
|
( $computeThis || $this->mVars[$varName] instanceof AFPData )
|
2016-12-17 17:52:36 +00:00
|
|
|
) {
|
|
|
|
$exported[$varName] = $this->getVar( $varName )->toNative();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return $exported;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2017-10-06 18:52:31 +00:00
|
|
|
* @param string $var
|
2016-12-17 17:52:36 +00:00
|
|
|
* @return bool
|
|
|
|
*/
|
2018-04-04 21:14:25 +00:00
|
|
|
public function varIsSet( $var ) {
|
2016-12-17 17:52:36 +00:00
|
|
|
return array_key_exists( $var, $this->mVars );
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Compute all vars which need DB access. Useful for vars which are going to be saved
|
|
|
|
* cross-wiki or used for offline analysis.
|
|
|
|
*/
|
2018-04-04 21:14:25 +00:00
|
|
|
public function computeDBVars() {
|
2017-06-15 14:23:34 +00:00
|
|
|
static $dbTypes = [
|
2016-12-17 17:52:36 +00:00
|
|
|
'links-from-wikitext-or-database',
|
|
|
|
'load-recent-authors',
|
2015-04-01 00:41:09 +00:00
|
|
|
'page-age',
|
2016-12-17 17:52:36 +00:00
|
|
|
'get-page-restrictions',
|
|
|
|
'simple-user-accessor',
|
|
|
|
'user-age',
|
|
|
|
'user-groups',
|
|
|
|
'user-rights',
|
|
|
|
'revision-text-by-id',
|
|
|
|
'revision-text-by-timestamp'
|
2017-06-15 14:23:34 +00:00
|
|
|
];
|
2016-12-17 17:52:36 +00:00
|
|
|
|
2019-01-27 12:52:41 +00:00
|
|
|
/** @var AFComputedVariable[] $missingVars */
|
|
|
|
$missingVars = array_filter( $this->mVars, function ( $el ) {
|
|
|
|
return ( $el instanceof AFComputedVariable );
|
|
|
|
} );
|
|
|
|
foreach ( $missingVars as $name => $value ) {
|
|
|
|
if ( in_array( $value->mMethod, $dbTypes ) ) {
|
2016-12-17 17:52:36 +00:00
|
|
|
$value = $value->compute( $this );
|
|
|
|
$this->setVar( $name, $value );
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2020-01-15 16:08:53 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* @fixme Back-compat hack for old objects serialized and stored in the DB.
|
|
|
|
* Remove this once T213006 is done.
|
|
|
|
*/
|
|
|
|
public function __wakeup() {
|
|
|
|
$this->keywordsManager = AbuseFilterServices::getKeywordsManager();
|
|
|
|
}
|
2016-12-17 17:52:36 +00:00
|
|
|
}
|