<?php

namespace MediaWiki\Extension\AbuseFilter\View;

use LogicException;
use MediaWiki\Context\IContextSource;
use MediaWiki\Extension\AbuseFilter\AbuseFilterPermissionManager;
use MediaWiki\Extension\AbuseFilter\AbuseFilterPermissionStatus;
use MediaWiki\Extension\AbuseFilter\Consequences\ConsequencesRegistry;
use MediaWiki\Extension\AbuseFilter\EditBox\EditBoxBuilderFactory;
use MediaWiki\Extension\AbuseFilter\Filter\Filter;
use MediaWiki\Extension\AbuseFilter\Filter\FilterNotFoundException;
use MediaWiki\Extension\AbuseFilter\Filter\FilterVersionNotFoundException;
use MediaWiki\Extension\AbuseFilter\Filter\MutableFilter;
use MediaWiki\Extension\AbuseFilter\FilterImporter;
use MediaWiki\Extension\AbuseFilter\FilterLookup;
use MediaWiki\Extension\AbuseFilter\FilterProfiler;
use MediaWiki\Extension\AbuseFilter\FilterStore;
use MediaWiki\Extension\AbuseFilter\InvalidImportDataException;
use MediaWiki\Extension\AbuseFilter\SpecsFormatter;
use MediaWiki\Html\Html;
use MediaWiki\Linker\Linker;
use MediaWiki\Linker\LinkRenderer;
use MediaWiki\MainConfigNames;
use MediaWiki\SpecialPage\SpecialPage;
use OOUI;
use StatusValue;
use UnexpectedValueException;
use Wikimedia\HtmlArmor\HtmlArmor;
use Wikimedia\Rdbms\IDBAccessObject;
use Wikimedia\Rdbms\IExpression;
use Wikimedia\Rdbms\LBFactory;
use Wikimedia\Rdbms\LikeValue;
use Wikimedia\Rdbms\SelectQueryBuilder;

class AbuseFilterViewEdit extends AbuseFilterView {
	/**
	 * @var int|null The history ID of the current filter
	 */
	private $historyID;
	/** @var int|string */
	private $filter;

	public function __construct(
		private readonly LBFactory $lbFactory,
		AbuseFilterPermissionManager $afPermManager,
		private readonly FilterProfiler $filterProfiler,
		private readonly FilterLookup $filterLookup,
		private readonly FilterImporter $filterImporter,
		private readonly FilterStore $filterStore,
		private readonly EditBoxBuilderFactory $boxBuilderFactory,
		private readonly ConsequencesRegistry $consequencesRegistry,
		private readonly SpecsFormatter $specsFormatter,
		IContextSource $context,
		LinkRenderer $linkRenderer,
		string $basePageName,
		array $params
	) {
		parent::__construct( $afPermManager, $context, $linkRenderer, $basePageName, $params );
		$this->specsFormatter->setMessageLocalizer( $this->getContext() );
		$this->filter = $this->mParams['filter'];
		$this->historyID = $this->mParams['history'] ?? null;
	}

	/**
	 * Shows the page
	 */
	public function show() {
		$out = $this->getOutput();
		$out->enableOOUI();
		$request = $this->getRequest();
		$out->setPageTitleMsg( $this->msg( 'abusefilter-edit' ) );
		$out->addHelpLink( 'Extension:AbuseFilter/Rules format' );

		if ( !is_numeric( $this->filter ) && $this->filter !== null ) {
			$this->showUnrecoverableError( 'abusefilter-edit-badfilter' );
			return;
		}
		$filter = $this->filter ? (int)$this->filter : null;
		$history_id = $this->historyID;
		if ( $this->historyID ) {
			$dbr = $this->lbFactory->getReplicaDatabase();
			$lastID = (int)$dbr->newSelectQueryBuilder()
				->select( 'afh_id' )
				->from( 'abuse_filter_history' )
				->where( [
					'afh_filter' => $filter,
				] )
				->orderBy( 'afh_id', SelectQueryBuilder::SORT_DESC )
				->caller( __METHOD__ )
				->fetchField();
			// change $history_id to null if it's current version id
			if ( $lastID === $this->historyID ) {
				$history_id = null;
			}
		}

		// Add the default warning and disallow messages in a JS variable
		$this->exposeMessages();

		$canEdit = $this->afPermManager->canEdit( $this->getAuthority() );

		if ( $filter === null && !$canEdit ) {
			// Special case: Special:AbuseFilter/new is certainly not usable if the user cannot edit
			$this->showUnrecoverableError( 'abusefilter-edit-notallowed' );
			return;
		}

		$isImport = $request->wasPosted() && $request->getRawVal( 'wpImportText' ) !== null;

		if ( !$isImport && $request->wasPosted() && $canEdit ) {
			$this->attemptSave( $filter, $history_id );
			return;
		}

		if ( $isImport ) {
			$filterObj = $this->loadImportRequest();
			if ( $filterObj === null ) {
				$this->showUnrecoverableError( 'abusefilter-import-invalid-data' );
				return;
			}
		} else {
			// The request wasn't posted (i.e. just viewing the filter) or the user cannot edit
			try {
				$filterObj = $this->loadFromDatabase( $filter, $history_id );
			} catch ( FilterNotFoundException ) {
				$filterObj = null;
			}
			if ( $filterObj === null || ( $history_id && (int)$filterObj->getID() !== $filter ) ) {
				$this->showUnrecoverableError( 'abusefilter-edit-badfilter' );
				return;
			}
		}

		$this->buildFilterEditor( null, $filterObj, $filter, $history_id );
	}

	/**
	 * @param int|null $filter The filter ID or null for a new filter
	 * @param int|null $history_id The history ID of the filter, if applicable. Otherwise null
	 */
	private function attemptSave( ?int $filter, $history_id ): void {
		$out = $this->getOutput();
		$request = $this->getRequest();
		$authority = $this->getAuthority();

		[ $newFilter, $origFilter ] = $this->loadRequest( $filter );

		$tokenFilter = $filter === null ? 'new' : (string)$filter;
		$editToken = $request->getVal( 'wpEditToken' );
		$tokenMatches = $this->getCsrfTokenSet()->matchToken( $editToken, [ 'abusefilter', $tokenFilter ] );

		if ( !$tokenMatches ) {
			// Token invalid or expired while the page was open, warn to retry
			$status = StatusValue::newGood();
			$status->warning( 'abusefilter-edit-token-not-match' );
			$this->buildFilterEditor( $status, $newFilter, $filter, $history_id );
			return;
		}

		if ( !$request->getBool( 'wpMakePublic' ) && !$newFilter->isHidden() && $origFilter->isHidden() ) {
			// Warn if the user attempts to make a private filter public
			$request->setVal( 'wpMakePublic', 1 );
			$status = StatusValue::newGood();
			$status->warning( 'abusefilter-edit-makepublic' );
			$this->buildFilterEditor( $status, $newFilter, $filter, $history_id );
			return;
		}

		$status = $this->filterStore->saveFilter( $authority, $filter, $newFilter, $origFilter );

		if ( !$status->isGood() ) {
			if ( $status->isOK() ) {
				// Fixable error, show the editing interface
				$this->buildFilterEditor( $status, $newFilter, $filter, $history_id );
			} else {
				$this->showUnrecoverableError( $status->getMessages()[0] );
			}
		} elseif ( $status->getValue() === false ) {
			// No change
			$out->redirect( $this->getTitle()->getLocalURL() );
		} else {
			// Everything went fine!
			[ $new_id, $history_id ] = $status->getValue();
			$out->redirect(
				$this->getTitle()->getLocalURL(
					[
						'result' => 'success',
						'changedfilter' => $new_id,
						'changeid' => $history_id,
					]
				)
			);
		}
	}

	/**
	 * @param string|\Wikimedia\Message\MessageSpecifier $msg
	 */
	private function showUnrecoverableError( $msg ): void {
		$out = $this->getOutput();

		$out->addModuleStyles( 'mediawiki.codex.messagebox.styles' );
		$out->addHTML( Html::errorBox( $this->msg( $msg )->parseAsBlock() ) );
		$href = $this->getTitle()->getFullURL();
		$btn = new OOUI\ButtonWidget( [
			'label' => $this->msg( 'abusefilter-return' )->text(),
			'href' => $href
		] );
		$out->addHTML( (string)$btn );
	}

	/**
	 * Builds the full form for edit filters, adding it to the OutputPage. This method can be called in 5 different
	 * situations, for a total of 5 different data sources for $filterObj and $actions:
	 *  1 - View the result of importing a filter
	 *  2 - Create a new filter
	 *  3 - Load the current version of an existing filter
	 *  4 - Load an old version of an existing filter
	 *  5 - Show the user input again if saving fails after one of the steps above
	 *
	 * @param StatusValue|null $status A status for showing warnings or errors above the filter box
	 * @param Filter $filterObj
	 * @param int|null $filter The filter ID, or null for a new filter
	 * @param int|null $history_id The history ID of the filter, if applicable. Otherwise null
	 */
	private function buildFilterEditor(
		?StatusValue $status,
		Filter $filterObj,
		?int $filter,
		$history_id
	) {
		$out = $this->getOutput();
		$out->addJsConfigVars( 'isFilterEditor', true );
		$lang = $this->getLanguage();
		$authority = $this->getAuthority();
		$actions = $filterObj->getActions();

		$isCreatingNewFilter = $filter === null;
		$out->addSubtitle( $this->msg(
			$isCreatingNewFilter ? 'abusefilter-edit-subtitle-new' : 'abusefilter-edit-subtitle',
			$isCreatingNewFilter ? $filter : $this->getLanguage()->formatNum( $filter ),
			$history_id
		)->parse() );

		// Filters that are suppressed should always be hidden from public view
		if (
			( $filterObj->isSuppressed() || (
				$filter !== null && $this->filterLookup->getFilter( $filter, false )->isSuppressed() )
			) && !$this->afPermManager->canViewSuppressed( $authority )
		) {
			$out->addHTML( $this->msg( 'abusefilter-edit-denied-suppressed' )->escaped() );
			return;
		}

		// Grab the current hidden flag from the DB, in case we're editing an older, public revision of a filter that is
		// currently hidden, so that we can also hide that public revision.
		if (
			( $filterObj->isHidden() || (
				$filter !== null && $this->filterLookup->getFilter( $filter, false )->isHidden() )
			) && !$this->afPermManager->canViewPrivateFilters( $authority )
		) {
			$out->addHTML( $this->msg( 'abusefilter-edit-denied' )->escaped() );
			return;
		}

		// Filters that use protected variables should always be hidden from public view
		$protectedVarsPermStatus = AbuseFilterPermissionStatus::newGood();
		if ( $filterObj->isProtected() ) {
			$protectedVarsPermStatus = $this->afPermManager
				->canViewProtectedVariablesInFilter( $authority, $filterObj );
		}

		if ( $filter !== null && $protectedVarsPermStatus->isGood() ) {
			$currentFilterObj = $this->filterLookup->getFilter( $filter, false );
			if ( $currentFilterObj->isProtected() ) {
				$protectedVarsPermStatus = $this->afPermManager
					->canViewProtectedVariablesInFilter( $authority, $currentFilterObj );
			}
		}

		if ( !$protectedVarsPermStatus->isGood() ) {
			if ( $protectedVarsPermStatus->getPermission() ) {
				$out->addWikiMsg(
					$this->msg(
						'abusefilter-edit-denied-protected-vars-because-of-permission',
						$this->msg( "action-{$protectedVarsPermStatus->getPermission()}" )->plain()
					)
				);
				return;
			}

			// Add any messages in the status after a generic error message.
			$additional = '';
			foreach ( $protectedVarsPermStatus->getMessages() as $message ) {
				$additional .= $this->msg( $message )->parseAsBlock();
			}

			$out->addWikiMsg(
				$this->msg( 'abusefilter-edit-denied-protected-vars' )->rawParams( $additional )
			);
			return;
		}

		if ( $isCreatingNewFilter ) {
			$title = $this->msg( 'abusefilter-add' );
		} elseif ( $this->afPermManager->canEditFilter( $authority, $filterObj ) ) {
			$title = $this->msg( 'abusefilter-edit-specific' )
				->numParams( $this->filter )
				->params( $filterObj->getName() );
		} else {
			$title = $this->msg( 'abusefilter-view-specific' )
				->numParams( $this->filter )
				->params( $filterObj->getName() );
		}
		$out->setPageTitleMsg( $title );

		$readOnly = !$this->afPermManager->canEditFilter( $authority, $filterObj );

		if ( $history_id ) {
			$oldWarningMessage = $readOnly
				? 'abusefilter-edit-oldwarning-view'
				: 'abusefilter-edit-oldwarning';
			$out->addWikiMsg( $oldWarningMessage, $history_id, (string)$filter );
		}

		if ( $status !== null && !$status->isGood() ) {
			$out->addModuleStyles( 'mediawiki.codex.messagebox.styles' );
			foreach ( $status->getMessages( 'error' ) as $message ) {
				$out->addHTML( Html::errorBox( $this->msg( $message )->parseAsBlock() ) );
			}

			foreach ( $status->getMessages( 'warning' ) as $message ) {
				$out->addHTML( Html::warningBox( $this->msg( $message )->parseAsBlock() ) );
			}
		}

		$fields = [];

		$fields[] = new OOUI\FieldLayout(
			new OOUI\LabelWidget( [
				'label' => $isCreatingNewFilter
					? $this->msg( 'abusefilter-edit-new' )->text()
					: $lang->formatNum( (string)$filter ),
			] ),
			[
				'label' => new OOUI\HtmlSnippet( $this->msg( 'abusefilter-edit-id' )->parse() ),
				'id' => 'mw-abusefilter-edit-id',
			]
		);
		$fields[] = new OOUI\FieldLayout(
			new OOUI\TextInputWidget( [
				'name' => 'wpFilterDescription',
				'id' => 'mw-abusefilter-edit-description-input',
				'value' => $filterObj->getName(),
				'readOnly' => $readOnly
			] ),
			[
				'label' => new OOUI\HtmlSnippet( $this->msg( 'abusefilter-edit-description' )->parse() ),
			]
		);

		$validGroups = $this->getConfig()->get( 'AbuseFilterValidGroups' );
		if ( count( $validGroups ) > 1 ) {
			$groupSelector =
				new OOUI\DropdownInputWidget( [
					'name' => 'wpFilterGroup',
					'id' => 'mw-abusefilter-edit-group-input',
					'value' => $filterObj->getGroup(),
					'disabled' => $readOnly
				] );

			$options = [];
			foreach ( $validGroups as $group ) {
				$options += [ $this->specsFormatter->nameGroup( $group ) => $group ];
			}

			$options = Html::listDropdownOptionsOoui( $options );
			$groupSelector->setOptions( $options );

			$fields[] = new OOUI\FieldLayout(
				$groupSelector,
				[
					'label' => new OOUI\HtmlSnippet( $this->msg( 'abusefilter-edit-group' )->parse() ),
				]
			);
		}

		// Hit count display
		$hitCount = $filterObj->getHitCount();
		if ( $hitCount !== null && $this->afPermManager->canSeeLogDetails( $authority ) ) {
			$count_display = $this->msg( 'abusefilter-hitcount' )
				->numParams( $hitCount )->text();
			$hitCount = $this->linkRenderer->makeKnownLink(
				SpecialPage::getTitleFor( 'AbuseLog' ),
				$count_display,
				[],
				[ 'wpSearchFilter' => $filterObj->getID() ]
			);

			$fields[] = new OOUI\FieldLayout(
				new OOUI\LabelWidget( [
					'label' => new OOUI\HtmlSnippet( $hitCount ),
				] ),
				[
					'label' => new OOUI\HtmlSnippet( $this->msg( 'abusefilter-edit-hitcount' )->parse() ),
				]
			);
		}

		if ( $filter !== null && $filterObj->isEnabled() ) {
			// Statistics
			[
				'count' => $totalCount,
				'matches' => $matchesCount,
				'total-time' => $curTotalTime,
				'total-cond' => $curTotalConds,
			] = $this->filterProfiler->getFilterProfile( $filter );

			if ( $totalCount > 0 ) {
				$matchesPercent = round( 100 * $matchesCount / $totalCount, 2 );
				$avgTime = round( $curTotalTime / $totalCount, 2 );
				$avgCond = round( $curTotalConds / $totalCount, 1 );

				$fields[] = new OOUI\FieldLayout(
					new OOUI\LabelWidget( [
						'label' => new OOUI\HtmlSnippet( $this->msg( 'abusefilter-edit-status' )
							->numParams( $totalCount, $matchesCount, $matchesPercent, $avgTime, $avgCond )
							->parse() ),
					] ),
					[
						'label' => new OOUI\HtmlSnippet( $this->msg( 'abusefilter-edit-status-label' )->parse() ),
					]
				);
			}
		}

		$boxBuilder = $this->boxBuilderFactory->newEditBoxBuilder( $this, $authority, $out );

		$fields[] = new OOUI\FieldLayout(
			new OOUI\LabelWidget( [
				'label' => new OOUI\HtmlSnippet( $boxBuilder->buildEditBox(
					$filterObj->getRules(),
					true
				) ),
			] ),
			[
				'label' => new OOUI\HtmlSnippet( $this->msg( 'abusefilter-edit-rules' )->parse() ),
				'id' => 'mw-abusefilter-edit-rules',
			]
		);
		$fields[] = new OOUI\FieldLayout(
			new OOUI\MultilineTextInputWidget( [
				'name' => 'wpFilterNotes',
				'value' => $filterObj->getComments() . "\n",
				'rows' => 15,
				'readOnly' => $readOnly,
				'id' => 'mw-abusefilter-notes-editor'
			] ),
			[
				'label' => new OOUI\HtmlSnippet( $this->msg( 'abusefilter-edit-notes' )->parse() ),
			]
		);

		// Build checkboxes
		$checkboxes = [ 'suppressed', 'hidden', 'enabled', 'deleted' ];
		$flags = '';

		// Show the 'protected' check box either to indicate that the filter is protected, or
		// to allow a user to protect the filter, if the filter needs to be protected.
		if (
			$filterObj->isProtected() ||
			(
				$status !== null &&
				$status->hasMessage( 'abusefilter-edit-protected-variable-not-protected' )
			)
		) {
			$checkboxes[] = 'protected';
		}

		if ( $this->getConfig()->get( 'AbuseFilterIsCentral' ) ) {
			$checkboxes[] = 'global';
		}

		if ( $filterObj->isThrottled() ) {
			$throttledActionNames = array_intersect(
				$filterObj->getActionsNames(),
				$this->consequencesRegistry->getDangerousActionNames()
			);

			if ( $throttledActionNames ) {
				$throttledActionsLocalized = [];
				foreach ( $throttledActionNames as $actionName ) {
					$throttledActionsLocalized[] = $this->specsFormatter->getActionMessage( $actionName )->text();
				}

				$throttledMsg = $this->msg( 'abusefilter-edit-throttled-warning' )
					->plaintextParams( $lang->commaList( $throttledActionsLocalized ) )
					->params( count( $throttledActionsLocalized ) )
					->parseAsBlock();
			} else {
				$throttledMsg = $this->msg( 'abusefilter-edit-throttled-warning-no-actions' )
					->parseAsBlock();
			}
			$fields[] = new OOUI\FieldLayout(
				new OOUI\MessageWidget( [
					'type' => 'warning',
					'label' => new OOUI\HtmlSnippet( $throttledMsg ),
				] ),
				[
					'label' => ' ',
				]
			);
		}

		foreach ( $checkboxes as $checkboxId ) {
			// Messages that can be used here:
			// * abusefilter-edit-enabled
			// * abusefilter-edit-deleted
			// * abusefilter-edit-suppressed
			// * abusefilter-edit-hidden
			// * abusefilter-edit-protected
			// * abusefilter-edit-global
			$message = "abusefilter-edit-$checkboxId";
			// isEnabled(), isDeleted(), isHidden(), isProtected(), isGlobal()
			$method = 'is' . ucfirst( $checkboxId );
			// wpFilterEnabled, wpFilterDeleted, wpFilterSuppressed, wpFilterHidden, wpFilterProtected, wpFilterGlobal
			$postVar = 'wpFilter' . ucfirst( $checkboxId );

			$checkboxAttribs = [
				'name' => $postVar,
				'id' => $postVar,
				'selected' => $filterObj->$method(),
				'disabled' => $readOnly
			];
			$labelAttribs = [
				'label' => $this->msg( $message )->text(),
				'align' => 'inline',
			];

			if ( $checkboxId === 'global' && !$this->afPermManager->canEditGlobal( $authority ) ) {
				$checkboxAttribs['disabled'] = 'disabled';
			}

			if ( $checkboxId === 'suppressed' && !$this->afPermManager->canSuppress( $authority ) ) {
				$checkboxAttribs['disabled'] = 'disabled';
			}

			if ( $checkboxId === 'protected' ) {
				if ( $filterObj->isProtected() ) {
					$checkboxAttribs['disabled'] = true;
					$labelAttribs['label'] = $this->msg(
						'abusefilter-edit-protected-variable-already-protected'
					)->text();
				} else {
					$labelAttribs['label'] = new OOUI\HtmlSnippet(
						$this->msg( $message )->parse()
					);
					$labelAttribs['help'] = $this->msg( 'abusefilter-edit-protected-help-message' )->text();
					$labelAttribs['helpInline'] = true;
				}
			}

			// Set readonly on deleted if the filter isn't disabled
			if ( $checkboxId === 'deleted' && $filterObj->isEnabled() ) {
				$checkboxAttribs['disabled'] = 'disabled';
			}

			// Add infusable where needed
			if ( $checkboxId === 'deleted' ) {
				// wpFilterDeletedLabel
				$labelAttribs['id'] = $postVar . 'Label';
				$labelAttribs['infusable'] = true;
			} elseif ( $checkboxId === 'enabled' ) {
				$checkboxAttribs['infusable'] = true;
			}

			$checkbox =
				new OOUI\FieldLayout(
					new OOUI\CheckboxInputWidget( $checkboxAttribs ),
					$labelAttribs
				);
			$flags .= $checkbox;
		}

		$fields[] = new OOUI\FieldLayout(
			new OOUI\LabelWidget( [
				'label' => new OOUI\HtmlSnippet( $flags ),
			] ),
			[
				'label' => new OOUI\HtmlSnippet( $this->msg( 'abusefilter-edit-flags' )->parse() ),
			]
		);

		if ( $filter !== null ) {
			$tools = '';
			if ( $this->afPermManager->canRevertFilterActions( $authority ) ) {
				$tools .= Html::rawElement(
					'p', [],
					$this->linkRenderer->makeLink(
						$this->getTitle( "revert/$filter" ),
						new HtmlArmor( $this->msg( 'abusefilter-edit-revert' )->parse() )
					)
				);
			}

			if ( $this->afPermManager->canUseTestTools( $authority ) ) {
				// Test link
				$tools .= Html::rawElement(
					'p', [],
					$this->linkRenderer->makeLink(
						$this->getTitle( "test/$filter" ),
						new HtmlArmor( $this->msg( 'abusefilter-edit-test-link' )->parse() )
					)
				);
			}

			// Last modification details
			$user = $this->getUser();
			$userLink =
				Linker::userLink( $filterObj->getUserID(), $filterObj->getUserName() ) .
				Linker::userToolLinks( $filterObj->getUserID(), $filterObj->getUserName() );
			$fields[] = new OOUI\FieldLayout(
				new OOUI\LabelWidget( [
					'label' => new OOUI\HtmlSnippet( $this->msg( 'abusefilter-edit-lastmod-text' )
						->rawParams(
							$this->getLinkToLatestDiff(
								$filter,
								$lang->userTimeAndDate( $filterObj->getTimestamp(), $user )
							),
							$userLink,
							$this->getLinkToLatestDiff(
								$filter,
								$lang->userDate( $filterObj->getTimestamp(), $user )
							),
							$this->getLinkToLatestDiff(
								$filter,
								$lang->userTime( $filterObj->getTimestamp(), $user )
							)
						)->params(
							wfEscapeWikiText( $filterObj->getUserName() )
						)->parse() ),
				] ),
				[
					'label' => new OOUI\HtmlSnippet( $this->msg( 'abusefilter-edit-lastmod' )->parse() ),
				]
			);
			$history_display = new HtmlArmor( $this->msg( 'abusefilter-edit-viewhistory' )->parse() );
			$fields[] = new OOUI\FieldLayout(
				new OOUI\LabelWidget( [
					'label' => new OOUI\HtmlSnippet(
						$this->linkRenderer->makeKnownLink( $this->getTitle( 'history/' . $filter ), $history_display )
					),
				] ),
				[
					'label' => new OOUI\HtmlSnippet( $this->msg( 'abusefilter-edit-history' )->parse() ),
				]
			);

			$exportText = $this->filterImporter->encodeData( $filterObj, $actions );
			$tools .= Html::rawElement( 'a', [ 'href' => '#', 'id' => 'mw-abusefilter-export-link' ],
				$this->msg( 'abusefilter-edit-export' )->parse() );
			$tools .=
				new OOUI\MultilineTextInputWidget( [
					'id' => 'mw-abusefilter-export',
					'readOnly' => true,
					'value' => $exportText,
					'rows' => 10
				] );

			$fields[] = new OOUI\FieldLayout(
				new OOUI\LabelWidget( [
					'label' => new OOUI\HtmlSnippet( $tools ),
				] ),
				[
					'label' => new OOUI\HtmlSnippet( $this->msg( 'abusefilter-edit-tools' )->parse() ),
				]
			);
		}

		$form = Html::openElement( 'fieldset', [ 'id' => 'mw-abusefilter-editing-main' ] ) . "\n" .
			Html::element( 'legend', [], $this->msg( 'abusefilter-edit-main' )->text() ) . "\n" .
			implode( '', $fields ) . "\n" .
			Html::closeElement( 'fieldset' ) . "\n";
		$form .= Html::openElement( 'fieldset' ) . "\n" .
			Html::element( 'legend', [], $this->msg( 'abusefilter-edit-consequences' )->text() ) . "\n" .
			$this->buildConsequenceEditor( $filterObj, $actions ) . "\n" .
			Html::closeElement( 'fieldset' );

		$urlFilter = $filter === null ? 'new' : (string)$filter;
		if ( !$readOnly ) {
			$form .=
				new OOUI\ButtonInputWidget( [
					'type' => 'submit',
					'label' => $this->msg( 'abusefilter-edit-save' )->text(),
					'useInputTag' => true,
					'accesskey' => 's',
					'flags' => [ 'progressive', 'primary' ]
				] );
			$form .= Html::hidden(
				'wpEditToken',
				$this->getCsrfTokenSet()->getToken( [ 'abusefilter', $urlFilter ] )->toString()
			);
			// Whether the abusefilter-edit-makepublic warning is currently shown
			$form .= Html::hidden(
				'wpMakePublic',
				$this->getRequest()->getInt( 'wpMakePublic' )
			);
		}

		$form = Html::rawElement( 'form',
			[
				'action' => $this->getTitle( $urlFilter )->getFullURL(),
				'method' => 'post',
				'id' => 'mw-abusefilter-editing-form'
			],
			$form
		);

		$out->addHTML( $form );

		if ( $history_id ) {
			// @phan-suppress-next-line PhanPossiblyUndeclaredVariable,PhanTypeMismatchArgumentNullable
			$out->addWikiMsg( $oldWarningMessage, $history_id, $filter );
		}
	}

	/**
	 * Builds the "actions" editor for a given filter.
	 * @param Filter $filterObj
	 * @param array[] $actions Array of rows from the abuse_filter_action table
	 *  corresponding to the filter object
	 * @return string HTML text for an action editor.
	 */
	private function buildConsequenceEditor( Filter $filterObj, array $actions ) {
		$enabledActions = $this->consequencesRegistry->getAllEnabledActionNames();

		$setActions = [];
		foreach ( $enabledActions as $action ) {
			$setActions[$action] = array_key_exists( $action, $actions );
		}

		$output = '';

		foreach ( $enabledActions as $action ) {
			$params = $actions[$action] ?? null;
			$output .= $this->buildConsequenceSelector(
				$action, $setActions[$action], $filterObj, $params );
		}

		return $output;
	}

	/**
	 * @param string $action The action to build an editor for
	 * @param bool $set Whether or not the action is activated
	 * @param Filter $filterObj
	 * @param string[]|null $parameters Action parameters. Null iff $set is false.
	 * @return string|\OOUI\FieldLayout
	 */
	private function buildConsequenceSelector( $action, $set, $filterObj, ?array $parameters ) {
		$config = $this->getConfig();
		$authority = $this->getAuthority();
		$actions = $this->consequencesRegistry->getAllEnabledActionNames();
		if ( !in_array( $action, $actions, true ) ) {
			return '';
		}

		$readOnly = !$this->afPermManager->canEditFilter( $authority, $filterObj );

		switch ( $action ) {
			case 'throttle':
				// Throttling is only available via object caching
				if ( $config->get( MainConfigNames::MainCacheType ) === CACHE_NONE ) {
					return '';
				}
				$throttleSettings =
					new OOUI\FieldLayout(
						new OOUI\CheckboxInputWidget( [
							'name' => 'wpFilterActionThrottle',
							'id' => 'mw-abusefilter-action-checkbox-throttle',
							'selected' => $set,
							'classes' => [ 'mw-abusefilter-action-checkbox' ],
							'disabled' => $readOnly
						]
						),
						[
							'label' => $this->msg( 'abusefilter-edit-action-throttle' )->text(),
							'align' => 'inline',
							'help' => $this->msg( 'abusefilter-edit-action-throttle-help' )->text(),
							'helpInline' => true,
						]
					);
				$throttleFields = [];

				if ( $set ) {
					// @phan-suppress-next-line PhanTypeArraySuspiciousNullable $parameters is array here
					[ $throttleCount, $throttlePeriod ] = explode( ',', $parameters[1], 2 );

					$throttleGroups = array_slice( $parameters, 2 );
				} else {
					$throttleCount = 3;
					$throttlePeriod = 60;

					$throttleGroups = [ 'user' ];
				}

				$throttleFields[] =
					new OOUI\FieldLayout(
						new OOUI\TextInputWidget( [
							'type' => 'number',
							'name' => 'wpFilterThrottleCount',
							'value' => $throttleCount,
							'disabled' => $readOnly
							]
						),
						[
							'label' => $this->msg( 'abusefilter-edit-throttle-count' )->text()
						]
					);
				$throttleFields[] =
					new OOUI\FieldLayout(
						new OOUI\TextInputWidget( [
							'type' => 'number',
							'name' => 'wpFilterThrottlePeriod',
							'value' => $throttlePeriod,
							'disabled' => $readOnly
							]
						),
						[
							'label' => $this->msg( 'abusefilter-edit-throttle-period' )->text()
						]
					);

				$groupsHelpLink = Html::element(
					'a',
					[
						'href' => 'https://www.mediawiki.org/wiki/Special:MyLanguage/' .
							'Extension:AbuseFilter/Actions#Throttling',
						'target' => '_blank'
					],
					$this->msg( 'abusefilter-edit-throttle-groups-help-text' )->text()
				);
				$groupsHelp = $this->msg( 'abusefilter-edit-throttle-groups-help' )
						->rawParams( $groupsHelpLink )->escaped();
				$hiddenGroups =
					new OOUI\FieldLayout(
						new OOUI\MultilineTextInputWidget( [
							'name' => 'wpFilterThrottleGroups',
							'value' => implode( "\n", $throttleGroups ),
							'rows' => 5,
							'placeholder' => $this->msg( 'abusefilter-edit-throttle-hidden-placeholder' )->text(),
							'infusable' => true,
							'id' => 'mw-abusefilter-hidden-throttle-field',
							'readOnly' => $readOnly
						]
						),
						[
							'label' => new OOUI\HtmlSnippet(
								$this->msg( 'abusefilter-edit-throttle-groups' )->parse()
							),
							'align' => 'top',
							'id' => 'mw-abusefilter-hidden-throttle',
							'help' => new OOUI\HtmlSnippet( $groupsHelp ),
							'helpInline' => true
						]
					);

				$throttleFields[] = $hiddenGroups;

				$throttleConfig = [
					'values' => $throttleGroups,
					'label' => $this->msg( 'abusefilter-edit-throttle-groups' )->parse(),
					'disabled' => $readOnly,
					'help' => $groupsHelp
				];
				$this->getOutput()->addJsConfigVars( 'throttleConfig', $throttleConfig );

				$throttleSettings .=
					Html::rawElement(
						'div',
						[ 'id' => 'mw-abusefilter-throttle-parameters' ],
						(string)new OOUI\FieldsetLayout( [ 'items' => $throttleFields ] )
					);
				return $throttleSettings;
			case 'disallow':
			case 'warn':
				$output = '';
				$formName = $action === 'warn' ? 'wpFilterActionWarn' : 'wpFilterActionDisallow';
				$checkbox =
					new OOUI\FieldLayout(
						new OOUI\CheckboxInputWidget( [
							'name' => $formName,
							// mw-abusefilter-action-checkbox-warn, mw-abusefilter-action-checkbox-disallow
							'id' => "mw-abusefilter-action-checkbox-$action",
							'selected' => $set,
							'classes' => [ 'mw-abusefilter-action-checkbox' ],
							'disabled' => $readOnly
						]
						),
						[
							// abusefilter-edit-action-warn, abusefilter-edit-action-disallow
							'label' => $this->msg( "abusefilter-edit-action-$action" )->text(),
							'align' => 'inline',
							// abusefilter-edit-action-warn-help, abusefilter-edit-action-disallow-help
							'help' => $this->msg( "abusefilter-edit-action-$action-help" )->text(),
							'helpInline' => true,
						]
					);
				$output .= $checkbox;
				$defaultWarnMsg = $config->get( 'AbuseFilterDefaultWarningMessage' );
				$defaultDisallowMsg = $config->get( 'AbuseFilterDefaultDisallowMessage' );

				if ( $set && isset( $parameters[0] ) ) {
					$msg = $parameters[0];
				} elseif (
					( $action === 'warn' && isset( $defaultWarnMsg[$filterObj->getGroup()] ) ) ||
					( $action === 'disallow' && isset( $defaultDisallowMsg[$filterObj->getGroup()] ) )
				) {
					$msg = $action === 'warn' ? $defaultWarnMsg[$filterObj->getGroup()] :
						$defaultDisallowMsg[$filterObj->getGroup()];
				} else {
					$msg = $action === 'warn' ? 'abusefilter-warning' : 'abusefilter-disallowed';
				}

				$fields = [];
				$fields[] =
					$this->getExistingSelector( $msg, $readOnly, $action );
				$otherFieldName = $action === 'warn' ? 'wpFilterWarnMessageOther'
					: 'wpFilterDisallowMessageOther';

				$fields[] =
					new OOUI\FieldLayout(
						new OOUI\TextInputWidget( [
							'name' => $otherFieldName,
							'value' => $msg,
							// mw-abusefilter-warn-message-other, mw-abusefilter-disallow-message-other
							'id' => "mw-abusefilter-$action-message-other",
							'infusable' => true,
							'disabled' => $readOnly
							]
						),
						[
							'label' => new OOUI\HtmlSnippet(
								// abusefilter-edit-warn-other-label, abusefilter-edit-disallow-other-label
								$this->msg( "abusefilter-edit-$action-other-label" )->parse()
							)
						]
					);

				$previewButton =
					new OOUI\ButtonInputWidget( [
						// abusefilter-edit-warn-preview, abusefilter-edit-disallow-preview
						'label' => $this->msg( "abusefilter-edit-$action-preview" )->text(),
						// mw-abusefilter-warn-preview-button, mw-abusefilter-disallow-preview-button
						'id' => "mw-abusefilter-$action-preview-button",
						'infusable' => true,
						'flags' => 'progressive'
						]
					);

				$buttonGroup = $previewButton;
				if ( $authority->isAllowed( 'editinterface' ) ) {
					$editButton =
						new OOUI\ButtonInputWidget( [
							// abusefilter-edit-warn-edit, abusefilter-edit-disallow-edit
							'label' => $this->msg( "abusefilter-edit-$action-edit" )->text(),
							// mw-abusefilter-warn-edit-button, mw-abusefilter-disallow-edit-button
							'id' => "mw-abusefilter-$action-edit-button"
							]
						);
					$buttonGroup =
						new OOUI\Widget( [
							'content' =>
								new OOUI\HorizontalLayout( [
									'items' => [ $previewButton, $editButton ],
									'classes' => [
										'mw-abusefilter-preview-buttons',
										'mw-abusefilter-javascript-tools'
									]
								] )
						] );
				}
				$previewHolder = Html::rawElement(
					'div',
					[
						// mw-abusefilter-warn-preview, mw-abusefilter-disallow-preview
						'id' => "mw-abusefilter-$action-preview",
						'style' => 'display:none'
					],
					''
				);
				$fields[] = $buttonGroup;
				$output .=
					Html::rawElement(
						'div',
						// mw-abusefilter-warn-parameters, mw-abusefilter-disallow-parameters
						[ 'id' => "mw-abusefilter-$action-parameters" ],
						(string)new OOUI\FieldsetLayout( [ 'items' => $fields ] )
					) . $previewHolder;

				return $output;
			case 'tag':
				$tags = $set ? ( $parameters ?? [] ) : [];
				$output = '';

				$checkbox =
					new OOUI\FieldLayout(
						new OOUI\CheckboxInputWidget( [
							'name' => 'wpFilterActionTag',
							'id' => 'mw-abusefilter-action-checkbox-tag',
							'selected' => $set,
							'classes' => [ 'mw-abusefilter-action-checkbox' ],
							'disabled' => $readOnly
						]
						),
						[
							'label' => $this->msg( 'abusefilter-edit-action-tag' )->text(),
							'align' => 'inline',
							'help' => $this->msg( 'abusefilter-edit-action-tag-help' )->text(),
							'helpInline' => true,
						]
					);
				$output .= $checkbox;

				$tagConfig = [
					'values' => $tags,
					'label' => $this->msg( 'abusefilter-edit-tag-tag' )->parse(),
					'disabled' => $readOnly
				];
				$this->getOutput()->addJsConfigVars( 'tagConfig', $tagConfig );

				$hiddenTags =
					new OOUI\FieldLayout(
						new OOUI\MultilineTextInputWidget( [
							'name' => 'wpFilterTags',
							'value' => implode( ',', $tags ),
							'rows' => 5,
							'placeholder' => $this->msg( 'abusefilter-edit-tag-hidden-placeholder' )->text(),
							'infusable' => true,
							'id' => 'mw-abusefilter-hidden-tag-field',
							'readOnly' => $readOnly
						]
						),
						[
							'label' => new OOUI\HtmlSnippet(
								$this->msg( 'abusefilter-edit-tag-tag' )->parse()
							),
							'align' => 'top',
							'id' => 'mw-abusefilter-hidden-tag'
						]
					);
				$output .=
					Html::rawElement( 'div',
						[ 'id' => 'mw-abusefilter-tag-parameters' ],
						(string)$hiddenTags
					);
				return $output;
			case 'block':
				if ( $set && count( $parameters ) === 3 ) {
					// Both blocktalk and custom block durations available
					[ $blockTalk, $defaultAnonDuration, $defaultUserDuration ] = $parameters;
				} else {
					if ( $set && count( $parameters ) === 1 ) {
						// Only blocktalk available
						$blockTalk = $parameters[0];
					}
					$defaultAnonDuration = $config->get( 'AbuseFilterAnonBlockDuration' ) ??
						$config->get( 'AbuseFilterBlockDuration' );
					$defaultUserDuration = $config->get( 'AbuseFilterBlockDuration' );
				}
				$suggestedBlocks = $this->getLanguage()->getBlockDurations( false );
				$suggestedBlocks = self::normalizeBlocks( $suggestedBlocks );

				$output = '';
				$checkbox =
					new OOUI\FieldLayout(
						new OOUI\CheckboxInputWidget( [
							'name' => 'wpFilterActionBlock',
							'id' => 'mw-abusefilter-action-checkbox-block',
							'selected' => $set,
							'classes' => [ 'mw-abusefilter-action-checkbox' ],
							'disabled' => $readOnly
						]
						),
						[
							'label' => $this->msg( 'abusefilter-edit-action-block' )->text(),
							'align' => 'inline',
							'help' => $this->msg( 'abusefilter-edit-action-block-help' )->text(),
							'helpInline' => true,
						]
					);
				$output .= $checkbox;

				$suggestedBlocks = Html::listDropdownOptionsOoui( $suggestedBlocks );

				$anonDuration =
					new OOUI\DropdownInputWidget( [
						'name' => 'wpBlockAnonDuration',
						'options' => $suggestedBlocks,
						'value' => $defaultAnonDuration,
						'disabled' => $readOnly
					] );

				$userDuration =
					new OOUI\DropdownInputWidget( [
						'name' => 'wpBlockUserDuration',
						'options' => $suggestedBlocks,
						'value' => $defaultUserDuration,
						'disabled' => $readOnly
					] );

				$blockOptions = [];
				if ( $config->get( MainConfigNames::BlockAllowsUTEdit ) === true ) {
					$talkCheckbox =
						new OOUI\FieldLayout(
							new OOUI\CheckboxInputWidget( [
								'name' => 'wpFilterBlockTalk',
								'id' => 'mw-abusefilter-action-checkbox-blocktalk',
								'selected' => isset( $blockTalk ) && $blockTalk === 'blocktalk',
								'classes' => [ 'mw-abusefilter-action-checkbox' ],
								'disabled' => $readOnly
							]
							),
							[
								'label' => $this->msg( 'abusefilter-edit-action-blocktalk' )->text(),
								'align' => 'left'
							]
						);

					$blockOptions[] = $talkCheckbox;
				}
				$blockOptions[] =
					new OOUI\FieldLayout(
						$anonDuration,
						[
							'label' => $this->msg( 'abusefilter-edit-block-anon-durations' )->text()
						]
					);
				$blockOptions[] =
					new OOUI\FieldLayout(
						$userDuration,
						[
							'label' => $this->msg( 'abusefilter-edit-block-user-durations' )->text()
						]
					);

				$output .= Html::rawElement(
						'div',
						[ 'id' => 'mw-abusefilter-block-parameters' ],
						(string)new OOUI\FieldsetLayout( [ 'items' => $blockOptions ] )
					);

				return $output;

			default:
				// Give grep a chance to find the usages:
				// abusefilter-edit-action-blockautopromote,
				// abusefilter-edit-action-degroup,
				// abusefilter-edit-action-rangeblock,
				$message = 'abusefilter-edit-action-' . $action;
				// Give grep a chance to find the usages:
				// abusefilter-edit-action-blockautopromote-help,
				// abusefilter-edit-action-degroup-help,
				// abusefilter-edit-action-rangeblock-help,
				$helpMessage = $this->msg( 'abusefilter-edit-action-' . $action . '-help' );
				$form_field = 'wpFilterAction' . ucfirst( $action );
				$status = $set;

				$thisAction =
					new OOUI\FieldLayout(
						new OOUI\CheckboxInputWidget( [
							'name' => $form_field,
							'id' => "mw-abusefilter-action-checkbox-$action",
							'selected' => $status,
							'classes' => [ 'mw-abusefilter-action-checkbox' ],
							'disabled' => $readOnly
						]
						),
						[
							'label' => $this->msg( $message )->text(),
							'align' => 'inline',
							'help' => $helpMessage->exists() ? $helpMessage->text() : '',
							'helpInline' => true,
						]
					);
				return $thisAction;
		}
	}

	/**
	 * @param string $warnMsg
	 * @param bool $readOnly
	 * @param string $action
	 * @return \OOUI\FieldLayout
	 */
	public function getExistingSelector( $warnMsg, $readOnly = false, $action = 'warn' ) {
		if ( $action === 'warn' ) {
			$action = 'warning';
			$formId = 'warn';
			$inputName = 'wpFilterWarnMessage';
		} elseif ( $action === 'disallow' ) {
			$action = 'disallowed';
			$formId = 'disallow';
			$inputName = 'wpFilterDisallowMessage';
		} else {
			throw new UnexpectedValueException( "Unexpected action value $action" );
		}

		$existingSelector =
			new OOUI\DropdownInputWidget( [
				'name' => $inputName,
				// mw-abusefilter-warn-message-existing, mw-abusefilter-disallow-message-existing
				'id' => "mw-abusefilter-$formId-message-existing",
				// abusefilter-warning, abusefilter-disallowed
				'value' => $warnMsg === "abusefilter-$action" ? "abusefilter-$action" : 'other',
				'infusable' => true
			] );

		// abusefilter-warning, abusefilter-disallowed
		$options = [ "abusefilter-$action" => "abusefilter-$action" ];

		if ( $readOnly ) {
			$existingSelector->setDisabled( true );
		} else {
			// Find other messages.
			$dbr = $this->lbFactory->getReplicaDatabase();
			$pageTitlePrefix = "Abusefilter-$action";
			$titles = $dbr->newSelectQueryBuilder()
				->select( 'page_title' )
				->from( 'page' )
				->where( [
					'page_namespace' => NS_MEDIAWIKI,
					$dbr->expr( 'page_title', IExpression::LIKE, new LikeValue( $pageTitlePrefix, $dbr->anyString() ) )
				] )
				->caller( __METHOD__ )
				->fetchFieldValues();

			$lang = $this->getLanguage();
			foreach ( $titles as $title ) {
				if ( $lang->lcfirst( $title ) === $lang->lcfirst( $warnMsg ) ) {
					$existingSelector->setValue( $lang->lcfirst( $warnMsg ) );
				}

				if ( $title !== "Abusefilter-$action" ) {
					$options[ $lang->lcfirst( $title ) ] = $lang->lcfirst( $title );
				}
			}
		}

		// abusefilter-edit-warn-other, abusefilter-edit-disallow-other
		$options[ $this->msg( "abusefilter-edit-$formId-other" )->text() ] = 'other';

		$options = Html::listDropdownOptionsOoui( $options );
		$existingSelector->setOptions( $options );

		$existingSelector =
			new OOUI\FieldLayout(
				$existingSelector,
				[
					// abusefilter-edit-warn-message, abusefilter-edit-disallow-message
					'label' => $this->msg( "abusefilter-edit-$formId-message" )->text()
				]
			);

		return $existingSelector;
	}

	/**
	 * @todo Maybe we should also check if global values belong to $durations
	 * and determine the right point to add them if missing.
	 *
	 * @param string[] $durations
	 * @return string[]
	 */
	private static function normalizeBlocks( array $durations ) {
		global $wgAbuseFilterBlockDuration, $wgAbuseFilterAnonBlockDuration;
		// We need to have same values since it may happen that ipblocklist
		// and one (or both) of the global variables use different wording
		// for the same duration. In such case, when setting the default of
		// the dropdowns it would fail.
		$anonDuration = self::getAbsoluteBlockDuration( $wgAbuseFilterAnonBlockDuration ??
			$wgAbuseFilterBlockDuration );
		$userDuration = self::getAbsoluteBlockDuration( $wgAbuseFilterBlockDuration );
		foreach ( $durations as &$duration ) {
			$currentDuration = self::getAbsoluteBlockDuration( $duration );

			if ( $duration !== $wgAbuseFilterBlockDuration &&
				$currentDuration === $userDuration ) {
				$duration = $wgAbuseFilterBlockDuration;

			} elseif ( $duration !== $wgAbuseFilterAnonBlockDuration &&
				$currentDuration === $anonDuration ) {
				$duration = $wgAbuseFilterAnonBlockDuration;
			}
		}

		return $durations;
	}

	/**
	 * Converts a string duration to an absolute timestamp, i.e. unrelated to the current
	 * time, taking into account infinity durations as well. The second parameter of
	 * strtotime is set to 0 in order to convert the duration in seconds (instead of
	 * a timestamp), thus making it unaffected by the execution time of the code.
	 *
	 * @param string $duration
	 * @return string|int
	 */
	private static function getAbsoluteBlockDuration( $duration ) {
		if ( wfIsInfinity( $duration ) ) {
			return 'infinity';
		}
		return strtotime( $duration, 0 );
	}

	/**
	 * Loads filter data from the database by ID.
	 * @param int|null $id The filter's ID number, or null for a new filter
	 * @return Filter
	 * @throws FilterNotFoundException
	 */
	private function loadFilterData( ?int $id ): Filter {
		if ( $id === null ) {
			return MutableFilter::newDefault();
		}

		$flags = $this->getRequest()->wasPosted()
			// Load from primary database to avoid unintended reversions where there's replication lag.
			? IDBAccessObject::READ_LATEST
			: IDBAccessObject::READ_NORMAL;

		return $this->filterLookup->getFilter( $id, false, $flags );
	}

	/**
	 * Load filter data to show in the edit view from the DB.
	 * @param int|null $filter The filter ID being requested or null for a new filter
	 * @param int|null $history_id If any, the history ID being requested.
	 * @return Filter|null Null if the filter does not exist.
	 */
	private function loadFromDatabase( ?int $filter, $history_id = null ): ?Filter {
		if ( $history_id ) {
			try {
				return $this->filterLookup->getFilterVersion( $history_id );
			} catch ( FilterVersionNotFoundException ) {
				return null;
			}
		} else {
			return $this->loadFilterData( $filter );
		}
	}

	/**
	 * Load data from the HTTP request. Used for saving the filter, and when the token doesn't match
	 * @param int|null $filter
	 * @return Filter[]
	 */
	private function loadRequest( ?int $filter ): array {
		$request = $this->getRequest();
		if ( !$request->wasPosted() ) {
			// Sanity
			throw new LogicException( __METHOD__ . ' called without the request being POSTed.' );
		}

		$origFilter = $this->loadFilterData( $filter );

		/** @var MutableFilter $newFilter */
		$newFilter = $origFilter instanceof MutableFilter
			? clone $origFilter
			: MutableFilter::newFromParentFilter( $origFilter );

		if ( $filter !== null ) {
			// Unchangeable values
			// @phan-suppress-next-line PhanTypeMismatchArgumentNullable
			$newFilter->setThrottled( $origFilter->isThrottled() );
			// @phan-suppress-next-line PhanTypeMismatchArgumentNullable
			$newFilter->setHitCount( $origFilter->getHitCount() );
			// These are needed if the save fails and the filter is not new
			$newFilter->setID( $origFilter->getID() );
			$newFilter->setUserIdentity( $origFilter->getUserIdentity() );
			$newFilter->setTimestamp( $origFilter->getTimestamp() );
		}

		$newFilter->setName( trim( $request->getVal( 'wpFilterDescription' ) ?? '' ) );
		$newFilter->setRules( trim( $request->getVal( 'wpFilterRules' ) ?? '' ) );
		$newFilter->setComments( trim( $request->getVal( 'wpFilterNotes' ) ?? '' ) );

		$newFilter->setGroup( $request->getVal( 'wpFilterGroup', 'default' ) );

		$newFilter->setDeleted( $request->getCheck( 'wpFilterDeleted' ) );
		$newFilter->setEnabled( $request->getCheck( 'wpFilterEnabled' ) );
		$newFilter->setSuppressed( $request->getCheck( 'wpFilterSuppressed' ) );
		$newFilter->setHidden( $request->getCheck( 'wpFilterHidden' ) );
		$newFilter->setProtected( $request->getCheck( 'wpFilterProtected' )
			|| $origFilter->isProtected() );
		$newFilter->setGlobal( $request->getCheck( 'wpFilterGlobal' )
			&& $this->getConfig()->get( 'AbuseFilterIsCentral' ) );

		$actions = $this->loadActions();

		$newFilter->setActions( $actions );

		return [ $newFilter, $origFilter ];
	}

	private function loadImportRequest(): ?Filter {
		$request = $this->getRequest();
		if ( !$request->wasPosted() ) {
			// Sanity
			throw new LogicException( __METHOD__ . ' called without the request being POSTed.' );
		}

		try {
			$filter = $this->filterImporter->decodeData( $request->getVal( 'wpImportText' ) );
		} catch ( InvalidImportDataException ) {
			return null;
		}

		return $filter;
	}

	/**
	 * @return array[]
	 */
	private function loadActions(): array {
		$request = $this->getRequest();
		$allActions = $this->consequencesRegistry->getAllEnabledActionNames();
		$actions = [];
		foreach ( $allActions as $action ) {
			// Check if it's set
			$enabled = $request->getCheck( 'wpFilterAction' . ucfirst( $action ) );

			if ( $enabled ) {
				$parameters = [];

				if ( $action === 'throttle' ) {
					// We need to load the parameters
					$throttleCount = $request->getIntOrNull( 'wpFilterThrottleCount' );
					$throttlePeriod = $request->getIntOrNull( 'wpFilterThrottlePeriod' );
					// First explode with \n, which is the delimiter used in the textarea
					$rawGroups = explode( "\n", $request->getText( 'wpFilterThrottleGroups' ) );
					// Trim any space, both as an actual group and inside subgroups
					$throttleGroups = [];
					foreach ( $rawGroups as $group ) {
						$group = preg_replace( '/\s*,\s*/', ',', trim( $group ) );
						if ( $group !== '' ) {
							$throttleGroups[] = $group;
						}
					}

					$parameters = [
						$this->filter,
						"$throttleCount,$throttlePeriod",
						...$throttleGroups,
					];
				} elseif ( $action === 'warn' ) {
					$specMsg = $request->getVal( 'wpFilterWarnMessage' );

					if ( $specMsg === 'other' ) {
						$specMsg = $request->getVal( 'wpFilterWarnMessageOther' );
					}

					$parameters = [ $specMsg ];
				} elseif ( $action === 'block' ) {
					$parameters = [
						// TODO: Should save a boolean
						$request->getCheck( 'wpFilterBlockTalk' )
							? 'blocktalk' : 'noTalkBlockSet',
						$request->getVal( 'wpBlockAnonDuration' ),
						$request->getVal( 'wpBlockUserDuration' ),
					];
				} elseif ( $action === 'disallow' ) {
					$specMsg = $request->getVal( 'wpFilterDisallowMessage' );

					if ( $specMsg === 'other' ) {
						$specMsg = $request->getVal( 'wpFilterDisallowMessageOther' );
					}

					$parameters = [ $specMsg ];
				} elseif ( $action === 'tag' ) {
					$tags = trim( $request->getText( 'wpFilterTags' ) );
					// Since it's not possible to manually add an empty tag, this only happens
					// if the form is submitted without touching the tag input field.
					// We pass an empty array so that the widget won't show an empty tag in the topbar
					if ( $tags !== '' ) {
						$parameters = explode( ',', $tags );
					}
				}

				$actions[$action] = $parameters;
			}
		}
		return $actions;
	}

	/**
	 * Exports the default warning and disallow messages to a JS variable
	 */
	private function exposeMessages() {
		$this->getOutput()->addJsConfigVars(
			'wgAbuseFilterDefaultWarningMessage',
			$this->getConfig()->get( 'AbuseFilterDefaultWarningMessage' )
		);
		$this->getOutput()->addJsConfigVars(
			'wgAbuseFilterDefaultDisallowMessage',
			$this->getConfig()->get( 'AbuseFilterDefaultDisallowMessage' )
		);
	}
}
