Page MenuHomeWickedGov Phorge

FilteredActionsHandler.php
No OneTemporary

Size
10 KB
Referenced Files
None
Subscribers
None

FilteredActionsHandler.php

<?php
namespace MediaWiki\Extension\AbuseFilter\Hooks\Handlers;
use MediaWiki\Api\ApiMessage;
use MediaWiki\Content\Content;
use MediaWiki\Context\IContextSource;
use MediaWiki\Deferred\DeferredUpdates;
use MediaWiki\Extension\AbuseFilter\BlockedDomainFilter;
use MediaWiki\Extension\AbuseFilter\EditRevUpdater;
use MediaWiki\Extension\AbuseFilter\FilterRunnerFactory;
use MediaWiki\Extension\AbuseFilter\VariableGenerator\VariableGeneratorFactory;
use MediaWiki\Hook\EditFilterMergedContentHook;
use MediaWiki\Hook\TitleMoveHook;
use MediaWiki\Hook\UploadStashFileHook;
use MediaWiki\Hook\UploadVerifyUploadHook;
use MediaWiki\Logger\LoggerFactory;
use MediaWiki\Page\Hook\ArticleDeleteHook;
use MediaWiki\Permissions\PermissionManager;
use MediaWiki\Revision\SlotRecord;
use MediaWiki\Status\Status;
use MediaWiki\Storage\Hook\ParserOutputStashForEditHook;
use MediaWiki\Title\Title;
use MediaWiki\User\User;
use UploadBase;
use Wikimedia\Stats\IBufferingStatsdDataFactory;
use WikiPage;
/**
* Handler for actions that can be filtered
*/
class FilteredActionsHandler implements
EditFilterMergedContentHook,
TitleMoveHook,
ArticleDeleteHook,
UploadVerifyUploadHook,
UploadStashFileHook,
ParserOutputStashForEditHook
{
/** @var IBufferingStatsdDataFactory */
private $statsDataFactory;
/** @var FilterRunnerFactory */
private $filterRunnerFactory;
/** @var VariableGeneratorFactory */
private $variableGeneratorFactory;
/** @var EditRevUpdater */
private $editRevUpdater;
private PermissionManager $permissionManager;
private BlockedDomainFilter $blockedDomainFilter;
/**
* @param IBufferingStatsdDataFactory $statsDataFactory
* @param FilterRunnerFactory $filterRunnerFactory
* @param VariableGeneratorFactory $variableGeneratorFactory
* @param EditRevUpdater $editRevUpdater
* @param BlockedDomainFilter $blockedDomainFilter
* @param PermissionManager $permissionManager
*/
public function __construct(
IBufferingStatsdDataFactory $statsDataFactory,
FilterRunnerFactory $filterRunnerFactory,
VariableGeneratorFactory $variableGeneratorFactory,
EditRevUpdater $editRevUpdater,
BlockedDomainFilter $blockedDomainFilter,
PermissionManager $permissionManager
) {
$this->statsDataFactory = $statsDataFactory;
$this->filterRunnerFactory = $filterRunnerFactory;
$this->variableGeneratorFactory = $variableGeneratorFactory;
$this->editRevUpdater = $editRevUpdater;
$this->blockedDomainFilter = $blockedDomainFilter;
$this->permissionManager = $permissionManager;
}
/**
* @inheritDoc
* @param string $slot Slot role for the content, added by Wikibase (T288885)
*/
public function onEditFilterMergedContent(
IContextSource $context,
Content $content,
Status $status,
$summary,
User $user,
$minoredit,
string $slot = SlotRecord::MAIN
) {
$startTime = microtime( true );
if ( !$status->isOK() ) {
// Investigate what happens if we skip filtering here (T211680)
LoggerFactory::getInstance( 'AbuseFilter' )->info(
'Status is already not OK',
[ 'status' => (string)$status ]
);
}
$this->filterEdit( $context, $user, $content, $summary, $slot, $status );
$this->statsDataFactory->timing( 'timing.editAbuseFilter', microtime( true ) - $startTime );
return $status->isOK();
}
/**
* Implementation for EditFilterMergedContent hook.
*
* @param IContextSource $context the context of the edit
* @param User $user
* @param Content $content the new Content generated by the edit
* @param string $summary Edit summary for page
* @param string $slot slot role for the content
* @param Status $status
*/
private function filterEdit(
IContextSource $context,
User $user,
Content $content,
string $summary,
string $slot,
Status $status
): void {
$this->editRevUpdater->clearLastEditPage();
$title = $context->getTitle();
$logger = LoggerFactory::getInstance( 'AbuseFilter' );
if ( $title === null ) {
// T144265: This *should* never happen.
$logger->warning( __METHOD__ . ' received a null title.' );
return;
}
if ( !$title->canExist() ) {
// This also should be handled in EditPage or whoever is calling the hook.
$logger->warning( __METHOD__ . ' received a Title that cannot exist.' );
// Note that if the title cannot exist, there's no much point in filtering the edit anyway
return;
}
$page = $context->getWikiPage();
$builder = $this->variableGeneratorFactory->newRunGenerator( $user, $title );
$vars = $builder->getEditVars( $content, $summary, $slot, $page );
if ( $vars === null ) {
// We don't have to filter the edit
return;
}
$runner = $this->filterRunnerFactory->newRunner( $user, $title, $vars, 'default' );
$filterResult = $runner->run();
if ( !$filterResult->isOK() ) {
// Produce a useful error message for API edits
$filterResultApi = self::getApiStatus( $filterResult );
$status->merge( $filterResultApi );
return;
}
$this->editRevUpdater->setLastEditPage( $page );
if ( $this->permissionManager->userHasRight( $user, 'abusefilter-bypass-blocked-external-domains' ) ) {
return;
}
$blockedDomainFilterResult = $this->blockedDomainFilter->filter( $vars, $user, $title );
if ( !$blockedDomainFilterResult->isOK() ) {
$status->merge( $blockedDomainFilterResult );
}
}
/**
* @param Status $status Error message details
* @return Status Status containing the same error messages with extra data for the API
*/
private static function getApiStatus( Status $status ): Status {
$allActionsTaken = $status->getValue();
$statusForApi = Status::newGood();
foreach ( $status->getMessages() as $msg ) {
[ $filterDescription, $filter ] = $msg->getParams();
$actionsTaken = $allActionsTaken[ $filter ];
$code = ( $actionsTaken === [ 'warn' ] ) ? 'abusefilter-warning' : 'abusefilter-disallowed';
$data = [
'abusefilter' => [
'id' => $filter,
'description' => $filterDescription,
'actions' => $actionsTaken,
],
];
$message = ApiMessage::create( $msg, $code, $data );
$statusForApi->fatal( $message );
}
return $statusForApi;
}
/**
* @inheritDoc
*/
public function onTitleMove( Title $old, Title $nt, User $user, $reason, Status &$status ) {
$builder = $this->variableGeneratorFactory->newRunGenerator( $user, $old );
$vars = $builder->getMoveVars( $nt, $reason );
$runner = $this->filterRunnerFactory->newRunner( $user, $old, $vars, 'default' );
$result = $runner->run();
$status->merge( $result );
}
/**
* @inheritDoc
*/
public function onArticleDelete( WikiPage $wikiPage, User $user, &$reason, &$error, Status &$status, $suppress ) {
if ( $suppress ) {
// Don't filter suppressions, T71617
return true;
}
$builder = $this->variableGeneratorFactory->newRunGenerator( $user, $wikiPage->getTitle() );
$vars = $builder->getDeleteVars( $reason );
$runner = $this->filterRunnerFactory->newRunner( $user, $wikiPage->getTitle(), $vars, 'default' );
$filterResult = $runner->run();
$status->merge( $filterResult );
$error = $filterResult->isOK() ? '' : $filterResult->getHTML();
return $filterResult->isOK();
}
/**
* @inheritDoc
*/
public function onUploadVerifyUpload(
UploadBase $upload,
User $user,
?array $props,
$comment,
$pageText,
&$error
) {
return $this->filterUpload( 'upload', $upload, $user, $props, $comment, $pageText, $error );
}
/**
* Filter an upload to stash. If a filter doesn't need to check the page contents or
* upload comment, it can use `action='stashupload'` to provide better experience to e.g.
* UploadWizard (rejecting files immediately, rather than after the user adds the details).
*
* @inheritDoc
*/
public function onUploadStashFile( UploadBase $upload, User $user, ?array $props, &$error ) {
return $this->filterUpload( 'stashupload', $upload, $user, $props, null, null, $error );
}
/**
* Implementation for UploadStashFile and UploadVerifyUpload hooks.
*
* @param string $action 'upload' or 'stashupload'
* @param UploadBase $upload
* @param User $user User performing the action
* @param array|null $props File properties, as returned by MWFileProps::getPropsFromPath().
* @param string|null $summary Upload log comment (also used as edit summary)
* @param string|null $text File description page text (only used for new uploads)
* @param array|ApiMessage &$error
* @return bool
*/
private function filterUpload(
string $action,
UploadBase $upload,
User $user,
?array $props,
?string $summary,
?string $text,
&$error
): bool {
$title = $upload->getTitle();
if ( $title === null ) {
// T144265: This could happen for 'stashupload' if the specified title is invalid.
// Let UploadBase warn the user about that, and we'll filter later.
$logger = LoggerFactory::getInstance( 'AbuseFilter' );
$logger->warning( __METHOD__ . " received a null title. Action: $action." );
return true;
}
$builder = $this->variableGeneratorFactory->newRunGenerator( $user, $title );
$vars = $builder->getUploadVars( $action, $upload, $summary, $text, $props );
if ( $vars === null ) {
return true;
}
$runner = $this->filterRunnerFactory->newRunner( $user, $title, $vars, 'default' );
$filterResult = $runner->run();
if ( !$filterResult->isOK() ) {
// Produce a useful error message for API edits
$filterResultApi = self::getApiStatus( $filterResult );
// @todo Return all errors instead of only the first one
$error = $filterResultApi->getMessages()[0];
} else {
if ( $this->permissionManager->userHasRight( $user, 'abusefilter-bypass-blocked-external-domains' ) ) {
return true;
}
$blockedDomainFilterResult = $this->blockedDomainFilter->filter( $vars, $user, $title );
if ( !$blockedDomainFilterResult->isOK() ) {
$error = $blockedDomainFilterResult->getMessages()[0];
return $blockedDomainFilterResult->isOK();
}
}
return $filterResult->isOK();
}
/**
* @inheritDoc
*/
public function onParserOutputStashForEdit( $page, $content, $output, $summary, $user ) {
// XXX: This makes the assumption that this method is only ever called for the main slot.
// Which right now holds true, but any more fancy MCR stuff will likely break here...
$slot = SlotRecord::MAIN;
// Cache any resulting filter matches.
// Do this outside the synchronous stash lock to avoid any chance of slowdown.
DeferredUpdates::addCallableUpdate(
function () use (
$user,
$page,
$summary,
$content,
$slot
) {
$startTime = microtime( true );
$generator = $this->variableGeneratorFactory->newRunGenerator( $user, $page->getTitle() );
$vars = $generator->getStashEditVars( $content, $summary, $slot, $page );
if ( !$vars ) {
return;
}
$runner = $this->filterRunnerFactory->newRunner( $user, $page->getTitle(), $vars, 'default' );
$runner->runForStash();
$totalTime = microtime( true ) - $startTime;
$this->statsDataFactory->timing( 'timing.stashAbuseFilter', $totalTime );
},
DeferredUpdates::PRESEND
);
}
}

File Metadata

Mime Type
text/x-php
Expires
Sat, Oct 3, 05:32 (2 w, 5 d ago)
Storage Engine
local-disk
Storage Format
Raw Data
Storage Handle
a3/0e/d4f986dd0f5cd844c0b198efdefc
Default Alt Text
FilteredActionsHandler.php (10 KB)

Event Timeline