Viewing File: /home/omtekel/www/wp-content/upgrade/backup/Variables.tar
VariablesManagerTest.php 0000666 00000021175 15133477263 0011345 0 ustar 00 <?php
namespace MediaWiki\Extension\AbuseFilter\Tests\Unit;
use LogicException;
use MediaWiki\Extension\AbuseFilter\Hooks\AbuseFilterHookRunner;
use MediaWiki\Extension\AbuseFilter\KeywordsManager;
use MediaWiki\Extension\AbuseFilter\Parser\AFPData;
use MediaWiki\Extension\AbuseFilter\Variables\LazyLoadedVariable;
use MediaWiki\Extension\AbuseFilter\Variables\LazyVariableComputer;
use MediaWiki\Extension\AbuseFilter\Variables\UnsetVariableException;
use MediaWiki\Extension\AbuseFilter\Variables\VariableHolder;
use MediaWiki\Extension\AbuseFilter\Variables\VariablesManager;
use MediaWikiUnitTestCase;
/**
* @group Test
* @group AbuseFilter
* @coversDefaultClass \MediaWiki\Extension\AbuseFilter\Variables\VariablesManager
*/
class VariablesManagerTest extends MediaWikiUnitTestCase {
/**
* @param LazyVariableComputer|null $lazyComputer
* @return VariablesManager
*/
private function getManager( LazyVariableComputer $lazyComputer = null ): VariablesManager {
return new VariablesManager(
new KeywordsManager( $this->createMock( AbuseFilterHookRunner::class ) ),
$lazyComputer ?? $this->createMock( LazyVariableComputer::class )
);
}
/**
* @covers ::translateDeprecatedVars
*/
public function testTranslateDeprecatedVars() {
$varsMap = [
'timestamp' => new AFPData( AFPData::DSTRING, '123' ),
'added_lines' => new AFPData( AFPData::DSTRING, 'foobar' ),
'article_text' => new AFPData( AFPData::DSTRING, 'FOO' ),
'article_articleid' => new AFPData( AFPData::DINT, 42 )
];
$translatedVarsMap = [
'timestamp' => $varsMap['timestamp'],
'added_lines' => $varsMap['added_lines'],
'page_title' => $varsMap['article_text'],
'page_id' => $varsMap['article_articleid']
];
$holder = VariableHolder::newFromArray( $varsMap );
$manager = $this->getManager();
$manager->translateDeprecatedVars( $holder );
$this->assertEquals( $translatedVarsMap, $holder->getVars() );
}
/**
* @param VariableHolder $holder
* @param array|bool $compute
* @param bool $includeUser
* @param array $expected
* @dataProvider provideDumpAllVars
*
* @covers ::dumpAllVars
*/
public function testDumpAllVars(
VariableHolder $holder,
$compute,
bool $includeUser,
array $expected
) {
$computer = $this->createMock( LazyVariableComputer::class );
$computer->method( 'compute' )->willReturnCallback(
static function ( LazyLoadedVariable $var ) {
switch ( $var->getMethod() ) {
case 'preftitle':
return new AFPData( AFPData::DSTRING, 'title' );
case 'lines':
return new AFPData( AFPData::DSTRING, 'lines' );
default:
throw new LogicException( 'Unrecognized value!' );
}
}
);
$manager = $this->getManager( $computer );
$this->assertEquals( $expected, $manager->dumpAllVars( $holder, $compute, $includeUser ) );
}
public static function provideDumpAllVars() {
$titleVal = 'title';
$preftitle = new LazyLoadedVariable( 'preftitle', [] );
$linesVal = 'lines';
$lines = new LazyLoadedVariable( 'lines', [] );
$pairs = [
'page_title' => 'foo',
'page_prefixedtitle' => $preftitle,
'added_lines' => $lines,
'user_name' => 'bar',
'custom-var' => 'foo'
];
$vars = VariableHolder::newFromArray( $pairs );
$nonLazy = array_fill_keys( [ 'page_title', 'user_name', 'custom-var' ], 1 );
$nonLazyExpect = array_intersect_key( $pairs, $nonLazy );
yield 'lazy-loaded vars are excluded if not computed' => [
clone $vars,
[],
true,
$nonLazyExpect
];
$nonUserExpect = array_diff_key( $nonLazyExpect, [ 'custom-var' => 1 ] );
yield 'user-set vars are excluded' => [ clone $vars, [], false, $nonUserExpect ];
$allExpect = $pairs;
$allExpect['page_prefixedtitle'] = $titleVal;
$allExpect['added_lines'] = $linesVal;
yield 'all vars computed' => [ clone $vars, true, true, $allExpect ];
$titleOnlyComputed = array_merge( $nonLazyExpect, [ 'page_prefixedtitle' => $titleVal ] );
yield 'Only a specific var computed' => [
clone $vars,
[ 'page_prefixedtitle' ],
true,
$titleOnlyComputed
];
}
/**
* @covers ::computeDBVars
*/
public function testComputeDBVars() {
$nonDBMet = [ 'unknown', 'certainly-not-db' ];
$dbMet = [ 'page-age', 'user-age', 'load-recent-authors' ];
$methods = array_merge( $nonDBMet, $dbMet );
$objs = [];
foreach ( $methods as $method ) {
$cur = new LazyLoadedVariable( $method, [] );
$objs[$method] = $cur;
}
$vars = VariableHolder::newFromArray( $objs );
$computer = $this->createMock( LazyVariableComputer::class );
$computer->method( 'compute' )->willReturnCallback(
static function ( LazyLoadedVariable $var ) {
return new AFPData( AFPData::DSTRING, $var->getMethod() );
}
);
$varManager = $this->getManager( $computer );
$varManager->computeDBVars( $vars );
$expAFCV = array_intersect_key( $vars->getVars(), array_fill_keys( $nonDBMet, 1 ) );
$this->assertContainsOnlyInstancesOf(
LazyLoadedVariable::class,
$expAFCV,
"non-DB methods shouldn't have been computed"
);
$expComputed = array_intersect_key( $vars->getVars(), array_fill_keys( $dbMet, 1 ) );
$this->assertContainsOnlyInstancesOf(
AFPData::class,
$expComputed,
'DB methods should have been computed'
);
}
/**
* @param VariablesManager $manager
* @param VariableHolder $holder
* @param string $name
* @param int $flags
* @param AFPData|string $expected String if expecting an exception
* @covers ::getVar
*
* @dataProvider provideGetVar
*/
public function testGetVar(
VariablesManager $manager,
VariableHolder $holder,
string $name,
int $flags,
$expected
) {
if ( is_string( $expected ) ) {
$this->expectException( $expected );
$manager->getVar( $holder, $name, $flags );
} else {
$this->assertEquals( $expected, $manager->getVar( $holder, $name, $flags ) );
}
}
/**
* @todo make static
* @return Generator|array
*/
public function provideGetVar() {
$vars = new VariableHolder();
$name = 'foo';
$expected = new AFPData( AFPData::DSTRING, 'foobarbaz' );
$computer = $this->createMock( LazyVariableComputer::class );
$computer->method( 'compute' )->willReturn( $expected );
$afcv = new LazyLoadedVariable( '', [] );
$vars->setVar( $name, $afcv );
yield 'set, lazy-loaded' => [ $this->getManager( $computer ), $vars, $name, 0, $expected ];
$alreadySetName = 'first-var';
$firstValue = new AFPData( AFPData::DSTRING, 'expected value' );
$setVars = VariableHolder::newFromArray( [ 'first-var' => $firstValue ] );
$computer = $this->createMock( LazyVariableComputer::class );
$computer->method( 'compute' )->willReturnCallback(
static function ( $var, $vars, $cb ) use ( $alreadySetName ) {
return $cb( $alreadySetName );
}
);
$name = 'second-var';
$manager = $this->getManager( $computer );
$lazyVar = new LazyLoadedVariable( '', [] );
$setVars->setVar( $name, $lazyVar );
yield 'set, lazy-loaded with callback' => [ $manager, $setVars, $name, 0, $firstValue ];
$name = 'afpd';
$afpd = new AFPData( AFPData::DINT, 42 );
$vars->setVar( $name, $afpd );
yield 'set, AFPData' => [ $this->getManager(), $vars, $name, 0, $afpd ];
$name = 'not-set';
$expected = new AFPData( AFPData::DUNDEFINED );
yield 'unset, lax' => [ $this->getManager(), $vars, $name, VariablesManager::GET_LAX, $expected ];
yield 'unset, strict' => [
$this->getManager(),
$vars,
$name,
VariablesManager::GET_STRICT,
UnsetVariableException::class
];
yield 'unset, bc' => [
$this->getManager(),
$vars,
$name,
VariablesManager::GET_BC,
new AFPData( AFPData::DNULL )
];
}
/**
* @covers ::exportAllVars
*/
public function testExportAllVars() {
$pairs = [
'foo' => 42,
'bar' => [ 'bar', 'baz' ],
'var' => false,
'boo' => null
];
$vars = VariableHolder::newFromArray( $pairs );
$manager = $this->getManager();
$this->assertSame( $pairs, $manager->exportAllVars( $vars ) );
}
/**
* @covers ::exportNonLazyVars
*/
public function testExportNonLazyVars() {
$afcv = $this->createMock( LazyLoadedVariable::class );
$pairs = [
'lazy1' => $afcv,
'lazy2' => $afcv,
'native1' => 42,
'native2' => 'foo',
'native3' => null,
'afpd' => new AFPData( AFPData::DSTRING, 'hey' ),
];
$vars = VariableHolder::newFromArray( $pairs );
$manager = $this->getManager();
$nonLazy = [
'native1' => '42',
'native2' => 'foo',
'native3' => '',
'afpd' => 'hey'
];
$this->assertSame( $nonLazy, $manager->exportNonLazyVars( $vars ) );
}
/**
* @covers ::__construct
*/
public function testConstruct() {
$this->assertInstanceOf(
VariablesManager::class,
new VariablesManager(
$this->createMock( KeywordsManager::class ),
$this->createMock( LazyVariableComputer::class )
)
);
}
}
VariablesBlobStoreTest.php 0000666 00000014337 15133477263 0011670 0 ustar 00 <?php
namespace MediaWiki\Extension\AbuseFilter\Tests\Unit\Variables;
use FormatJson;
use MediaWiki\Extension\AbuseFilter\KeywordsManager;
use MediaWiki\Extension\AbuseFilter\Parser\AFPData;
use MediaWiki\Extension\AbuseFilter\Variables\VariableHolder;
use MediaWiki\Extension\AbuseFilter\Variables\VariablesBlobStore;
use MediaWiki\Extension\AbuseFilter\Variables\VariablesManager;
use MediaWiki\Storage\BlobAccessException;
use MediaWiki\Storage\BlobStore;
use MediaWiki\Storage\BlobStoreFactory;
use MediaWikiUnitTestCase;
use Wikimedia\TestingAccessWrapper;
/**
* @group Test
* @group AbuseFilter
* @coversDefaultClass \MediaWiki\Extension\AbuseFilter\Variables\VariablesBlobStore
* @covers ::__construct
*/
class VariablesBlobStoreTest extends MediaWikiUnitTestCase {
private function getStore(
BlobStoreFactory $blobStoreFactory = null,
BlobStore $blobStore = null
): VariablesBlobStore {
$manager = $this->createMock( VariablesManager::class );
$manager->method( 'dumpAllVars' )->willReturnCallback( static function ( VariableHolder $holder ) {
$ret = [];
foreach ( $holder->getVars() as $name => $var ) {
if ( $var instanceof AFPData ) {
$ret[$name] = $var->toNative();
}
}
return $ret;
} );
$manager->method( 'translateDeprecatedVars' )->willReturnCallback( static function ( VariableHolder $holder ) {
$depVars = TestingAccessWrapper::constant( KeywordsManager::class, 'DEPRECATED_VARS' );
foreach ( $holder->getVars() as $name => $value ) {
if ( array_key_exists( $name, $depVars ) ) {
$holder->setVar( $depVars[$name], $value );
$holder->removeVar( $name );
}
}
} );
return new VariablesBlobStore(
$manager,
$blobStoreFactory ?? $this->createMock( BlobStoreFactory::class ),
$blobStore ?? $this->createMock( BlobStore::class ),
null
);
}
/**
* @covers ::storeVarDump
*/
public function testStoreVarDump() {
$expectID = 123456;
$blobStore = $this->createMock( BlobStore::class );
$blobStore->expects( $this->once() )->method( 'storeBlob' )->willReturn( $expectID );
$blobStoreFactory = $this->createMock( BlobStoreFactory::class );
$blobStoreFactory->method( 'newBlobStore' )->willReturn( $blobStore );
$varBlobStore = $this->getStore( $blobStoreFactory );
$this->assertSame( $expectID, $varBlobStore->storeVarDump( new VariableHolder() ) );
}
/**
* @covers ::loadVarDump
*/
public function testLoadVarDump() {
$vars = [ 'foo-variable' => 42 ];
$blob = FormatJson::encode( $vars );
$blobStore = $this->createMock( BlobStore::class );
$blobStore->expects( $this->once() )->method( 'getBlob' )->willReturn( $blob );
$varBlobStore = $this->getStore( null, $blobStore );
$loadedVars = $varBlobStore->loadVarDump( 'foo' )->getVars();
$this->assertArrayHasKey( 'foo-variable', $loadedVars );
$this->assertSame( 42, $loadedVars['foo-variable']->toNative() );
}
/**
* @covers ::loadVarDump
*/
public function testLoadVarDump_fail() {
$blobStore = $this->createMock( BlobStore::class );
$blobStore->expects( $this->once() )->method( 'getBlob' )->willThrowException( new BlobAccessException );
$varBlobStore = $this->getStore( null, $blobStore );
$this->assertCount( 0, $varBlobStore->loadVarDump( 'foo' )->getVars() );
}
private function getBlobStore(): BlobStore {
return new class implements BlobStore {
private $blobs;
private function getKey( string $data ) {
return md5( $data );
}
public function getBlob( $blobAddress, $queryFlags = 0 ) {
return $this->blobs[$blobAddress];
}
public function getBlobBatch( $blobAddresses, $queryFlags = 0 ) {
}
public function storeBlob( $data, $hints = [] ) {
$key = $this->getKey( $data );
$this->blobs[$key] = $data;
return $key;
}
public function isReadOnly() {
}
};
}
/**
* @covers ::storeVarDump
* @covers ::loadVarDump
* @dataProvider provideVariables
*/
public function testRoundTrip( array $toStore, array $expected = null ) {
$blobStore = $this->getBlobStore();
$blobStoreFactory = $this->createMock( BlobStoreFactory::class );
$blobStoreFactory->method( 'newBlobStore' )->willReturn( $blobStore );
$varBlobStore = $this->getStore( $blobStoreFactory, $blobStore );
$storeID = $varBlobStore->storeVarDump( VariableHolder::newFromArray( $toStore ) );
$this->assertIsString( $storeID );
$loadedVars = $varBlobStore->loadVarDump( $storeID )->getVars();
$nativeLoadedVars = array_map( static function ( AFPData $el ) {
return $el->toNative();
}, $loadedVars );
$this->assertSame( $expected ?? $toStore, $nativeLoadedVars );
}
/**
* Data provider for testVarDump
*
* @return array
*/
public static function provideVariables() {
return [
'Only basic variables' => [
[
'action' => 'edit',
'old_wikitext' => 'Old text',
'new_wikitext' => 'New text'
]
],
'Normal case' => [
[
'action' => 'edit',
'old_wikitext' => 'Old text',
'new_wikitext' => 'New text',
'user_editcount' => 15,
'added_lines' => [ 'Foo', '', 'Bar' ]
]
],
'Deprecated variables' => [
[
'action' => 'edit',
'old_wikitext' => 'Old text',
'new_wikitext' => 'New text',
'article_articleid' => 11745,
'article_first_contributor' => 'Good guy'
],
[
'action' => 'edit',
'old_wikitext' => 'Old text',
'new_wikitext' => 'New text',
'page_id' => 11745,
'page_first_contributor' => 'Good guy'
]
],
'Move action' => [
[
'action' => 'move',
'old_wikitext' => 'Old text',
'new_wikitext' => 'New text',
'all_links' => [ 'https://en.wikipedia.org' ],
'moved_to_id' => 156,
'moved_to_prefixedtitle' => 'MediaWiki:Foobar.js',
'new_content_model' => CONTENT_MODEL_JAVASCRIPT
]
],
'Delete action' => [
[
'old_wikitext' => 'Old text',
'new_wikitext' => 'New text',
'timestamp' => 1546000295,
'action' => 'delete',
'page_namespace' => 114
]
],
'Disabled vars' => [
[
'action' => 'edit',
'old_wikitext' => 'Old text',
'new_wikitext' => 'New text',
'old_html' => 'Foo <small>bar</small> <s>lol</s>.',
'old_text' => 'Foobar'
]
],
'Account creation' => [
[
'action' => 'createaccount',
'accountname' => 'XXX'
]
]
];
}
}
LazyLoadedVariableTest.php 0000666 00000001307 15133477263 0011633 0 ustar 00 <?php
namespace MediaWiki\Extension\AbuseFilter\Tests\Unit;
use MediaWiki\Extension\AbuseFilter\Variables\LazyLoadedVariable;
use MediaWikiUnitTestCase;
/**
* @group Test
* @group AbuseFilter
* @coversDefaultClass \MediaWiki\Extension\AbuseFilter\Variables\LazyLoadedVariable
*/
class LazyLoadedVariableTest extends MediaWikiUnitTestCase {
/**
* @covers ::__construct
* @covers ::getMethod
* @covers ::getParameters
*/
public function testGetters() {
$method = 'magic';
$params = [ 'foo', true, 1, null ];
$obj = new LazyLoadedVariable( $method, $params );
$this->assertSame( $method, $obj->getMethod(), 'method' );
$this->assertSame( $params, $obj->getParameters(), 'params' );
}
}
LazyVariableComputerTest.php 0000666 00000025334 15133477263 0012247 0 ustar 00 <?php
namespace MediaWiki\Extension\AbuseFilter\Tests\Unit;
use Generator;
use Language;
use LogicException;
use MediaWiki\Block\SystemBlock;
use MediaWiki\Extension\AbuseFilter\Hooks\AbuseFilterHookRunner;
use MediaWiki\Extension\AbuseFilter\Parser\AFPData;
use MediaWiki\Extension\AbuseFilter\TextExtractor;
use MediaWiki\Extension\AbuseFilter\Variables\LazyLoadedVariable;
use MediaWiki\Extension\AbuseFilter\Variables\LazyVariableComputer;
use MediaWiki\Extension\AbuseFilter\Variables\VariableHolder;
use MediaWiki\Permissions\PermissionManager;
use MediaWiki\Permissions\RestrictionStore;
use MediaWiki\Revision\RevisionLookup;
use MediaWiki\Revision\RevisionRecord;
use MediaWiki\Revision\RevisionStore;
use MediaWiki\Title\Title;
use MediaWiki\User\UserEditTracker;
use MediaWiki\User\UserGroupManager;
use MediaWiki\User\UserIdentityValue;
use MediaWikiUnitTestCase;
use ParserFactory;
use Psr\Log\NullLogger;
use UnexpectedValueException;
use User;
use WANObjectCache;
use Wikimedia\Rdbms\LBFactory;
/**
* @coversDefaultClass \MediaWiki\Extension\AbuseFilter\Variables\LazyVariableComputer
* @covers ::__construct
*/
class LazyVariableComputerTest extends MediaWikiUnitTestCase {
private function getComputer(
array $services = [],
array $hookHandlers = [],
string $wikiID = ''
): LazyVariableComputer {
return new LazyVariableComputer(
$this->createMock( TextExtractor::class ),
new AbuseFilterHookRunner( $this->createHookContainer( $hookHandlers ) ),
new NullLogger(),
$this->createMock( LBFactory::class ),
$this->createMock( WANObjectCache::class ),
$services['RevisionLookup'] ?? $this->createMock( RevisionLookup::class ),
$this->createMock( RevisionStore::class ),
$services['ContentLanguage'] ?? $this->createMock( Language::class ),
$this->createMock( ParserFactory::class ),
$services['UserEditTracker'] ?? $this->createMock( UserEditTracker::class ),
$services['UserGroupManager'] ?? $this->createMock( UserGroupManager::class ),
$services['PermissionManager'] ?? $this->createMock( PermissionManager::class ),
$services['RestrictionStore'] ?? $this->createMock( RestrictionStore::class ),
$wikiID
);
}
private function getForbidComputeCB(): callable {
return static function () {
throw new LogicException( 'Not expected to be called' );
};
}
/**
* @covers ::compute
*/
public function testWikiNameVar() {
$fakeID = 'some-wiki-ID';
$var = new LazyLoadedVariable( 'get-wiki-name', [] );
$computer = $this->getComputer( [], [], $fakeID );
$this->assertSame(
$fakeID,
$computer->compute( $var, new VariableHolder(), $this->getForbidComputeCB() )->toNative()
);
}
/**
* @covers ::compute
*/
public function testWikiLanguageVar() {
$fakeCode = 'foobar';
$fakeLang = $this->createMock( Language::class );
$fakeLang->method( 'getCode' )->willReturn( $fakeCode );
$computer = $this->getComputer( [ 'ContentLanguage' => $fakeLang ] );
$var = new LazyLoadedVariable( 'get-wiki-language', [] );
$this->assertSame(
$fakeCode,
$computer->compute( $var, new VariableHolder(), $this->getForbidComputeCB() )->toNative()
);
}
/**
* @covers ::compute
*/
public function testCompute_invalidName() {
$computer = $this->getComputer();
$this->expectException( UnexpectedValueException::class );
$computer->compute(
new LazyLoadedVariable( 'method-does-not-exist', [] ),
new VariableHolder(),
$this->getForbidComputeCB()
);
}
/**
* @covers ::compute
*/
public function testInterceptVariableHook() {
$expected = new AFPData( AFPData::DSTRING, 'foobar' );
$handler = static function ( $method, $vars, $params, &$result ) use ( $expected ) {
$result = $expected;
return false;
};
$computer = $this->getComputer( [], [ 'AbuseFilter-interceptVariable' => $handler ] );
$actual = $computer->compute(
new LazyLoadedVariable( 'get-wiki-name', [] ),
new VariableHolder(),
$this->getForbidComputeCB()
);
$this->assertSame( $expected, $actual );
}
/**
* @covers ::compute
*/
public function testComputeVariableHook() {
$expected = new AFPData( AFPData::DSTRING, 'foobar' );
$handler = static function ( $method, $vars, $params, &$result ) use ( $expected ) {
$result = $expected;
return false;
};
$computer = $this->getComputer( [], [ 'AbuseFilter-computeVariable' => $handler ] );
$actual = $computer->compute(
new LazyLoadedVariable( 'method-does-not-exist', [] ),
new VariableHolder(),
$this->getForbidComputeCB()
);
$this->assertSame( $expected, $actual );
}
/**
* @param LazyLoadedVariable $var
* @param mixed $expected
* @param array $services
* @covers ::compute
* @dataProvider provideUserRelatedVars
*/
public function testUserRelatedVars(
LazyLoadedVariable $var,
$expected,
array $services = []
) {
$computer = $this->getComputer( $services );
$this->assertSame(
$expected,
$computer->compute( $var, new VariableHolder(), $this->getForbidComputeCB() )->toNative()
);
}
public function provideUserRelatedVars(): Generator {
$user = $this->createMock( User::class );
$getUserVar = static function ( $user, $method ): LazyLoadedVariable {
return new LazyLoadedVariable(
$method,
[ 'user' => $user, 'user-identity' => $user ]
);
};
$editCount = 7;
$userEditTracker = $this->createMock( UserEditTracker::class );
$userEditTracker->method( 'getUserEditCount' )->with( $user )->willReturn( $editCount );
$var = $getUserVar( $user, 'user-editcount' );
yield 'user_editcount' => [ $var, $editCount, [ 'UserEditTracker' => $userEditTracker ] ];
$emailConfirm = '20000101000000';
$user->method( 'getEmailAuthenticationTimestamp' )->willReturn( $emailConfirm );
$var = $getUserVar( $user, 'user-emailconfirm' );
yield 'user_emailconfirm' => [ $var, $emailConfirm ];
$groups = [ '*', 'group1', 'group2' ];
$userGroupManager = $this->createMock( UserGroupManager::class );
$userGroupManager->method( 'getUserEffectiveGroups' )->with( $user )->willReturn( $groups );
$var = $getUserVar( $user, 'user-groups' );
yield 'user_groups' => [ $var, $groups, [ 'UserGroupManager' => $userGroupManager ] ];
$rights = [ 'abusefilter-foo', 'abusefilter-bar' ];
$permissionManager = $this->createMock( PermissionManager::class );
$permissionManager->method( 'getUserPermissions' )->with( $user )->willReturn( $rights );
$var = $getUserVar( $user, 'user-rights' );
yield 'user_rights' => [ $var, $rights, [ 'PermissionManager' => $permissionManager ] ];
$block = new SystemBlock( [] );
$user->method( 'getBlock' )->willReturn( $block );
$var = $getUserVar( $user, 'user-block' );
yield 'user_blocked' => [ $var, (bool)$block ];
$fakeTime = 1514700000;
$anonUser = $this->createMock( User::class );
$anonymousAge = 0;
$var = new LazyLoadedVariable(
'user-age',
[ 'user' => $anonUser, 'asof' => $fakeTime ]
);
yield 'user_age, anonymous' => [ $var, $anonymousAge ];
$user->method( 'isRegistered' )->willReturn( true );
$missingRegistrationUser = clone $user;
$var = new LazyLoadedVariable(
'user-age',
[ 'user' => $missingRegistrationUser, 'asof' => $fakeTime ]
);
$expected = (int)wfTimestamp( TS_UNIX, $fakeTime ) - (int)wfTimestamp( TS_UNIX, '20080115000000' );
yield 'user_age, registered but not available' => [ $var, $expected ];
$age = 163;
$user->method( 'getRegistration' )->willReturn( $fakeTime - $age );
$var = new LazyLoadedVariable(
'user-age',
[ 'user' => $user, 'asof' => $fakeTime ]
);
yield 'user_age, registered' => [ $var, $age ];
}
/**
* @param LazyLoadedVariable $var
* @param mixed $expected
* @param array $services
* @covers ::compute
* @dataProvider provideTitleRelatedVars
*/
public function testTitleRelatedVars(
LazyLoadedVariable $var,
$expected,
array $services = []
) {
$computer = $this->getComputer( $services );
$this->assertSame(
$expected,
$computer->compute( $var, new VariableHolder(), $this->getForbidComputeCB() )->toNative()
);
}
public function provideTitleRelatedVars(): Generator {
$restrictions = [ 'create', 'edit', 'move', 'upload' ];
foreach ( $restrictions as $restriction ) {
$appliedRestrictions = [ 'sysop' ];
$restrictedTitle = $this->createMock( Title::class );
$restrictionStore = $this->createMock( RestrictionStore::class );
$restrictionStore->expects( $this->once() )
->method( 'getRestrictions' )
->with( $restrictedTitle, $restriction )
->willReturn( $appliedRestrictions );
$var = new LazyLoadedVariable(
'get-page-restrictions',
[ 'title' => $restrictedTitle, 'action' => $restriction ]
);
yield "*_restrictions_{$restriction}, restricted" => [
$var, $appliedRestrictions, [ 'RestrictionStore' => $restrictionStore ]
];
$unrestrictedTitle = $this->createMock( Title::class );
$restrictionStore = $this->createMock( RestrictionStore::class );
$restrictionStore->expects( $this->once() )
->method( 'getRestrictions' )
->with( $unrestrictedTitle, $restriction )
->willReturn( [] );
$var = new LazyLoadedVariable(
'get-page-restrictions',
[ 'title' => $unrestrictedTitle, 'action' => $restriction ]
);
yield "*_restrictions_{$restriction}, unrestricted" => [
$var, [], [ 'RestrictionStore' => $restrictionStore ]
];
}
$fakeTime = 1514700000;
$age = 163;
$title = $this->createMock( Title::class );
$revision = $this->createMock( RevisionRecord::class );
$revision->method( 'getTimestamp' )->willReturn( $fakeTime - $age );
$revLookup = $this->createMock( RevisionLookup::class );
$revLookup->method( 'getFirstRevision' )->with( $title )->willReturn( $revision );
$var = new LazyLoadedVariable(
'page-age',
[ 'title' => $title, 'asof' => $fakeTime ]
);
yield "*_age" => [ $var, $age, [ 'RevisionLookup' => $revLookup ] ];
$title = $this->createMock( Title::class );
$firstRev = $this->createMock( RevisionRecord::class );
$firstUserName = 'First author';
$firstUser = new UserIdentityValue( 1, $firstUserName );
$firstRev->expects( $this->once() )->method( 'getUser' )->willReturn( $firstUser );
$revLookup = $this->createMock( RevisionLookup::class );
$revLookup->method( 'getFirstRevision' )->with( $title )->willReturn( $firstRev );
$var = new LazyLoadedVariable(
'load-first-author',
[ 'title' => $title ]
);
yield '*_first_contributor, with rev' => [
$var, $firstUserName, [ 'RevisionLookup' => $revLookup ]
];
$title = $this->createMock( Title::class );
$revLookup = $this->createMock( RevisionLookup::class );
$revLookup->method( 'getFirstRevision' )->with( $title )->willReturn( null );
$var = new LazyLoadedVariable(
'load-first-author',
[ 'title' => $title ]
);
yield '*_first_contributor, no rev' => [ $var, '', [ 'RevisionLookup' => $revLookup ] ];
// TODO _recent_contributors is tested in LazyVariableComputerDBTest
}
}
VariableHolderTest.php 0000666 00000013566 15133477263 0011032 0 ustar 00 <?php
/**
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along
* with this program; if not, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
* http://www.gnu.org/copyleft/gpl.html
*
* @file
*
* @license GPL-2.0-or-later
*/
namespace MediaWiki\Extension\AbuseFilter\Tests\Unit;
use MediaWiki\Extension\AbuseFilter\Parser\AFPData;
use MediaWiki\Extension\AbuseFilter\Variables\LazyLoadedVariable;
use MediaWiki\Extension\AbuseFilter\Variables\UnsetVariableException;
use MediaWiki\Extension\AbuseFilter\Variables\VariableHolder;
use MediaWikiUnitTestCase;
/**
* @group Test
* @group AbuseFilter
* @group AbuseFilterParser
* @coversDefaultClass \MediaWiki\Extension\AbuseFilter\Variables\VariableHolder
*/
class VariableHolderTest extends MediaWikiUnitTestCase {
/**
* @covers ::newFromArray
*/
public function testNewFromArray() {
$vars = [
'foo' => 12,
'bar' => [ 'x', 'y' ],
'baz' => false
];
$actual = VariableHolder::newFromArray( $vars );
$expected = new VariableHolder();
foreach ( $vars as $var => $value ) {
$expected->setVar( $var, $value );
}
$this->assertEquals( $expected, $actual );
}
/**
* @covers ::setVar
*/
public function testVarsAreLowercased() {
$vars = new VariableHolder();
$this->assertCount( 0, $vars->getVars(), 'precondition' );
$vars->setVar( 'FOO', 42 );
$this->assertCount( 1, $vars->getVars(), 'variable should be set' );
$this->assertArrayHasKey( 'foo', $vars->getVars(), 'var should be lowercase' );
}
/**
* @param string $name
* @param mixed $val
* @param mixed $expected
*
* @dataProvider provideSetVar
*
* @covers ::setVar
*/
public function testSetVar( string $name, $val, $expected ) {
$vars = new VariableHolder();
$vars->setVar( $name, $val );
$this->assertEquals( $expected, $vars->getVars()[$name] );
}
public static function provideSetVar() {
yield 'native' => [ 'foo', 12, new AFPData( AFPData::DINT, 12 ) ];
$afpdata = new AFPData( AFPData::DSTRING, 'foobar' );
yield 'AFPData' => [ 'foo', $afpdata, $afpdata ];
$lazyloadVar = new LazyLoadedVariable( 'foo', [] );
yield 'lazy-loaded' => [ 'foo', $lazyloadVar, $lazyloadVar ];
}
/**
* @covers ::getVars
*/
public function testGetVars() {
$vars = new VariableHolder();
$this->assertSame( [], $vars->getVars(), 'precondition' );
$vars->setVar( 'foo', [ true ] );
$vars->setVar( 'bar', 'bar' );
$exp = [
'foo' => new AFPData( AFPData::DARRAY, [ new AFPData( AFPData::DBOOL, true ) ] ),
'bar' => new AFPData( AFPData::DSTRING, 'bar' )
];
$this->assertEquals( $exp, $vars->getVars() );
}
/**
* @param VariableHolder $vars
* @param string $name
* @param AFPData|LazyLoadedVariable $expected
* @covers ::getVarThrow
*
* @dataProvider provideGetVarThrow
*/
public function testGetVarThrow( VariableHolder $vars, string $name, $expected ) {
$this->assertEquals( $expected, $vars->getVarThrow( $name ) );
}
public static function provideGetVarThrow() {
$vars = new VariableHolder();
$name = 'foo';
$afcv = new LazyLoadedVariable( 'method', [ 'param' ] );
$vars->setVar( $name, $afcv );
yield 'set, lazy-loaded' => [ $vars, $name, $afcv ];
$name = 'afpd';
$afpd = new AFPData( AFPData::DINT, 42 );
$vars->setVar( $name, $afpd );
yield 'set, AFPData' => [ $vars, $name, $afpd ];
}
/**
* @covers ::getVarThrow
*/
public function testGetVarThrow_unset() {
$vars = new VariableHolder();
$this->expectException( UnsetVariableException::class );
$vars->getVarThrow( 'unset-variable' );
}
/**
* @param array $expected
* @param VariableHolder ...$holders
* @dataProvider provideHoldersForAddition
*
* @covers ::addHolders
*/
public function testAddHolders( array $expected, VariableHolder ...$holders ) {
$actual = new VariableHolder();
$actual->addHolders( ...$holders );
$this->assertEquals( $expected, $actual->getVars() );
}
public static function provideHoldersForAddition() {
$v1 = VariableHolder::newFromArray( [ 'a' => 1, 'b' => 2 ] );
$v2 = VariableHolder::newFromArray( [ 'b' => 3, 'c' => 4 ] );
$v3 = VariableHolder::newFromArray( [ 'c' => 5, 'd' => 6 ] );
$expected = [
'a' => new AFPData( AFPData::DINT, 1 ),
'b' => new AFPData( AFPData::DINT, 3 ),
'c' => new AFPData( AFPData::DINT, 5 ),
'd' => new AFPData( AFPData::DINT, 6 )
];
return [ [ $expected, $v1, $v2, $v3 ] ];
}
/**
* @covers ::varIsSet
*/
public function testVarIsSet() {
$vars = new VariableHolder();
$vars->setVar( 'foo', null );
$this->assertTrue( $vars->varIsSet( 'foo' ), 'Set variable should be set' );
$this->assertFalse( $vars->varIsSet( 'foobarbaz' ), 'Unset variable should be unset' );
}
/**
* @covers ::setLazyLoadVar
*/
public function testLazyLoader() {
$var = 'foobar';
$method = 'compute-foo';
$params = [ 'baz', 1 ];
$exp = new LazyLoadedVariable( $method, $params );
$vars = new VariableHolder();
$vars->setLazyLoadVar( $var, $method, $params );
$this->assertEquals( $exp, $vars->getVars()[$var] );
}
/**
* @covers ::removeVar
*/
public function testRemoveVar() {
$vars = new VariableHolder();
$varName = 'foo';
$vars->setVar( $varName, 'foobar' );
$this->assertInstanceOf( AFPData::class, $vars->getVarThrow( $varName ) );
$vars->removeVar( $varName );
$this->expectException( UnsetVariableException::class );
$vars->getVarThrow( $varName );
}
}
VariablesFormatterTest.php 0000666 00000010742 15133477263 0011734 0 ustar 00 <?php
namespace MediaWiki\Extension\AbuseFilter\Tests\Unit;
use MediaWiki\Extension\AbuseFilter\KeywordsManager;
use MediaWiki\Extension\AbuseFilter\Variables\VariableHolder;
use MediaWiki\Extension\AbuseFilter\Variables\VariablesFormatter;
use MediaWiki\Extension\AbuseFilter\Variables\VariablesManager;
use MediaWikiUnitTestCase;
use MessageLocalizer;
use Wikimedia\TestingAccessWrapper;
/**
* @coversDefaultClass \MediaWiki\Extension\AbuseFilter\Variables\VariablesFormatter
*/
class VariablesFormatterTest extends MediaWikiUnitTestCase {
/**
* @covers ::__construct
*/
public function testConstruct() {
$this->assertInstanceOf(
VariablesFormatter::class,
new VariablesFormatter(
$this->createMock( KeywordsManager::class ),
$this->createMock( VariablesManager::class ),
$this->createMock( MessageLocalizer::class )
)
);
}
/**
* @covers ::setMessageLocalizer
*/
public function testSetMessageLocalizer() {
$formatter = new VariablesFormatter(
$this->createMock( KeywordsManager::class ),
$this->createMock( VariablesManager::class ),
$this->createMock( MessageLocalizer::class )
);
$ml = $this->createMock( MessageLocalizer::class );
$formatter->setMessageLocalizer( $ml );
/** @var VariablesFormatter $wrapper */
$wrapper = TestingAccessWrapper::newFromObject( $formatter );
$this->assertSame( $ml, $wrapper->messageLocalizer );
}
/**
* @param mixed $var
* @param string $expected
* @covers ::formatVar
* @dataProvider provideFormatVar
*/
public function testFormatVar( $var, string $expected ) {
$this->assertSame( $expected, VariablesFormatter::formatVar( $var ) );
}
/**
* Provider for testFormatVar
* @return array
*/
public static function provideFormatVar() {
return [
'boolean' => [ true, 'true' ],
'single-quote string' => [ 'foo', "'foo'" ],
'string with quotes' => [ "ba'r'", "'ba'r''" ],
'integer' => [ 42, '42' ],
'float' => [ 0.1, '0.1' ],
'null' => [ null, 'null' ],
'simple list' => [ [ true, 1, 'foo' ], "[\n\t0 => true,\n\t1 => 1,\n\t2 => 'foo'\n]" ],
'assoc array' => [ [ 'foo' => 1, 'bar' => 'bar' ], "[\n\t'foo' => 1,\n\t'bar' => 'bar'\n]" ],
'nested array' => [
[ 'a1' => 1, [ 'a2' => 2, [ 'a3' => 3, [ 'a4' => 4 ] ] ] ],
"[\n\t'a1' => 1,\n\t0 => [\n\t\t'a2' => 2,\n\t\t0 => [\n\t\t\t'a3' => 3,\n\t\t\t0 => " .
"[\n\t\t\t\t'a4' => 4\n\t\t\t]\n\t\t]\n\t]\n]"
],
'empty array' => [ [], '[]' ],
'mixed array' => [
[ 3 => true, 'foo' => false, 1, [ 1, 'foo' => 42 ] ],
"[\n\t3 => true,\n\t'foo' => false,\n\t4 => 1,\n\t5 => [\n\t\t0 => 1,\n\t\t'foo' => 42\n\t]\n]"
]
];
}
/**
* @covers ::buildVarDumpTable
*/
public function testBuildVarDumpTable_empty() {
$ml = $this->createMock( MessageLocalizer::class );
$ml->method( 'msg' )->willReturnCallback( function ( $key ) {
return $this->getMockMessage( $key );
} );
$formatter = new VariablesFormatter(
$this->createMock( KeywordsManager::class ),
$this->createMock( VariablesManager::class ),
$ml
);
$actual = $formatter->buildVarDumpTable( new VariableHolder() );
$this->assertStringContainsString( 'abusefilter-log-details-var', $actual, 'header' );
$this->assertStringNotContainsString( 'mw-abuselog-var-value', $actual, 'no values' );
}
/**
* @covers ::buildVarDumpTable
*/
public function testBuildVarDumpTable() {
$ml = $this->createMock( MessageLocalizer::class );
$ml->method( 'msg' )->willReturnCallback( function ( $key ) {
return $this->getMockMessage( $key );
} );
$kManager = $this->createMock( KeywordsManager::class );
$varMessage = 'Dummy variable message';
$varArray = [ 'foo' => true, 'bar' => 'foobar' ];
$kManager->expects( $this->atLeastOnce() )
->method( 'getMessageKeyForVar' )
->willReturnCallback( static function ( $var ) use ( $varMessage ) {
return $var === 'foo' ? $varMessage : null;
} );
$holder = VariableHolder::newFromArray( $varArray );
$varManager = $this->createMock( VariablesManager::class );
$varManager->method( 'exportAllVars' )->willReturn( $varArray );
$formatter = new VariablesFormatter( $kManager, $varManager, $ml );
$actual = $formatter->buildVarDumpTable( $holder );
$this->assertStringContainsString( 'abusefilter-log-details-var', $actual, 'header' );
$this->assertStringContainsString( 'mw-abuselog-var-value', $actual, 'values' );
$this->assertStringContainsString( '<code', $actual, 'formatted var name' );
$this->assertSame( 1, substr_count( $actual, $varMessage ), 'only one var with message' );
}
}
LazyLoadedVariable.php 0000666 00000001232 15133501624 0010755 0 ustar 00 <?php
namespace MediaWiki\Extension\AbuseFilter\Variables;
class LazyLoadedVariable {
/**
* @var string The method used to compute the variable
*/
private $method;
/**
* @var array Parameters to be used with the specified method
*/
private $parameters;
/**
* @param string $method
* @param array $parameters
*/
public function __construct( string $method, array $parameters ) {
$this->method = $method;
$this->parameters = $parameters;
}
/**
* @return string
*/
public function getMethod(): string {
return $this->method;
}
/**
* @return array
*/
public function getParameters(): array {
return $this->parameters;
}
}
UnsetVariableException.php 0000666 00000000500 15133501624 0011677 0 ustar 00 <?php
namespace MediaWiki\Extension\AbuseFilter\Variables;
use RuntimeException;
/**
* @codeCoverageIgnore
*/
class UnsetVariableException extends RuntimeException {
/**
* @param string $varName
*/
public function __construct( string $varName ) {
parent::__construct( "Variable $varName is not set" );
}
}
VariablesFormatter.php 0000666 00000007275 15133501624 0011070 0 ustar 00 <?php
namespace MediaWiki\Extension\AbuseFilter\Variables;
use Html;
use MediaWiki\Extension\AbuseFilter\KeywordsManager;
use MessageLocalizer;
use Xml;
/**
* Pretty-prints the content of a VariableHolder for use e.g. in AbuseLog hit details
*/
class VariablesFormatter {
public const SERVICE_NAME = 'AbuseFilterVariablesFormatter';
/** @var KeywordsManager */
private $keywordsManager;
/** @var VariablesManager */
private $varManager;
/** @var MessageLocalizer */
private $messageLocalizer;
/**
* @param KeywordsManager $keywordsManager
* @param VariablesManager $variablesManager
* @param MessageLocalizer $messageLocalizer
*/
public function __construct(
KeywordsManager $keywordsManager,
VariablesManager $variablesManager,
MessageLocalizer $messageLocalizer
) {
$this->keywordsManager = $keywordsManager;
$this->varManager = $variablesManager;
$this->messageLocalizer = $messageLocalizer;
}
/**
* @param MessageLocalizer $messageLocalizer
*/
public function setMessageLocalizer( MessageLocalizer $messageLocalizer ): void {
$this->messageLocalizer = $messageLocalizer;
}
/**
* @param VariableHolder $varHolder
* @return string
*/
public function buildVarDumpTable( VariableHolder $varHolder ): string {
$vars = $this->varManager->exportAllVars( $varHolder );
$output = '';
$output .=
Xml::openElement( 'table', [ 'class' => 'mw-abuselog-details' ] ) .
Xml::openElement( 'tbody' ) .
"\n";
$header =
Xml::element( 'th', null, $this->messageLocalizer->msg( 'abusefilter-log-details-var' )->text() ) .
Xml::element( 'th', null, $this->messageLocalizer->msg( 'abusefilter-log-details-val' )->text() );
$output .= Xml::tags( 'tr', null, $header ) . "\n";
if ( !count( $vars ) ) {
$output .= Xml::closeElement( 'tbody' ) . Xml::closeElement( 'table' );
return $output;
}
// Now, build the body of the table.
foreach ( $vars as $key => $value ) {
$key = strtolower( $key );
$varMsgKey = $this->keywordsManager->getMessageKeyForVar( $key );
if ( $varMsgKey ) {
$keyDisplay = $this->messageLocalizer->msg( $varMsgKey )->parse() . ' ' .
Html::element(
'code',
[],
// @phan-suppress-next-line SecurityCheck-XSS Keys are safe
$this->messageLocalizer->msg( 'parentheses' )->rawParams( $key )->text()
);
} else {
$keyDisplay = Html::element( 'code', [], $key );
}
$value = Html::element(
'div',
[ 'class' => 'mw-abuselog-var-value' ],
self::formatVar( $value )
);
$trow =
Xml::tags( 'td', [ 'class' => 'mw-abuselog-var' ], $keyDisplay ) .
Xml::tags( 'td', [ 'class' => 'mw-abuselog-var-value' ], $value );
$output .=
Xml::tags( 'tr',
[ 'class' => "mw-abuselog-details-$key mw-abuselog-value" ], $trow
) . "\n";
}
$output .= Xml::closeElement( 'tbody' ) . Xml::closeElement( 'table' );
return $output;
}
/**
* @param mixed $var
* @param string $indent
* @return string
*/
public static function formatVar( $var, string $indent = '' ): string {
if ( $var === [] ) {
return '[]';
} elseif ( is_array( $var ) ) {
$ret = '[';
$indent .= "\t";
foreach ( $var as $key => $val ) {
$ret .= "\n$indent" . self::formatVar( $key, $indent ) .
' => ' . self::formatVar( $val, $indent ) . ',';
}
// Strip trailing commas
return substr( $ret, 0, -1 ) . "\n" . substr( $indent, 0, -1 ) . ']';
} elseif ( is_string( $var ) ) {
// Don't escape the string (specifically backslashes) to avoid displaying wrong stuff
return "'$var'";
} elseif ( $var === null ) {
return 'null';
} elseif ( is_float( $var ) ) {
// Don't let float precision produce weirdness
return (string)$var;
}
return var_export( $var, true );
}
}
VariablesManager.php 0000666 00000015535 15133501624 0010475 0 ustar 00 <?php
namespace MediaWiki\Extension\AbuseFilter\Variables;
use LogicException;
use MediaWiki\Extension\AbuseFilter\KeywordsManager;
use MediaWiki\Extension\AbuseFilter\Parser\AFPData;
/**
* Service that allows manipulating a VariableHolder
*/
class VariablesManager {
public const SERVICE_NAME = 'AbuseFilterVariablesManager';
/**
* Used in self::getVar() to determine what to do if the requested variable is missing. See
* the docs of that method for an explanation.
*/
public const GET_LAX = 0;
public const GET_STRICT = 1;
public const GET_BC = 2;
/** @var KeywordsManager */
private $keywordsManager;
/** @var LazyVariableComputer */
private $lazyComputer;
/**
* @param KeywordsManager $keywordsManager
* @param LazyVariableComputer $lazyComputer
*/
public function __construct(
KeywordsManager $keywordsManager,
LazyVariableComputer $lazyComputer
) {
$this->keywordsManager = $keywordsManager;
$this->lazyComputer = $lazyComputer;
}
/**
* 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.
*
* @param VariableHolder $holder
*/
public function translateDeprecatedVars( VariableHolder $holder ): void {
$deprecatedVars = $this->keywordsManager->getDeprecatedVariables();
foreach ( $holder->getVars() as $name => $value ) {
if ( array_key_exists( $name, $deprecatedVars ) ) {
$holder->setVar( $deprecatedVars[$name], $value );
$holder->removeVar( $name );
}
}
}
/**
* Get a variable from the current object
*
* @param VariableHolder $holder
* @param string $varName The variable name
* @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)
* @return AFPData
*/
public function getVar(
VariableHolder $holder,
string $varName,
$mode = self::GET_STRICT
): AFPData {
$varName = strtolower( $varName );
if ( $holder->varIsSet( $varName ) ) {
/** @var $variable LazyLoadedVariable|AFPData */
$variable = $holder->getVarThrow( $varName );
if ( $variable instanceof LazyLoadedVariable ) {
$getVarCB = function ( string $varName ) use ( $holder ): AFPData {
return $this->getVar( $holder, $varName );
};
$value = $this->lazyComputer->compute( $variable, $holder, $getVarCB );
$holder->setVar( $varName, $value );
return $value;
} elseif ( $variable instanceof AFPData ) {
return $variable;
} else {
// @codeCoverageIgnoreStart
throw new \UnexpectedValueException(
"Variable $varName has unexpected type " . gettype( $variable )
);
// @codeCoverageIgnoreEnd
}
}
// The variable is not set.
switch ( $mode ) {
case self::GET_STRICT:
throw new UnsetVariableException( $varName );
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:
// @codeCoverageIgnoreStart
throw new LogicException( "Mode '$mode' not recognized." );
// @codeCoverageIgnoreEnd
}
}
/**
* Dump all variables stored in the holder 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.
*
* @param VariableHolder $holder
* @param array|bool $compute Variables we should compute if not yet set
* @param bool $includeUserVars Include user set variables
* @return array
*/
public function dumpAllVars(
VariableHolder $holder,
$compute = [],
bool $includeUserVars = false
): array {
$coreVariables = [];
if ( !$includeUserVars ) {
// Compile a list of all variables set by the extension to be able
// to filter user set ones by name
$activeVariables = array_keys( $this->keywordsManager->getVarsMappings() );
$deprecatedVariables = array_keys( $this->keywordsManager->getDeprecatedVariables() );
$disabledVariables = array_keys( $this->keywordsManager->getDisabledVariables() );
$coreVariables = array_merge( $activeVariables, $deprecatedVariables, $disabledVariables );
$coreVariables = array_map( 'strtolower', $coreVariables );
}
$exported = [];
foreach ( array_keys( $holder->getVars() ) as $varName ) {
$computeThis = ( is_array( $compute ) && in_array( $varName, $compute ) ) || $compute === true;
if (
( $includeUserVars || in_array( strtolower( $varName ), $coreVariables ) ) &&
// Only include variables set in the extension in case $includeUserVars is false
( $computeThis || $holder->getVarThrow( $varName ) instanceof AFPData )
) {
$exported[$varName] = $this->getVar( $holder, $varName )->toNative();
}
}
return $exported;
}
/**
* Compute all vars which need DB access. Useful for vars which are going to be saved
* cross-wiki or used for offline analysis.
*
* @param VariableHolder $holder
*/
public function computeDBVars( VariableHolder $holder ): void {
static $dbTypes = [
'links-from-database',
'links-from-update',
'links-from-wikitext-or-database',
'load-recent-authors',
'page-age',
'get-page-restrictions',
'user-editcount',
'user-emailconfirm',
'user-groups',
'user-rights',
'user-age',
'user-block',
'revision-text-by-id',
'content-model-by-id',
];
/** @var LazyLoadedVariable[] $missingVars */
$missingVars = array_filter( $holder->getVars(), static function ( $el ) {
return ( $el instanceof LazyLoadedVariable );
} );
foreach ( $missingVars as $name => $var ) {
if ( in_array( $var->getMethod(), $dbTypes ) ) {
$holder->setVar( $name, $this->getVar( $holder, $name ) );
}
}
}
/**
* Export all variables stored in this object with their native (PHP) types.
*
* @param VariableHolder $holder
* @return array
*/
public function exportAllVars( VariableHolder $holder ): array {
$exported = [];
foreach ( array_keys( $holder->getVars() ) as $varName ) {
$exported[ $varName ] = $this->getVar( $holder, $varName )->toNative();
}
return $exported;
}
/**
* Export all non-lazy variables stored in this object as string
*
* @param VariableHolder $holder
* @return string[]
*/
public function exportNonLazyVars( VariableHolder $holder ): array {
$exported = [];
foreach ( $holder->getVars() as $varName => $data ) {
if ( !( $data instanceof LazyLoadedVariable ) ) {
$exported[$varName] = $holder->getComputedVariable( $varName )->toString();
}
}
return $exported;
}
}
VariablesBlobStore.php 0000666 00000004513 15133501624 0011010 0 ustar 00 <?php
namespace MediaWiki\Extension\AbuseFilter\Variables;
use FormatJson;
use MediaWiki\Storage\BlobAccessException;
use MediaWiki\Storage\BlobStore;
use MediaWiki\Storage\BlobStoreFactory;
/**
* This service is used to store and load var dumps to a BlobStore
*/
class VariablesBlobStore {
public const SERVICE_NAME = 'AbuseFilterVariablesBlobStore';
/** @var VariablesManager */
private $varManager;
/** @var BlobStoreFactory */
private $blobStoreFactory;
/** @var BlobStore */
private $blobStore;
/** @var string|null */
private $centralDB;
/**
* @param VariablesManager $varManager
* @param BlobStoreFactory $blobStoreFactory
* @param BlobStore $blobStore
* @param string|null $centralDB
*/
public function __construct(
VariablesManager $varManager,
BlobStoreFactory $blobStoreFactory,
BlobStore $blobStore,
?string $centralDB
) {
$this->varManager = $varManager;
$this->blobStoreFactory = $blobStoreFactory;
$this->blobStore = $blobStore;
$this->centralDB = $centralDB;
}
/**
* Store a var dump to a BlobStore.
*
* @param VariableHolder $varsHolder
* @param bool $global
*
* @return string Address of the record
*/
public function storeVarDump( VariableHolder $varsHolder, $global = false ) {
// Get all variables yet set and compute old and new wikitext if not yet done
// as those are needed for the diff view on top of the abuse log pages
$vars = $this->varManager->dumpAllVars( $varsHolder, [ 'old_wikitext', 'new_wikitext' ] );
// Vars is an array with native PHP data types (non-objects) now
$text = FormatJson::encode( $vars );
$dbDomain = $global ? $this->centralDB : false;
$blobStore = $this->blobStoreFactory->newBlobStore( $dbDomain );
$hints = [
BlobStore::DESIGNATION_HINT => 'AbuseFilter',
BlobStore::MODEL_HINT => 'AbuseFilter',
];
return $blobStore->storeBlob( $text, $hints );
}
/**
* Retrieve a var dump from a BlobStore.
*
* @param string $address
*
* @return VariableHolder
*/
public function loadVarDump( string $address ): VariableHolder {
try {
$blob = $this->blobStore->getBlob( $address );
} catch ( BlobAccessException $ex ) {
return new VariableHolder;
}
$vars = FormatJson::decode( $blob, true );
$obj = VariableHolder::newFromArray( $vars );
$this->varManager->translateDeprecatedVars( $obj );
return $obj;
}
}
VariableHolder.php 0000666 00000005557 15133501624 0010160 0 ustar 00 <?php
namespace MediaWiki\Extension\AbuseFilter\Variables;
use MediaWiki\Extension\AbuseFilter\Parser\AFPData;
/**
* Mutable value object that holds a list of variables
*/
class VariableHolder {
/**
* @var (AFPData|LazyLoadedVariable)[]
*/
private $mVars = [];
/**
* Utility function to translate an array with shape [ varname => value ] into a self instance
*
* @param array $vars
* @return VariableHolder
*/
public static function newFromArray( array $vars ): VariableHolder {
$ret = new self();
foreach ( $vars as $var => $value ) {
$ret->setVar( $var, $value );
}
return $ret;
}
/**
* @param string $variable
* @param mixed $datum
*/
public function setVar( string $variable, $datum ): void {
$variable = strtolower( $variable );
if ( !( $datum instanceof AFPData || $datum instanceof LazyLoadedVariable ) ) {
$datum = AFPData::newFromPHPVar( $datum );
}
$this->mVars[$variable] = $datum;
}
/**
* Get all variables stored in this object
*
* @return (AFPData|LazyLoadedVariable)[]
*/
public function getVars(): array {
return $this->mVars;
}
/**
* @param string $variable
* @param string $method
* @param array $parameters
*/
public function setLazyLoadVar( string $variable, string $method, array $parameters ): void {
$placeholder = new LazyLoadedVariable( $method, $parameters );
$this->setVar( $variable, $placeholder );
}
/**
* Get a variable from the current object, or throw if not set
*
* @param string $varName The variable name
* @return AFPData|LazyLoadedVariable
*/
public function getVarThrow( string $varName ) {
$varName = strtolower( $varName );
if ( !$this->varIsSet( $varName ) ) {
throw new UnsetVariableException( $varName );
}
return $this->mVars[$varName];
}
/**
* A stronger version of self::getVarThrow that also asserts that the variable was computed
* @param string $varName
* @return AFPData
* @codeCoverageIgnore
*/
public function getComputedVariable( string $varName ): AFPData {
return $this->getVarThrow( $varName );
}
/**
* Merge any number of holders given as arguments into this holder.
*
* @param VariableHolder ...$holders
*/
public function addHolders( VariableHolder ...$holders ): void {
foreach ( $holders as $addHolder ) {
$this->mVars = array_merge( $this->mVars, $addHolder->mVars );
}
}
/**
* @param string $var
* @return bool
*/
public function varIsSet( string $var ): bool {
return array_key_exists( $var, $this->mVars );
}
/**
* @param string $varName
*/
public function removeVar( string $varName ): void {
unset( $this->mVars[$varName] );
}
}
// @deprecated Since 1.36. Kept for BC with the UpdateVarDumps script, see T331861. The alias can be removed
// once we no longer support updating from a MW version where that script may run.
class_alias( VariableHolder::class, 'AbuseFilterVariableHolder' );
LazyVariableComputer.php 0000666 00000042022 15133501624 0011365 0 ustar 00 <?php
namespace MediaWiki\Extension\AbuseFilter\Variables;
use ContentHandler;
use Language;
use MediaWiki\Extension\AbuseFilter\Hooks\AbuseFilterHookRunner;
use MediaWiki\Extension\AbuseFilter\Parser\AFPData;
use MediaWiki\Extension\AbuseFilter\TextExtractor;
use MediaWiki\ExternalLinks\ExternalLinksLookup;
use MediaWiki\ExternalLinks\LinkFilter;
use MediaWiki\Permissions\PermissionManager;
use MediaWiki\Permissions\RestrictionStore;
use MediaWiki\Revision\RevisionLookup;
use MediaWiki\Revision\RevisionRecord;
use MediaWiki\Revision\RevisionStore;
use MediaWiki\Revision\SlotRecord;
use MediaWiki\Storage\PreparedUpdate;
use MediaWiki\Title\Title;
use MediaWiki\User\UserEditTracker;
use MediaWiki\User\UserGroupManager;
use MediaWiki\User\UserIdentity;
use ParserFactory;
use ParserOptions;
use Psr\Log\LoggerInterface;
use stdClass;
use StringUtils;
use TextContent;
use UnexpectedValueException;
use User;
use WANObjectCache;
use Wikimedia\Diff\Diff;
use Wikimedia\Diff\UnifiedDiffFormatter;
use Wikimedia\Rdbms\Database;
use Wikimedia\Rdbms\LBFactory;
use WikiPage;
/**
* Service used to compute lazy-loaded variable.
* @internal
*/
class LazyVariableComputer {
public const SERVICE_NAME = 'AbuseFilterLazyVariableComputer';
/**
* @var float The amount of time to subtract from profiling
* @todo This is a hack
*/
public static $profilingExtraTime = 0;
/** @var TextExtractor */
private $textExtractor;
/** @var AbuseFilterHookRunner */
private $hookRunner;
/** @var LoggerInterface */
private $logger;
/** @var LBFactory */
private $lbFactory;
/** @var WANObjectCache */
private $wanCache;
/** @var RevisionLookup */
private $revisionLookup;
/** @var RevisionStore */
private $revisionStore;
/** @var Language */
private $contentLanguage;
/** @var ParserFactory */
private $parserFactory;
/** @var UserEditTracker */
private $userEditTracker;
/** @var UserGroupManager */
private $userGroupManager;
/** @var PermissionManager */
private $permissionManager;
/** @var RestrictionStore */
private $restrictionStore;
/** @var string */
private $wikiID;
/**
* @param TextExtractor $textExtractor
* @param AbuseFilterHookRunner $hookRunner
* @param LoggerInterface $logger
* @param LBFactory $lbFactory
* @param WANObjectCache $wanCache
* @param RevisionLookup $revisionLookup
* @param RevisionStore $revisionStore
* @param Language $contentLanguage
* @param ParserFactory $parserFactory
* @param UserEditTracker $userEditTracker
* @param UserGroupManager $userGroupManager
* @param PermissionManager $permissionManager
* @param RestrictionStore $restrictionStore
* @param string $wikiID
*/
public function __construct(
TextExtractor $textExtractor,
AbuseFilterHookRunner $hookRunner,
LoggerInterface $logger,
LBFactory $lbFactory,
WANObjectCache $wanCache,
RevisionLookup $revisionLookup,
RevisionStore $revisionStore,
Language $contentLanguage,
ParserFactory $parserFactory,
UserEditTracker $userEditTracker,
UserGroupManager $userGroupManager,
PermissionManager $permissionManager,
RestrictionStore $restrictionStore,
string $wikiID
) {
$this->textExtractor = $textExtractor;
$this->hookRunner = $hookRunner;
$this->logger = $logger;
$this->lbFactory = $lbFactory;
$this->wanCache = $wanCache;
$this->revisionLookup = $revisionLookup;
$this->revisionStore = $revisionStore;
$this->contentLanguage = $contentLanguage;
$this->parserFactory = $parserFactory;
$this->userEditTracker = $userEditTracker;
$this->userGroupManager = $userGroupManager;
$this->permissionManager = $permissionManager;
$this->restrictionStore = $restrictionStore;
$this->wikiID = $wikiID;
}
/**
* XXX: $getVarCB is a hack to hide the cyclic dependency with VariablesManager. See T261069 for possible
* solutions. This might also be merged into VariablesManager, but it would bring a ton of dependencies.
* @todo Should we remove $vars parameter (check hooks)?
*
* @param LazyLoadedVariable $var
* @param VariableHolder $vars
* @param callable $getVarCB
* @phan-param callable(string $name):AFPData $getVarCB
* @return AFPData
*/
public function compute( LazyLoadedVariable $var, VariableHolder $vars, callable $getVarCB ) {
$parameters = $var->getParameters();
$varMethod = $var->getMethod();
$result = null;
if ( !$this->hookRunner->onAbuseFilter_interceptVariable(
$varMethod,
$vars,
$parameters,
$result
) ) {
return $result instanceof AFPData
? $result : AFPData::newFromPHPVar( $result );
}
switch ( $varMethod ) {
case 'diff':
$text1Var = $parameters['oldtext-var'];
$text2Var = $parameters['newtext-var'];
$text1 = $getVarCB( $text1Var )->toString();
$text2 = $getVarCB( $text2Var )->toString();
// T74329: if there's no text, don't return an array with the empty string
$text1 = $text1 === '' ? [] : explode( "\n", $text1 );
$text2 = $text2 === '' ? [] : explode( "\n", $text2 );
$diffs = new Diff( $text1, $text2 );
$format = new UnifiedDiffFormatter();
$result = $format->format( $diffs );
break;
case 'diff-split':
$diff = $getVarCB( $parameters['diff-var'] )->toString();
$line_prefix = $parameters['line-prefix'];
$diff_lines = explode( "\n", $diff );
$result = [];
foreach ( $diff_lines as $line ) {
if ( ( $line[0] ?? '' ) === $line_prefix ) {
$result[] = substr( $line, 1 );
}
}
break;
case 'links-from-wikitext':
// This should ONLY be used when sharing a parse operation with the edit.
/** @var WikiPage $article */
$article = $parameters['article'];
if ( $article->getContentModel() === CONTENT_MODEL_WIKITEXT ) {
// Shared with the edit, don't count it in profiling
$startTime = microtime( true );
$textVar = $parameters['text-var'];
$new_text = $getVarCB( $textVar )->toString();
$content = ContentHandler::makeContent( $new_text, $article->getTitle() );
$editInfo = $article->prepareContentForEdit(
$content,
null,
$parameters['contextUserIdentity']
);
$result = LinkFilter::getIndexedUrlsNonReversed(
array_keys( $editInfo->output->getExternalLinks() )
);
self::$profilingExtraTime += ( microtime( true ) - $startTime );
break;
}
// Otherwise fall back to database
case 'links-from-wikitext-or-database':
// TODO: use Content object instead, if available!
/** @var WikiPage $article */
$article ??= $parameters['article'];
// this inference is ugly, but the name isn't accessible from here
// and we only want this for debugging
$textVar = $parameters['text-var'];
$varName = strpos( $textVar, 'old_' ) === 0 ? 'old_links' : 'all_links';
if ( $parameters['forFilter'] ?? false ) {
$this->logger->debug( "Loading $varName from DB" );
$links = $this->getLinksFromDB( $article );
} elseif ( $article->getContentModel() === CONTENT_MODEL_WIKITEXT ) {
$this->logger->debug( "Loading $varName from Parser" );
$wikitext = $getVarCB( $textVar )->toString();
$editInfo = $this->parseNonEditWikitext(
$wikitext,
$article,
$parameters['contextUserIdentity']
);
$links = LinkFilter::getIndexedUrlsNonReversed(
array_keys( $editInfo->output->getExternalLinks() )
);
} else {
// TODO: Get links from Content object. But we don't have the content object.
// And for non-text content, $wikitext is usually not going to be a valid
// serialization, but rather some dummy text for filtering.
$links = [];
}
$result = $links;
break;
case 'links-from-update':
/** @var PreparedUpdate $update */
$update = $parameters['update'];
// Shared with the edit, don't count it in profiling
$startTime = microtime( true );
$result = LinkFilter::getIndexedUrlsNonReversed(
array_keys( $update->getParserOutputForMetaData()->getExternalLinks() )
);
self::$profilingExtraTime += ( microtime( true ) - $startTime );
break;
case 'links-from-database':
/** @var WikiPage $article */
$article = $parameters['article'];
$this->logger->debug( 'Loading old_links from DB' );
$result = $this->getLinksFromDB( $article );
break;
case 'link-diff-added':
case 'link-diff-removed':
$oldLinkVar = $parameters['oldlink-var'];
$newLinkVar = $parameters['newlink-var'];
$oldLinks = $getVarCB( $oldLinkVar )->toNative();
$newLinks = $getVarCB( $newLinkVar )->toNative();
if ( $varMethod === 'link-diff-added' ) {
$result = array_diff( $newLinks, $oldLinks );
}
if ( $varMethod === 'link-diff-removed' ) {
$result = array_diff( $oldLinks, $newLinks );
}
break;
case 'parse-wikitext':
// Should ONLY be used when sharing a parse operation with the edit.
// TODO: use Content object instead, if available!
/* @var WikiPage $article */
$article = $parameters['article'];
if ( $article->getContentModel() === CONTENT_MODEL_WIKITEXT ) {
// Shared with the edit, don't count it in profiling
$startTime = microtime( true );
$textVar = $parameters['wikitext-var'];
$new_text = $getVarCB( $textVar )->toString();
$content = ContentHandler::makeContent( $new_text, $article->getTitle() );
$editInfo = $article->prepareContentForEdit(
$content,
null,
$parameters['contextUserIdentity']
);
if ( isset( $parameters['pst'] ) && $parameters['pst'] ) {
$result = $editInfo->pstContent->serialize( $editInfo->format );
} else {
// Note: as of core change r727361, the PP limit comments (which we don't want to be here)
// are already excluded.
$result = $editInfo->getOutput()->getText();
}
self::$profilingExtraTime += ( microtime( true ) - $startTime );
} else {
$result = '';
}
break;
case 'html-from-update':
/** @var PreparedUpdate $update */
$update = $parameters['update'];
// Shared with the edit, don't count it in profiling
$startTime = microtime( true );
$result = $update->getCanonicalParserOutput()->getText();
self::$profilingExtraTime += ( microtime( true ) - $startTime );
break;
case 'strip-html':
$htmlVar = $parameters['html-var'];
$html = $getVarCB( $htmlVar )->toString();
$stripped = StringUtils::delimiterReplace( '<', '>', '', $html );
// We strip extra spaces to the right because the stripping above
// could leave a lot of whitespace.
// @fixme Find a better way to do this.
$result = TextContent::normalizeLineEndings( $stripped );
break;
case 'load-recent-authors':
$result = $this->getLastPageAuthors( $parameters['title'] );
break;
case 'load-first-author':
$revision = $this->revisionLookup->getFirstRevision( $parameters['title'] );
if ( $revision ) {
// TODO T233241
$user = $revision->getUser();
$result = $user === null ? '' : $user->getName();
} else {
$result = '';
}
break;
case 'get-page-restrictions':
$action = $parameters['action'];
/** @var Title $title */
$title = $parameters['title'];
$result = $this->restrictionStore->getRestrictions( $title, $action );
break;
case 'user-editcount':
/** @var UserIdentity $userIdentity */
$userIdentity = $parameters['user-identity'];
$result = $this->userEditTracker->getUserEditCount( $userIdentity );
break;
case 'user-emailconfirm':
/** @var User $user */
$user = $parameters['user'];
$result = $user->getEmailAuthenticationTimestamp();
break;
case 'user-groups':
/** @var UserIdentity $userIdentity */
$userIdentity = $parameters['user-identity'];
$result = $this->userGroupManager->getUserEffectiveGroups( $userIdentity );
break;
case 'user-rights':
/** @var UserIdentity $userIdentity */
$userIdentity = $parameters['user-identity'];
$result = $this->permissionManager->getUserPermissions( $userIdentity );
break;
case 'user-block':
// @todo Support partial blocks?
/** @var User $user */
$user = $parameters['user'];
$result = (bool)$user->getBlock();
break;
case 'user-age':
/** @var User $user */
$user = $parameters['user'];
$asOf = $parameters['asof'];
if ( !$user->isRegistered() ) {
$result = 0;
} else {
// HACK: If there's no registration date, assume 2008-01-15, Wikipedia Day
// in the year before the new user log was created. See T243469.
$registration = $user->getRegistration() ?? "20080115000000";
$result = (int)wfTimestamp( TS_UNIX, $asOf ) - (int)wfTimestamp( TS_UNIX, $registration );
}
break;
case 'page-age':
/** @var Title $title */
$title = $parameters['title'];
$firstRev = $this->revisionLookup->getFirstRevision( $title );
$firstRevisionTime = $firstRev ? $firstRev->getTimestamp() : null;
if ( !$firstRevisionTime ) {
$result = 0;
break;
}
$asOf = $parameters['asof'];
$result = (int)wfTimestamp( TS_UNIX, $asOf ) - (int)wfTimestamp( TS_UNIX, $firstRevisionTime );
break;
case 'length':
$s = $getVarCB( $parameters['length-var'] )->toString();
$result = strlen( $s );
break;
case 'subtract-int':
$v1 = $getVarCB( $parameters['val1-var'] )->toInt();
$v2 = $getVarCB( $parameters['val2-var'] )->toInt();
$result = $v1 - $v2;
break;
case 'content-model-by-id':
$revRec = $this->revisionLookup->getRevisionById( $parameters['revid'] );
$result = $this->getContentModelFromRevision( $revRec );
break;
case 'revision-text-by-id':
$revRec = $this->revisionLookup->getRevisionById( $parameters['revid'] );
$result = $this->textExtractor->revisionToString( $revRec, $parameters['contextUser'] );
break;
case 'get-wiki-name':
$result = $this->wikiID;
break;
case 'get-wiki-language':
$result = $this->contentLanguage->getCode();
break;
default:
if ( $this->hookRunner->onAbuseFilter_computeVariable(
$varMethod,
$vars,
$parameters,
$result
) ) {
throw new UnexpectedValueException( 'Unknown variable compute type ' . $varMethod );
}
}
return $result instanceof AFPData ? $result : AFPData::newFromPHPVar( $result );
}
/**
* @param WikiPage $article
* @return array
*/
private function getLinksFromDB( WikiPage $article ) {
$id = $article->getId();
if ( !$id ) {
return [];
}
return ExternalLinksLookup::getExternalLinksForPage(
$id,
$this->lbFactory->getReplicaDatabase(),
__METHOD__
);
}
/**
* @todo Move to MW core (T272050)
* @param Title $title
* @return string[] Usernames of the last 10 (unique) authors from $title
*/
private function getLastPageAuthors( Title $title ) {
if ( !$title->exists() ) {
return [];
}
$fname = __METHOD__;
return $this->wanCache->getWithSetCallback(
$this->wanCache->makeKey( 'last-10-authors', 'revision', $title->getLatestRevID() ),
WANObjectCache::TTL_MINUTE,
function ( $oldValue, &$ttl, array &$setOpts ) use ( $title, $fname ) {
$dbr = $this->lbFactory->getReplicaDatabase();
$setOpts += Database::getCacheSetOptions( $dbr );
// Get the last 100 edit authors with a trivial query (avoid T116557)
$revQuery = $this->revisionStore->getQueryInfo();
$revAuthors = $dbr->selectFieldValues(
$revQuery['tables'],
$revQuery['fields']['rev_user_text'],
[
'rev_page' => $title->getArticleID(),
// TODO Should deleted names be counted in the 10 authors? If yes, this check should
// be moved inside the foreach
'rev_deleted' => 0
],
$fname,
// Some pages have < 10 authors but many revisions (e.g. bot pages)
[ 'ORDER BY' => 'rev_timestamp DESC, rev_id DESC',
'LIMIT' => 100,
// Force index per T116557
'USE INDEX' => [ 'revision' => 'rev_page_timestamp' ],
],
$revQuery['joins']
);
// Get the last 10 distinct authors within this set of edits
$users = [];
foreach ( $revAuthors as $author ) {
$users[$author] = 1;
if ( count( $users ) >= 10 ) {
break;
}
}
return array_keys( $users );
}
);
}
/**
* @param ?RevisionRecord $revision
* @return string
*/
private function getContentModelFromRevision( ?RevisionRecord $revision ): string {
// this is consistent with what is done on various places in RunVariableGenerator
// and RCVariableGenerator
if ( $revision !== null ) {
$content = $revision->getContent( SlotRecord::MAIN, RevisionRecord::RAW );
return $content->getModel();
}
return '';
}
/**
* It's like WikiPage::prepareContentForEdit, but not for editing (old wikitext usually)
*
* @param string $wikitext
* @param WikiPage $article
* @param UserIdentity $userIdentity Context user
*
* @return stdClass
*/
private function parseNonEditWikitext( $wikitext, WikiPage $article, UserIdentity $userIdentity ) {
static $cache = [];
$cacheKey = md5( $wikitext ) . ':' . $article->getTitle()->getPrefixedText();
if ( !isset( $cache[$cacheKey] ) ) {
$options = ParserOptions::newFromUser( $userIdentity );
$cache[$cacheKey] = (object)[
'output' => $this->parserFactory->getInstance()->parse( $wikitext, $article->getTitle(), $options )
];
}
return $cache[$cacheKey];
}
}
Back to Directory
File Manager