Viewing File: /home/omtekel/www/wp-content/upgrade/backup/Handlers.tar

FilteredActionsHandler.php000066600000025622151334765630011662 0ustar00<?php

namespace MediaWiki\Extension\AbuseFilter\Hooks\Handlers;

use ApiMessage;
use Content;
use DeferredUpdates;
use IBufferingStatsdDataFactory;
use IContextSource;
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\Storage\Hook\ParserOutputStashForEditHook;
use MediaWiki\Title\Title;
use Status;
use UploadBase;
use User;
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 ]
			);
		}

		$filterResult = $this->filterEdit( $context, $user, $content, $summary, $slot );

		if ( !$filterResult->isOK() ) {
			// Produce a useful error message for API edits
			$filterResultApi = self::getApiStatus( $filterResult );
			$status->merge( $filterResultApi );
		}
		$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
	 * @return Status
	 */
	private function filterEdit(
		IContextSource $context,
		User $user,
		Content $content,
		string $summary,
		string $slot = SlotRecord::MAIN
	): Status {
		$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 Status::newGood();
		}
		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 Status::newGood();
		}

		$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 Status::newGood();
		}
		$runner = $this->filterRunnerFactory->newRunner( $user, $title, $vars, 'default' );
		$filterResult = $runner->run();
		if ( !$filterResult->isOK() ) {
			return $filterResult;
		}

		$this->editRevUpdater->setLastEditPage( $page );

		if ( $this->permissionManager->userHasRight( $user, 'abusefilter-bypass-blocked-external-domains' ) ) {
			return Status::newGood();
		}
		$blockedDomainFilterResult = $this->blockedDomainFilter->filter( $vars, $user, $title );
		if ( $blockedDomainFilterResult instanceof Status ) {
			return $blockedDomainFilterResult;
		}

		return Status::newGood();
	}

	/**
	 * @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->getErrors() as $error ) {
			[ $filterDescription, $filter ] = $error['params'];
			$actionsTaken = $allActionsTaken[ $filter ];

			$code = ( $actionsTaken === [ 'warn' ] ) ? 'abusefilter-warning' : 'abusefilter-disallowed';
			$data = [
				'abusefilter' => [
					'id' => $filter,
					'description' => $filterDescription,
					'actions' => $actionsTaken,
				],
			];

			$message = ApiMessage::create( $error, $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->getErrors()[0]['message'];
		} else {
			if ( $this->permissionManager->userHasRight( $user, 'abusefilter-bypass-blocked-external-domains' ) ) {
				return true;
			}
			$blockedDomainFilterResult = $this->blockedDomainFilter->filter( $vars, $user, $title );
			if ( $blockedDomainFilterResult instanceof Status ) {
				$error = $blockedDomainFilterResult->getErrors()[0]['message'];
				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
		);
	}
}
RecentChangeSaveHandler.php000066600000001340151334765630011737 0ustar00<?php
// phpcs:disable MediaWiki.NamingConventions.LowerCamelFunctionsName.FunctionName

namespace MediaWiki\Extension\AbuseFilter\Hooks\Handlers;

use MediaWiki\Extension\AbuseFilter\ChangeTags\ChangeTagger;
use MediaWiki\Hook\RecentChange_saveHook;

class RecentChangeSaveHandler implements RecentChange_saveHook {
	/** @var ChangeTagger */
	private $changeTagger;

	/**
	 * @param ChangeTagger $changeTagger
	 */
	public function __construct( ChangeTagger $changeTagger ) {
		$this->changeTagger = $changeTagger;
	}

	/**
	 * @inheritDoc
	 */
	public function onRecentChange_save( $recentChange ) {
		$tags = $this->changeTagger->getTagsForRecentChange( $recentChange );
		if ( $tags ) {
			$recentChange->addTags( $tags );
		}
	}
}
PageSaveHandler.php000066600000001156151334765630010272 0ustar00<?php

namespace MediaWiki\Extension\AbuseFilter\Hooks\Handlers;

use MediaWiki\Extension\AbuseFilter\EditRevUpdater;
use MediaWiki\Storage\Hook\PageSaveCompleteHook;

class PageSaveHandler implements PageSaveCompleteHook {
	/** @var EditRevUpdater */
	private $revUpdater;

	/**
	 * @param EditRevUpdater $revUpdater
	 */
	public function __construct( EditRevUpdater $revUpdater ) {
		$this->revUpdater = $revUpdater;
	}

	/**
	 * @inheritDoc
	 */
	public function onPageSaveComplete( $wikiPage, $user, $summary, $flags, $revisionRecord, $editResult ) {
		$this->revUpdater->updateRev( $wikiPage, $revisionRecord );
	}
}
UserMergeHandler.php000066600000001762151334765630010500 0ustar00<?php

namespace MediaWiki\Extension\AbuseFilter\Hooks\Handlers;

use Config;
use MediaWiki\Extension\UserMerge\Hooks\AccountFieldsHook;

class UserMergeHandler implements AccountFieldsHook {

	private Config $config;

	public function __construct( Config $config ) {
		$this->config = $config;
	}

	/**
	 * Tables that Extension:UserMerge needs to update
	 *
	 * @param array[] &$updateFields
	 */
	public function onUserMergeAccountFields( array &$updateFields ) {
		$actorStage = $this->config->get( 'AbuseFilterActorTableSchemaMigrationStage' );
		$updateFields[] = [
			'abuse_filter',
			'af_user',
			'af_user_text',
			'batchKey' => 'af_id',
			'actorId' => 'af_actor',
			'actorStage' => $actorStage,
		];
		$updateFields[] = [
			'abuse_filter_log',
			'afl_user',
			'afl_user_text',
			'batchKey' => 'afl_id',
		];
		$updateFields[] = [
			'abuse_filter_history',
			'afh_user',
			'afh_user_text',
			'batchKey' => 'afh_id',
			'actorId' => 'afh_actor',
			'actorStage' => $actorStage,
		];
	}

}
CheckUserHandler.php000066600000004503151334765630010452 0ustar00<?php

namespace MediaWiki\Extension\AbuseFilter\Hooks\Handlers;

use MediaWiki\CheckUser\Hook\CheckUserInsertChangesRowHook;
use MediaWiki\CheckUser\Hook\CheckUserInsertLogEventRowHook;
use MediaWiki\CheckUser\Hook\CheckUserInsertPrivateEventRowHook;
use MediaWiki\Extension\AbuseFilter\FilterUser;
use MediaWiki\User\UserIdentity;
use MediaWiki\User\UserIdentityUtils;
use RecentChange;

class CheckUserHandler implements
	CheckUserInsertChangesRowHook,
	CheckUserInsertPrivateEventRowHook,
	CheckUserInsertLogEventRowHook
{

	/** @var FilterUser */
	private $filterUser;

	/** @var UserIdentityUtils */
	private $userIdentityUtils;

	/**
	 * @param FilterUser $filterUser
	 * @param UserIdentityUtils $userIdentityUtils
	 */
	public function __construct(
		FilterUser $filterUser,
		UserIdentityUtils $userIdentityUtils
	) {
		$this->filterUser = $filterUser;
		$this->userIdentityUtils = $userIdentityUtils;
	}

	/**
	 * Any edits by the filter user should always be marked as by the software
	 * using IP 127.0.0.1, no XFF and no UA.
	 *
	 * @inheritDoc
	 */
	public function onCheckUserInsertChangesRow(
		string &$ip, &$xff, array &$row, UserIdentity $user, ?RecentChange $rc
	) {
		if (
			$this->userIdentityUtils->isNamed( $user ) &&
			$this->filterUser->getUserIdentity()->getId() == $user->getId()
		) {
			$ip = '127.0.0.1';
			$xff = false;
			$row['cuc_agent'] = '';
		}
	}

	/**
	 * Any log actions by the filter user should always be marked as by the software
	 * using IP 127.0.0.1, no XFF and no UA.
	 *
	 * @inheritDoc
	 */
	public function onCheckUserInsertLogEventRow(
		string &$ip, &$xff, array &$row, UserIdentity $user, int $id, ?RecentChange $rc
	) {
		if (
			$this->userIdentityUtils->isNamed( $user ) &&
			$this->filterUser->getUserIdentity()->getId() == $user->getId()
		) {
			$ip = '127.0.0.1';
			$xff = false;
			$row['cule_agent'] = '';
		}
	}

	/**
	 * Any log actions by the filter user should always be marked as by the software
	 * using IP 127.0.0.1, no XFF and no UA.
	 *
	 * @inheritDoc
	 */
	public function onCheckUserInsertPrivateEventRow(
		string &$ip, &$xff, array &$row, UserIdentity $user, ?RecentChange $rc
	) {
		if (
			$this->userIdentityUtils->isNamed( $user ) &&
			$this->filterUser->getUserIdentity()->getId() == $user->getId()
		) {
			$ip = '127.0.0.1';
			$xff = false;
			$row['cupe_agent'] = '';
		}
	}
}
EditPermissionHandler.php000066600000006362151334765630011541 0ustar00<?php
// phpcs:disable MediaWiki.NamingConventions.LowerCamelFunctionsName.FunctionName

namespace MediaWiki\Extension\AbuseFilter\Hooks\Handlers;

use JsonContent;
use MediaWiki\Content\Hook\JsonValidateSaveHook;
use MediaWiki\Extension\AbuseFilter\BlockedDomainStorage;
use MediaWiki\MediaWikiServices;
use MediaWiki\Page\PageIdentity;
use MediaWiki\Permissions\Hook\GetUserPermissionsErrorsHook;
use MediaWiki\Title\Title;
use MessageSpecifier;
use StatusValue;
use TitleValue;
use User;

/**
 * This hook handler is for very simple checks, rather than the much more advanced ones
 * undertaken by the FilteredActionsHandler.
 */
class EditPermissionHandler implements GetUserPermissionsErrorsHook, JsonValidateSaveHook {

	/** @var string[] */
	private const JSON_OBJECT_FIELDS = [
		'domain',
		'notes'
	];

	/**
	 * @see https://www.mediawiki.org/wiki/Manual:Hooks/getUserPermissionsErrors
	 *
	 * @param Title $title
	 * @param User $user
	 * @param string $action
	 * @param array|string|MessageSpecifier &$result
	 * @return bool|void
	 */
	public function onGetUserPermissionsErrors( $title, $user, $action, &$result ) {
		$services = MediaWikiServices::getInstance();

		// Only do anything if we're enabled on this wiki.
		if ( !$services->getMainConfig()->get( 'AbuseFilterEnableBlockedExternalDomain' ) ) {
			return;
		}

		// Ignore all actions and pages except MediaWiki: edits (and creates)
		// to the page we care about
		if (
			!( $action == 'create' || $action == 'edit' ) ||
			!$title->inNamespace( NS_MEDIAWIKI ) ||
			$title->getDBkey() !== BlockedDomainStorage::TARGET_PAGE
		) {
			return;
		}

		if ( $services->getPermissionManager()->userHasRight( $user, 'editinterface' ) ) {
			return;
		}

		// Prohibit direct actions on our page.
		$result = [ 'abusefilter-blocked-domains-cannot-edit-directly', BlockedDomainStorage::TARGET_PAGE ];
		return false;
	}

	/**
	 * @param JsonContent $content
	 * @param PageIdentity $pageIdentity
	 * @param StatusValue $status
	 * @return bool|void
	 */
	public function onJsonValidateSave( JsonContent $content, PageIdentity $pageIdentity, StatusValue $status ) {
		$services = MediaWikiServices::getInstance();

		// Only do anything if we're enabled on this wiki.
		if ( !$services->getMainConfig()->get( 'AbuseFilterEnableBlockedExternalDomain' ) ) {
			return;
		}

		$title = TitleValue::newFromPage( $pageIdentity );
		if ( !$title->inNamespace( NS_MEDIAWIKI ) || $title->getText() !== BlockedDomainStorage::TARGET_PAGE ) {
			return;
		}
		$data = $content->getData()->getValue();

		if ( !is_array( $data ) ) {
			$status->fatal( 'abusefilter-blocked-domains-json-error' );
			return;
		}

		$isValid = true;
		$entryNumber = 0;
		foreach ( $data as $element ) {
			$entryNumber++;
			// Check if each element is an object with all known fields, and no other fields
			if ( is_object( $element ) && count( get_object_vars( $element ) ) === count( self::JSON_OBJECT_FIELDS ) ) {
				foreach ( self::JSON_OBJECT_FIELDS as $field ) {
					if ( !property_exists( $element, $field ) || !is_string( $element->{$field} ) ) {
						$isValid = false;
						break 2;
					}
				}
			} else {
				$isValid = false;
				break;
			}
		}

		if ( !$isValid ) {
			$status->fatal( 'abusefilter-blocked-domains-invalid-entry', $entryNumber );
		}
	}

}
RegistrationCallback.php000066600000010237151334765630011370 0ustar00<?php

namespace MediaWiki\Extension\AbuseFilter\Hooks\Handlers;

use InvalidArgumentException;

/**
 * This class runs a callback when the extension is registered, right after configuration has been
 * loaded (not really a hook, but almost).
 * @codeCoverageIgnore Mainly deprecation warnings and other things that can be tested by running the updater
 */
class RegistrationCallback {

	public static function onRegistration(): void {
		global $wgAbuseFilterProfile,
			$wgAbuseFilterProfiling, $wgAbuseFilterPrivateLog, $wgAbuseFilterForceSummary,
			$wgGroupPermissions, $wgAbuseFilterRestrictions, $wgAbuseFilterDisallowGlobalLocalBlocks,
			$wgAbuseFilterActionRestrictions, $wgAbuseFilterLocallyDisabledGlobalActions,
			$wgAbuseFilterActorTableSchemaMigrationStage;

		// @todo Remove this in a future release (added in 1.33)
		if ( isset( $wgAbuseFilterProfile ) || isset( $wgAbuseFilterProfiling ) ) {
			wfWarn( '$wgAbuseFilterProfile and $wgAbuseFilterProfiling have been removed and ' .
				'profiling is now enabled by default.' );
		}

		if ( isset( $wgAbuseFilterPrivateLog ) ) {
			global $wgAbuseFilterLogPrivateDetailsAccess;
			$wgAbuseFilterLogPrivateDetailsAccess = $wgAbuseFilterPrivateLog;
			wfWarn( '$wgAbuseFilterPrivateLog has been renamed to $wgAbuseFilterLogPrivateDetailsAccess. ' .
				'Please make the change in your settings; the format is identical.'
			);
		}
		if ( isset( $wgAbuseFilterForceSummary ) ) {
			global $wgAbuseFilterPrivateDetailsForceReason;
			$wgAbuseFilterPrivateDetailsForceReason = $wgAbuseFilterForceSummary;
			wfWarn( '$wgAbuseFilterForceSummary has been renamed to ' .
				'$wgAbuseFilterPrivateDetailsForceReason. Please make the change in your settings; ' .
				'the format is identical.'
			);
		}

		$found = false;
		foreach ( $wgGroupPermissions as &$perms ) {
			if ( array_key_exists( 'abusefilter-private', $perms ) ) {
				$perms['abusefilter-privatedetails'] = $perms[ 'abusefilter-private' ];
				unset( $perms[ 'abusefilter-private' ] );
				$found = true;
			}
			if ( array_key_exists( 'abusefilter-private-log', $perms ) ) {
				$perms['abusefilter-privatedetails-log'] = $perms[ 'abusefilter-private-log' ];
				unset( $perms[ 'abusefilter-private-log' ] );
				$found = true;
			}
		}
		unset( $perms );

		if ( $found ) {
			wfWarn( 'The group permissions "abusefilter-private-log" and "abusefilter-private" have ' .
				'been renamed, respectively, to "abusefilter-privatedetails-log" and ' .
				'"abusefilter-privatedetails". Please update the names in your settings.'
			);
		}

		// @todo Remove this in a future release (added in 1.36)
		if ( isset( $wgAbuseFilterDisallowGlobalLocalBlocks ) ) {
			wfWarn( '$wgAbuseFilterDisallowGlobalLocalBlocks has been removed and replaced by ' .
				'$wgAbuseFilterLocallyDisabledGlobalActions. You can now specify which actions to disable. ' .
				'If you had set the former to true, you should set to true all of the actions in ' .
				'$wgAbuseFilterRestrictions (if you were manually setting the variable) or ' .
				'ConsequencesRegistry::DANGEROUS_ACTIONS. ' .
				'If you had set it to false (or left the default), just remove it from your wiki settings.'
			);
			if ( $wgAbuseFilterDisallowGlobalLocalBlocks === true ) {
				$wgAbuseFilterLocallyDisabledGlobalActions = [
					'throttle' => false,
					'warn' => false,
					'disallow' => false,
					'blockautopromote' => true,
					'block' => true,
					'rangeblock' => true,
					'degroup' => true,
					'tag' => false
				];
			}
		}

		// @todo Remove this in a future release (added in 1.36)
		if ( isset( $wgAbuseFilterRestrictions ) ) {
			wfWarn( '$wgAbuseFilterRestrictions has been renamed to $wgAbuseFilterActionRestrictions.' );
			$wgAbuseFilterActionRestrictions = $wgAbuseFilterRestrictions;
		}

		// in order
		$allowedStages = [
			SCHEMA_COMPAT_OLD,
			SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_OLD,
			SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_NEW,
			SCHEMA_COMPAT_NEW,
		];
		if ( !in_array( $wgAbuseFilterActorTableSchemaMigrationStage, $allowedStages ) ) {
			throw new InvalidArgumentException(
				'$wgAbuseFilterActorTableSchemaMigrationStage must specify a supported ' .
				'combination of schema compatibility flags'
			);
		}
	}

}
ToolLinksHandler.php000066600000006240151334765630010514 0ustar00<?php

namespace MediaWiki\Extension\AbuseFilter\Hooks\Handlers;

use IContextSource;
use MediaWiki\Extension\AbuseFilter\AbuseFilterPermissionManager;
use MediaWiki\Extension\AbuseFilter\Special\SpecialAbuseLog;
use MediaWiki\Linker\LinkRenderer;
use MediaWiki\Linker\LinkTarget;
use MediaWiki\Title\Title;
use SpecialPage;
use TitleValue;
use Wikimedia\IPUtils;

class ToolLinksHandler implements
	\MediaWiki\Hook\ContributionsToolLinksHook,
	\MediaWiki\Hook\HistoryPageToolLinksHook,
	\MediaWiki\Hook\UndeletePageToolLinksHook
{

	/** @var AbuseFilterPermissionManager */
	private $afPermManager;

	/**
	 * ToolLinksHandler constructor.
	 * @param AbuseFilterPermissionManager $afPermManager
	 */
	public function __construct( AbuseFilterPermissionManager $afPermManager ) {
		$this->afPermManager = $afPermManager;
	}

	/**
	 * @param int $id
	 * @param Title $nt
	 * @param array &$tools
	 * @param SpecialPage $sp for context
	 */
	public function onContributionsToolLinks( $id, Title $nt, array &$tools, SpecialPage $sp ) {
		$username = $nt->getText();
		if ( $this->afPermManager->canViewAbuseLog( $sp->getAuthority() )
			&& !IPUtils::isValidRange( $username )
		) {
			$linkRenderer = $sp->getLinkRenderer();
			$tools['abuselog'] = $linkRenderer->makeLink(
				$this->getSpecialPageTitle(),
				$sp->msg( 'abusefilter-log-linkoncontribs' )->text(),
				[ 'title' => $sp->msg( 'abusefilter-log-linkoncontribs-text',
					$username )->text(), 'class' => 'mw-contributions-link-abuse-log' ],
				[ 'wpSearchUser' => $username ]
			);
		}
	}

	/**
	 * @param IContextSource $context
	 * @param LinkRenderer $linkRenderer
	 * @param string[] &$links
	 */
	public function onHistoryPageToolLinks( IContextSource $context, LinkRenderer $linkRenderer, array &$links ) {
		if ( $this->afPermManager->canViewAbuseLog( $context->getAuthority() ) ) {
			$links[] = $linkRenderer->makeLink(
				$this->getSpecialPageTitle(),
				$context->msg( 'abusefilter-log-linkonhistory' )->text(),
				[ 'title' => $context->msg( 'abusefilter-log-linkonhistory-text' )->text() ],
				[ 'wpSearchTitle' => $context->getTitle()->getPrefixedText() ]
			);
		}
	}

	/**
	 * @param IContextSource $context
	 * @param LinkRenderer $linkRenderer
	 * @param string[] &$links
	 */
	public function onUndeletePageToolLinks( IContextSource $context, LinkRenderer $linkRenderer, array &$links ) {
		$show = $this->afPermManager->canViewAbuseLog( $context->getAuthority() );
		$action = $context->getRequest()->getVal( 'action', 'view' );

		// For 'history action', the link would be added by HistoryPageToolLinks hook.
		if ( $show && $action !== 'history' ) {
			$links[] = $linkRenderer->makeLink(
				$this->getSpecialPageTitle(),
				$context->msg( 'abusefilter-log-linkonundelete' )->text(),
				[ 'title' => $context->msg( 'abusefilter-log-linkonundelete-text' )->text() ],
				[ 'wpSearchTitle' => $context->getTitle()->getPrefixedText() ]
			);
		}
	}

	/**
	 * @codeCoverageIgnore Helper for tests
	 * @return LinkTarget
	 */
	private function getSpecialPageTitle(): LinkTarget {
		return defined( 'MW_PHPUNIT_TEST' )
			? new TitleValue( NS_SPECIAL, SpecialAbuseLog::PAGE_NAME )
			: SpecialPage::getTitleFor( SpecialAbuseLog::PAGE_NAME );
	}
}
ChangeTagsHandler.php000066600000001744151334765630010606 0ustar00<?php

namespace MediaWiki\Extension\AbuseFilter\Hooks\Handlers;

use MediaWiki\Extension\AbuseFilter\ChangeTags\ChangeTagsManager;

class ChangeTagsHandler implements
	\MediaWiki\ChangeTags\Hook\ListDefinedTagsHook,
	\MediaWiki\ChangeTags\Hook\ChangeTagsListActiveHook
{

	/** @var ChangeTagsManager */
	private $changeTagsManager;

	/**
	 * @param ChangeTagsManager $changeTagsManager
	 */
	public function __construct( ChangeTagsManager $changeTagsManager ) {
		$this->changeTagsManager = $changeTagsManager;
	}

	/**
	 * @param string[] &$tags
	 */
	public function onListDefinedTags( &$tags ) {
		$tags = array_merge(
			$tags,
			$this->changeTagsManager->getTagsDefinedByFilters(),
			[ $this->changeTagsManager->getCondsLimitTag() ]
		);
	}

	/**
	 * @param string[] &$tags
	 */
	public function onChangeTagsListActive( &$tags ) {
		$tags = array_merge(
			$tags,
			$this->changeTagsManager->getTagsDefinedByActiveFilters(),
			[ $this->changeTagsManager->getCondsLimitTag() ]
		);
	}
}
SchemaChangesHandler.php000066600000017567151334765630011305 0ustar00<?php

namespace MediaWiki\Extension\AbuseFilter\Hooks\Handlers;

use DatabaseUpdater;
use MediaWiki\Extension\AbuseFilter\Maintenance\FixOldLogEntries;
use MediaWiki\Extension\AbuseFilter\Maintenance\MigrateActorsAF;
use MediaWiki\Extension\AbuseFilter\Maintenance\NormalizeThrottleParameters;
use MediaWiki\Extension\AbuseFilter\Maintenance\UpdateVarDumps;
use MediaWiki\Installer\Hook\LoadExtensionSchemaUpdatesHook;
use MediaWiki\MediaWikiServices;
use MediaWiki\User\UserGroupManager;
use MessageLocalizer;
use RequestContext;
use User;

class SchemaChangesHandler implements LoadExtensionSchemaUpdatesHook {
	/** @var MessageLocalizer */
	private $messageLocalizer;
	/** @var UserGroupManager */
	private $userGroupManager;

	/**
	 * @param MessageLocalizer $messageLocalizer
	 * @param UserGroupManager $userGroupManager
	 */
	public function __construct( MessageLocalizer $messageLocalizer, UserGroupManager $userGroupManager ) {
		$this->messageLocalizer = $messageLocalizer;
		$this->userGroupManager = $userGroupManager;
	}

	/**
	 * @note The hook doesn't allow injecting services!
	 * @codeCoverageIgnore
	 * @return self
	 */
	public static function newFromGlobalState(): self {
		return new self(
			// @todo Use a proper MessageLocalizer once available (T247127)
			RequestContext::getMain(),
			MediaWikiServices::getInstance()->getUserGroupManager()
		);
	}

	/**
	 * @codeCoverageIgnore This is tested by installing or updating MediaWiki
	 * @param DatabaseUpdater $updater
	 */
	public function onLoadExtensionSchemaUpdates( $updater ) {
		global $wgAbuseFilterActorTableSchemaMigrationStage;

		$dbType = $updater->getDB()->getType();
		$dir = __DIR__ . "/../../../db_patches";

		$updater->addExtensionTable(
			'abuse_filter',
			"$dir/$dbType/tables-generated.sql"
		);

		if ( $dbType === 'mysql' || $dbType === 'sqlite' ) {
			$updater->dropExtensionField(
				'abuse_filter_log',
				'afl_log_id',
				"$dir/$dbType/patch-drop_afl_log_id.sql"
			);

			$updater->addExtensionField(
				'abuse_filter_log',
				'afl_filter_id',
				"$dir/$dbType/patch-split-afl_filter.sql"
			);

			if ( $dbType === 'mysql' ) {
				$updater->renameExtensionIndex(
					'abuse_filter_log',
					'ip_timestamp',
					'afl_ip_timestamp',
					"$dir/mysql/patch-rename-indexes.sql",
					true
				);
				// This one has its own files because apparently, sometimes this particular index can already
				// have the correct name (T291725)
				$updater->renameExtensionIndex(
					'abuse_filter_log',
					'wiki_timestamp',
					'afl_wiki_timestamp',
					"$dir/mysql/patch-rename-wiki-timestamp-index.sql",
					true
				);
				// This one is also separate to avoid interferences with the afl_filter field removal below.
				$updater->renameExtensionIndex(
					'abuse_filter_log',
					'filter_timestamp',
					'afl_filter_timestamp',
					"$dir/mysql/patch-rename-filter_timestamp-index.sql",
					true
				);
			}
			$updater->dropExtensionField(
				'abuse_filter_log',
				'afl_filter',
				"$dir/$dbType/patch-remove-afl_filter.sql"
			);
		} elseif ( $dbType === 'postgres' ) {
			$updater->addExtensionUpdate( [
				'dropPgField', 'abuse_filter_log', 'afl_log_id' ] );
			$updater->addExtensionUpdate( [
				'setDefault', 'abuse_filter_log', 'afl_filter', ''
			] );
			$updater->addExtensionUpdate( [
				'addPgField', 'abuse_filter_log', 'afl_global', 'SMALLINT NOT NULL DEFAULT 0'
			] );
			$updater->addExtensionUpdate( [
				'addPgField', 'abuse_filter_log', 'afl_filter_id', 'INTEGER NOT NULL DEFAULT 0'
			] );
			$updater->addExtensionUpdate( [
				'addPgIndex', 'abuse_filter_log', 'abuse_filter_log_filter_timestamp_full',
				'(afl_global, afl_filter_id, afl_timestamp)'
			] );
			$updater->addExtensionUpdate( [
				'dropPgIndex', 'abuse_filter_log', 'abuse_filter_log_timestamp'
			] );
			$updater->addExtensionUpdate( [
				'dropPgField', 'abuse_filter_log', 'afl_filter'
			] );
			$updater->addExtensionUpdate( [
				'dropDefault', 'abuse_filter_log', 'afl_filter_id'
			] );
			$updater->addExtensionUpdate( [
				'dropDefault', 'abuse_filter_log', 'afl_global'
			] );
			$updater->addExtensionUpdate( [
				'renameIndex', 'abuse_filter', 'abuse_filter_user', 'af_user'
			] );
			$updater->addExtensionUpdate( [
				'renameIndex', 'abuse_filter', 'abuse_filter_group_enabled_id', 'af_group_enabled'
			] );
			$updater->addExtensionUpdate( [
				'renameIndex', 'abuse_filter_action', 'abuse_filter_action_consequence', 'afa_consequence'
			] );
			$updater->addExtensionUpdate( [
				'renameIndex', 'abuse_filter_log', 'abuse_filter_log_filter_timestamp_full', 'afl_filter_timestamp_full'
			] );
			$updater->addExtensionUpdate( [
				'renameIndex', 'abuse_filter_log', 'abuse_filter_log_user_timestamp', 'afl_user_timestamp'
			] );
			$updater->addExtensionUpdate( [
				'renameIndex', 'abuse_filter_log', 'abuse_filter_log_timestamp', 'afl_timestamp'
			] );
			$updater->addExtensionUpdate( [
				'renameIndex', 'abuse_filter_log', 'abuse_filter_log_page_timestamp', 'afl_page_timestamp'
			] );
			$updater->addExtensionUpdate( [
				'renameIndex', 'abuse_filter_log', 'abuse_filter_log_ip_timestamp', 'afl_ip_timestamp'
			] );
			$updater->addExtensionUpdate( [
				'renameIndex', 'abuse_filter_log', 'abuse_filter_log_rev_id', 'afl_rev_id'
			] );
			$updater->addExtensionUpdate( [
				'renameIndex', 'abuse_filter_log', 'abuse_filter_log_wiki_timestamp', 'afl_wiki_timestamp'
			] );
			$updater->addExtensionUpdate( [
				'renameIndex', 'abuse_filter_history', 'abuse_filter_history_filter', 'afh_filter'
			] );
			$updater->addExtensionUpdate( [
				'renameIndex', 'abuse_filter_history', 'abuse_filter_history_user', 'afh_user'
			] );
			$updater->addExtensionUpdate( [
				'renameIndex', 'abuse_filter_history', 'abuse_filter_history_user_text', 'afh_user_text'
			] );
			$updater->addExtensionUpdate( [
				'renameIndex', 'abuse_filter_history', 'abuse_filter_history_timestamp', 'afh_timestamp'
			] );
			$updater->addExtensionUpdate( [
				'changeNullableField', ' abuse_filter_history', 'afh_public_comments', 'NULL', true
			] );
			$updater->addExtensionUpdate( [
				'changeNullableField', ' abuse_filter_history', 'afh_actions', 'NULL', true
			] );
		}

		$updater->addExtensionUpdate( [
			'addField', 'abuse_filter', 'af_actor',
			"$dir/$dbType/patch-add-af_actor.sql", true
		] );

		$updater->addExtensionUpdate( [
			'addField', 'abuse_filter_history', 'afh_actor',
			"$dir/$dbType/patch-add-afh_actor.sql", true
		] );

		$updater->addExtensionUpdate( [ [ $this, 'createAbuseFilterUser' ] ] );
		$updater->addPostDatabaseUpdateMaintenance( NormalizeThrottleParameters::class );
		$updater->addPostDatabaseUpdateMaintenance( FixOldLogEntries::class );
		$updater->addPostDatabaseUpdateMaintenance( UpdateVarDumps::class );
		// Don't launch the script on update.php if the migration stage is not high enough.
		// This would throw an exception.
		// Also check if the global is set.
		// If globals aren't loaded, it's install.php, and not update.php. This is intentional,
		// see for instance T193855 or T198331.
		if ( isset( $wgAbuseFilterActorTableSchemaMigrationStage ) &&
			( $wgAbuseFilterActorTableSchemaMigrationStage & SCHEMA_COMPAT_WRITE_NEW )
		) {
			$updater->addPostDatabaseUpdateMaintenance( MigrateActorsAF::class );
		}
	}

	/**
	 * Updater callback to create the AbuseFilter user after the user tables have been updated.
	 * @param DatabaseUpdater $updater
	 * @return bool
	 */
	public function createAbuseFilterUser( DatabaseUpdater $updater ): bool {
		$username = $this->messageLocalizer->msg( 'abusefilter-blocker' )->inContentLanguage()->text();
		$user = User::newFromName( $username );

		if ( $user && !$updater->updateRowExists( 'create abusefilter-blocker-user' ) ) {
			$user = User::newSystemUser( $username, [ 'steal' => true ] );
			$updater->insertUpdateRow( 'create abusefilter-blocker-user' );
			// Promote user so it doesn't look too crazy.
			$this->userGroupManager->addUserToGroup( $user, 'sysop' );
			return true;
		}
		return false;
	}
}
AutoPromoteGroupsHandler.php000066600000004203151334765630012251 0ustar00<?php

namespace MediaWiki\Extension\AbuseFilter\Hooks\Handlers;

use BagOStuff;
use MediaWiki\Extension\AbuseFilter\BlockAutopromoteStore;
use MediaWiki\Extension\AbuseFilter\Consequences\ConsequencesRegistry;
use MediaWiki\User\Hook\GetAutoPromoteGroupsHook;
use MediaWiki\User\UserIdentity;
use ObjectCache;

class AutoPromoteGroupsHandler implements GetAutoPromoteGroupsHook {

	/** @var BagOStuff */
	private $cache;

	/** @var ConsequencesRegistry */
	private $consequencesRegistry;

	/** @var BlockAutopromoteStore */
	private $blockAutopromoteStore;

	/**
	 * @param BagOStuff $cache
	 * @param ConsequencesRegistry $consequencesRegistry
	 * @param BlockAutopromoteStore $blockAutopromoteStore
	 */
	public function __construct(
		BagOStuff $cache,
		ConsequencesRegistry $consequencesRegistry,
		BlockAutopromoteStore $blockAutopromoteStore
	) {
		$this->cache = $cache;
		$this->consequencesRegistry = $consequencesRegistry;
		$this->blockAutopromoteStore = $blockAutopromoteStore;
	}

	/**
	 * @param ConsequencesRegistry $consequencesRegistry
	 * @param BlockAutopromoteStore $blockAutopromoteStore
	 * @return self
	 * @todo Can we avoid this factory method?
	 */
	public static function factory(
		ConsequencesRegistry $consequencesRegistry,
		BlockAutopromoteStore $blockAutopromoteStore
	): self {
		return new self(
			ObjectCache::getInstance( 'hash' ),
			$consequencesRegistry,
			$blockAutopromoteStore
		);
	}

	/**
	 * @param UserIdentity $user
	 * @param string[] &$promote
	 */
	public function onGetAutoPromoteGroups( $user, &$promote ): void {
		if (
			in_array( 'blockautopromote', $this->consequencesRegistry->getAllEnabledActionNames() )
			&& $promote
		) {
			// Proxy the blockautopromote data to a faster backend, using an appropriate key
			$quickCacheKey = $this->cache->makeKey(
				'abusefilter',
				'blockautopromote',
				'quick',
				$user->getId()
			);
			$blocked = (bool)$this->cache->getWithSetCallback(
				$quickCacheKey,
				BagOStuff::TTL_PROC_LONG,
				function () use ( $user ) {
					return $this->blockAutopromoteStore->getAutoPromoteBlockStatus( $user );
				}
			);

			if ( $blocked ) {
				$promote = [];
			}
		}
	}
}
EchoHandler.php000066600000001646151334765630007461 0ustar00<?php

namespace MediaWiki\Extension\AbuseFilter\Hooks\Handlers;

use EchoAttributeManager;
use EchoUserLocator;
use MediaWiki\Extension\AbuseFilter\EchoNotifier;
use MediaWiki\Extension\AbuseFilter\ThrottleFilterPresentationModel;
use MediaWiki\Extension\Notifications\Hooks\BeforeCreateEchoEventHook;

class EchoHandler implements BeforeCreateEchoEventHook {

	/**
	 * @param array &$notifications
	 * @param array &$notificationCategories
	 * @param array &$icons
	 */
	public function onBeforeCreateEchoEvent(
		array &$notifications,
		array &$notificationCategories,
		array &$icons
	) {
		$notifications[ EchoNotifier::EVENT_TYPE ] = [
			'category' => 'system',
			'section' => 'alert',
			'group' => 'negative',
			'presentation-model' => ThrottleFilterPresentationModel::class,
			EchoAttributeManager::ATTR_LOCATORS => [
				[
					[ EchoUserLocator::class, 'locateFromEventExtra' ],
					[ 'user' ]
				]
			],
		];
	}

}
UserRenameHandler.php000066600000001606151334765630010645 0ustar00<?php

namespace MediaWiki\Extension\AbuseFilter\Hooks\Handlers;

use MediaWiki\RenameUser\Hook\RenameUserSQLHook;
use MediaWiki\RenameUser\RenameuserSQL;

class UserRenameHandler implements RenameUserSQLHook {

	/**
	 * @inheritDoc
	 */
	public function onRenameUserSQL( RenameuserSQL $renameUserSql ): void {
		global $wgAbuseFilterActorTableSchemaMigrationStage;
		if ( !( $wgAbuseFilterActorTableSchemaMigrationStage & SCHEMA_COMPAT_OLD ) ) {
			return;
		}
		$renameUserSql->tablesJob['abuse_filter'] = [
			RenameuserSQL::NAME_COL => 'af_user_text',
			RenameuserSQL::UID_COL => 'af_user',
			RenameuserSQL::TIME_COL => 'af_timestamp',
			'uniqueKey' => 'af_id'
		];
		$renameUserSql->tablesJob['abuse_filter_history'] = [
			RenameuserSQL::NAME_COL => 'afh_user_text',
			RenameuserSQL::UID_COL => 'afh_user',
			RenameuserSQL::TIME_COL => 'afh_timestamp',
			'uniqueKey' => 'afh_id'
		];
	}

}
Back to Directory File Manager