summaryrefslogtreecommitdiff
path: root/www/wiki/extensions/AbuseFilter/includes/Views
diff options
context:
space:
mode:
Diffstat (limited to 'www/wiki/extensions/AbuseFilter/includes/Views')
-rw-r--r--www/wiki/extensions/AbuseFilter/includes/Views/AbuseFilterView.php112
-rw-r--r--www/wiki/extensions/AbuseFilter/includes/Views/AbuseFilterViewDiff.php387
-rw-r--r--www/wiki/extensions/AbuseFilter/includes/Views/AbuseFilterViewEdit.php1252
-rw-r--r--www/wiki/extensions/AbuseFilter/includes/Views/AbuseFilterViewExamine.php219
-rw-r--r--www/wiki/extensions/AbuseFilter/includes/Views/AbuseFilterViewHistory.php85
-rw-r--r--www/wiki/extensions/AbuseFilter/includes/Views/AbuseFilterViewImport.php24
-rw-r--r--www/wiki/extensions/AbuseFilter/includes/Views/AbuseFilterViewList.php267
-rw-r--r--www/wiki/extensions/AbuseFilter/includes/Views/AbuseFilterViewRevert.php299
-rw-r--r--www/wiki/extensions/AbuseFilter/includes/Views/AbuseFilterViewTestBatch.php207
-rw-r--r--www/wiki/extensions/AbuseFilter/includes/Views/AbuseFilterViewTools.php56
10 files changed, 2908 insertions, 0 deletions
diff --git a/www/wiki/extensions/AbuseFilter/includes/Views/AbuseFilterView.php b/www/wiki/extensions/AbuseFilter/includes/Views/AbuseFilterView.php
new file mode 100644
index 00000000..11b06bbf
--- /dev/null
+++ b/www/wiki/extensions/AbuseFilter/includes/Views/AbuseFilterView.php
@@ -0,0 +1,112 @@
+<?php
+
+use Wikimedia\Rdbms\IDatabase;
+
+abstract class AbuseFilterView extends ContextSource {
+ public $mFilter, $mHistoryID, $mSubmit;
+
+ /**
+ * @var \MediaWiki\Linker\LinkRenderer
+ */
+ protected $linkRenderer;
+
+ /**
+ * @param SpecialAbuseFilter $page
+ * @param array $params
+ */
+ function __construct( $page, $params ) {
+ $this->mPage = $page;
+ $this->mParams = $params;
+ $this->setContext( $this->mPage->getContext() );
+ $this->linkRenderer = $page->getLinkRenderer();
+ }
+
+ /**
+ * @param string $subpage
+ * @return Title
+ */
+ function getTitle( $subpage = '' ) {
+ return $this->mPage->getPageTitle( $subpage );
+ }
+
+ abstract function show();
+
+ /**
+ * @return bool
+ */
+ public function canEdit() {
+ return (
+ !$this->getUser()->isBlocked() &&
+ $this->getUser()->isAllowed( 'abusefilter-modify' )
+ );
+ }
+
+ /**
+ * @return bool
+ */
+ public function canEditGlobal() {
+ return $this->getUser()->isAllowed( 'abusefilter-modify-global' );
+ }
+
+ /**
+ * Whether the user can edit the given filter.
+ *
+ * @param object $row Filter row
+ *
+ * @return bool
+ */
+ public function canEditFilter( $row ) {
+ return (
+ $this->canEdit() &&
+ !( isset( $row->af_global ) && $row->af_global == 1 && !$this->canEditGlobal() )
+ );
+ }
+
+ /**
+ * @param IDatabase $db
+ * @return string
+ */
+ public function buildTestConditions( IDatabase $db ) {
+ // If one of these is true, we're abusefilter compatible.
+ return $db->makeList( [
+ 'rc_source' => [
+ RecentChange::SRC_EDIT,
+ RecentChange::SRC_NEW,
+ ],
+ $db->makeList( [
+ 'rc_source' => RecentChange::SRC_LOG,
+ $db->makeList( [
+ $db->makeList( [
+ 'rc_log_type' => 'move',
+ 'rc_log_action' => 'move'
+ ], LIST_AND ),
+ $db->makeList( [
+ 'rc_log_type' => 'newusers',
+ 'rc_log_action' => 'create'
+ ], LIST_AND ),
+ $db->makeList( [
+ 'rc_log_type' => 'delete',
+ 'rc_log_action' => 'delete'
+ ], LIST_AND ),
+ // @todo: add upload
+ ], LIST_OR ),
+ ], LIST_AND ),
+ ], LIST_OR );
+ }
+
+ /**
+ * @static
+ * @return bool
+ */
+ static function canViewPrivate() {
+ global $wgUser;
+ static $canView = null;
+
+ if ( is_null( $canView ) ) {
+ $canView = $wgUser->isAllowedAny( 'abusefilter-modify', 'abusefilter-view-private' );
+ }
+
+ return $canView;
+ }
+
+}
diff --git a/www/wiki/extensions/AbuseFilter/includes/Views/AbuseFilterViewDiff.php b/www/wiki/extensions/AbuseFilter/includes/Views/AbuseFilterViewDiff.php
new file mode 100644
index 00000000..4a3b1881
--- /dev/null
+++ b/www/wiki/extensions/AbuseFilter/includes/Views/AbuseFilterViewDiff.php
@@ -0,0 +1,387 @@
+<?php
+
+class AbuseFilterViewDiff extends AbuseFilterView {
+ public $mOldVersion = null;
+ public $mNewVersion = null;
+ public $mNextHistoryId = null;
+ public $mFilter = null;
+
+ function show() {
+ $show = $this->loadData();
+ $out = $this->getOutput();
+
+ $links = [];
+ if ( $this->mFilter ) {
+ $links['abusefilter-history-backedit'] = $this->getTitle( $this->mFilter );
+ $links['abusefilter-diff-backhistory'] = $this->getTitle( 'history/' . $this->mFilter );
+ }
+
+ foreach ( $links as $msg => $title ) {
+ $links[$msg] = $this->linkRenderer->makeLink( $title, $this->msg( $msg )->text() );
+ }
+
+ $backlinks = $this->getLanguage()->pipeList( $links );
+ $out->addHTML( Xml::tags( 'p', null, $backlinks ) );
+
+ if ( $show ) {
+ $out->addHTML( $this->formatDiff() );
+
+ // Next and previous change links
+ $links = [];
+ if ( AbuseFilter::getFirstFilterChange( $this->mFilter ) !=
+ $this->mOldVersion['meta']['history_id']
+ ) {
+ // Create a "previous change" link if this isn't the first change of the given filter
+ $links[] = $this->linkRenderer->makeLink(
+ $this->getTitle(
+ 'history/' . $this->mFilter . '/diff/prev/' . $this->mOldVersion['meta']['history_id']
+ ),
+ $this->getLanguage()->getArrow( 'backwards' ) .
+ ' ' . $this->msg( 'abusefilter-diff-prev' )->text()
+ );
+ }
+
+ if ( !is_null( $this->mNextHistoryId ) ) {
+ // Create a "next change" link if this isn't the last change of the given filter
+ $links[] = $this->linkRenderer->makeLink(
+ $this->getTitle(
+ 'history/' . $this->mFilter . '/diff/prev/' . $this->mNextHistoryId
+ ),
+ $this->msg( 'abusefilter-diff-next' )->text() .
+ ' ' . $this->getLanguage()->getArrow( 'forwards' )
+ );
+ }
+
+ if ( count( $links ) > 0 ) {
+ $backlinks = $this->getLanguage()->pipeList( $links );
+ $out->addHTML( Xml::tags( 'p', null, $backlinks ) );
+ }
+ }
+ }
+
+ function loadData() {
+ $oldSpec = $this->mParams[3];
+ $newSpec = $this->mParams[4];
+ $this->mFilter = $this->mParams[1];
+
+ if ( AbuseFilter::filterHidden( $this->mFilter )
+ && !$this->getUser()->isAllowedAny( 'abusefilter-modify', 'abusefilter-view-private' )
+ ) {
+ $this->getOutput()->addWikiMsg( 'abusefilter-history-error-hidden' );
+ return false;
+ }
+
+ $this->mOldVersion = $this->loadSpec( $oldSpec, $newSpec );
+ $this->mNewVersion = $this->loadSpec( $newSpec, $oldSpec );
+
+ if ( is_null( $this->mOldVersion ) || is_null( $this->mNewVersion ) ) {
+ $this->getOutput()->addWikiMsg( 'abusefilter-diff-invalid' );
+ return false;
+ }
+
+ $this->mNextHistoryId = $this->getNextHistoryId(
+ $this->mNewVersion['meta']['history_id']
+ );
+
+ return true;
+ }
+
+ /**
+ * Get the history ID of the next change
+ *
+ * @param int $historyId History id to find next change of
+ * @return int|null Id of the next change or null if there isn't one
+ */
+ function getNextHistoryId( $historyId ) {
+ $dbr = wfGetDB( DB_REPLICA );
+ $row = $dbr->selectRow(
+ 'abuse_filter_history',
+ 'afh_id',
+ [
+ 'afh_filter' => $this->mFilter,
+ 'afh_id > ' . $dbr->addQuotes( $historyId ),
+ ],
+ __METHOD__,
+ [ 'ORDER BY' => 'afh_timestamp ASC' ]
+ );
+ if ( $row ) {
+ return $row->afh_id;
+ }
+ return null;
+ }
+
+ function loadSpec( $spec, $otherSpec ) {
+ static $dependentSpecs = [ 'prev', 'next' ];
+ static $cache = [];
+
+ if ( isset( $cache[$spec] ) ) {
+ return $cache[$spec];
+ }
+
+ $dbr = wfGetDB( DB_REPLICA );
+ $row = null;
+ if ( is_numeric( $spec ) ) {
+ $row = $dbr->selectRow(
+ 'abuse_filter_history',
+ '*',
+ [ 'afh_id' => $spec, 'afh_filter' => $this->mFilter ],
+ __METHOD__
+ );
+ } elseif ( $spec == 'cur' ) {
+ $row = $dbr->selectRow(
+ 'abuse_filter_history',
+ '*',
+ [ 'afh_filter' => $this->mFilter ],
+ __METHOD__,
+ [ 'ORDER BY' => 'afh_timestamp desc' ]
+ );
+ } elseif ( $spec == 'prev' && !in_array( $otherSpec, $dependentSpecs ) ) {
+ // cached
+ $other = $this->loadSpec( $otherSpec, $spec );
+
+ $row = $dbr->selectRow(
+ 'abuse_filter_history',
+ '*',
+ [
+ 'afh_filter' => $this->mFilter,
+ 'afh_id<' . $dbr->addQuotes( $other['meta']['history_id'] ),
+ ],
+ __METHOD__,
+ [ 'ORDER BY' => 'afh_timestamp desc' ]
+ );
+ if ( $other && !$row ) {
+ $t = $this->getTitle(
+ 'history/' . $this->mFilter . '/item/' . $other['meta']['history_id'] );
+ $this->getOutput()->redirect( $t->getFullURL() );
+ return null;
+ }
+ } elseif ( $spec == 'next' && !in_array( $otherSpec, $dependentSpecs ) ) {
+ // cached
+ $other = $this->loadSpec( $otherSpec, $spec );
+
+ $row = $dbr->selectRow(
+ 'abuse_filter_history',
+ '*',
+ [
+ 'afh_filter' => $this->mFilter,
+ 'afh_id>' . $dbr->addQuotes( $other['meta']['history_id'] ),
+ ],
+ __METHOD__,
+ [ 'ORDER BY' => 'afh_timestamp ASC' ]
+ );
+
+ if ( $other && !$row ) {
+ $t = $this->getTitle(
+ 'history/' . $this->mFilter . '/item/' . $other['meta']['history_id'] );
+ $this->getOutput()->redirect( $t->getFullURL() );
+ return null;
+ }
+ }
+
+ if ( !$row ) {
+ return null;
+ }
+
+ $data = $this->loadFromHistoryRow( $row );
+ $cache[$spec] = $data;
+ return $data;
+ }
+
+ function loadFromHistoryRow( $row ) {
+ return [
+ 'meta' => [
+ 'history_id' => $row->afh_id,
+ 'modified_by' => $row->afh_user,
+ 'modified_by_text' => $row->afh_user_text,
+ 'modified' => $row->afh_timestamp,
+ ],
+ 'info' => [
+ 'description' => $row->afh_public_comments,
+ 'flags' => $row->afh_flags,
+ 'notes' => $row->afh_comments,
+ 'group' => $row->afh_group,
+ ],
+ 'pattern' => $row->afh_pattern,
+ 'actions' => unserialize( $row->afh_actions ),
+ ];
+ }
+
+ /**
+ * @param string $timestamp
+ * @param int $history_id
+ * @return string
+ */
+ function formatVersionLink( $timestamp, $history_id ) {
+ $filter = $this->mFilter;
+ $text = $this->getLanguage()->timeanddate( $timestamp, true );
+ $title = $this->getTitle( "history/$filter/item/$history_id" );
+
+ $link = $this->linkRenderer->makeLink( $title, $text );
+
+ return $link;
+ }
+
+ /**
+ * @return string
+ */
+ function formatDiff() {
+ $oldVersion = $this->mOldVersion;
+ $newVersion = $this->mNewVersion;
+
+ // headings
+ $oldLink = $this->formatVersionLink(
+ $oldVersion['meta']['modified'],
+ $oldVersion['meta']['history_id']
+ );
+ $newLink = $this->formatVersionLink(
+ $newVersion['meta']['modified'],
+ $newVersion['meta']['history_id']
+ );
+
+ $oldUserLink = Linker::userLink(
+ $oldVersion['meta']['modified_by'],
+ $oldVersion['meta']['modified_by_text']
+ );
+ $newUserLink = Linker::userLink(
+ $newVersion['meta']['modified_by'],
+ $newVersion['meta']['modified_by_text']
+ );
+
+ $headings = '';
+ $headings .= Xml::tags( 'th', null,
+ $this->msg( 'abusefilter-diff-item' )->parse() );
+ $headings .= Xml::tags( 'th', null,
+ $this->msg( 'abusefilter-diff-version' )
+ ->rawParams( $oldLink, $oldUserLink )
+ ->params( $newVersion['meta']['modified_by_text'] )
+ ->parse()
+ );
+ $headings .= Xml::tags( 'th', null,
+ $this->msg( 'abusefilter-diff-version' )
+ ->rawParams( $newLink, $newUserLink )
+ ->params( $newVersion['meta']['modified_by_text'] )
+ ->parse()
+ );
+
+ $headings = Xml::tags( 'tr', null, $headings );
+
+ // Basic info
+ $info = '';
+ $info .= $this->getHeaderRow( 'abusefilter-diff-info' );
+ $info .= $this->getDiffRow(
+ 'abusefilter-edit-description',
+ $oldVersion['info']['description'],
+ $newVersion['info']['description']
+ );
+ global $wgAbuseFilterValidGroups;
+ if (
+ count( $wgAbuseFilterValidGroups ) > 1 ||
+ $oldVersion['info']['group'] != $newVersion['info']['group']
+ ) {
+ $info .= $this->getDiffRow(
+ 'abusefilter-edit-group',
+ AbuseFilter::nameGroup( $oldVersion['info']['group'] ),
+ AbuseFilter::nameGroup( $newVersion['info']['group'] )
+ );
+ }
+ $info .= $this->getDiffRow(
+ 'abusefilter-edit-flags',
+ AbuseFilter::formatFlags( $oldVersion['info']['flags'] ),
+ AbuseFilter::formatFlags( $newVersion['info']['flags'] )
+ );
+
+ $info .= $this->getDiffRow(
+ 'abusefilter-edit-notes',
+ $oldVersion['info']['notes'],
+ $newVersion['info']['notes']
+ );
+
+ // Pattern
+ $info .= $this->getHeaderRow( 'abusefilter-diff-pattern' );
+ $info .= $this->getDiffRow(
+ 'abusefilter-edit-rules',
+ $oldVersion['pattern'],
+ $newVersion['pattern'],
+ 'text'
+ );
+
+ // Actions
+ $oldActions = $this->stringifyActions( $oldVersion['actions'] );
+ $newActions = $this->stringifyActions( $newVersion['actions'] );
+
+ $info .= $this->getHeaderRow( 'abusefilter-edit-consequences' );
+ $info .= $this->getDiffRow(
+ 'abusefilter-edit-consequences',
+ $oldActions,
+ $newActions
+ );
+
+ $html = "<table class='wikitable'>
+ <thead>$headings</thead>
+ <tbody>$info</tbody>
+ </table>";
+
+ $html = Xml::tags( 'h2', null, $this->msg( 'abusefilter-diff-title' )->parse() ) . $html;
+
+ return $html;
+ }
+
+ /**
+ * @param array $actions
+ * @return array
+ */
+ function stringifyActions( $actions ) {
+ $lines = [];
+
+ ksort( $actions );
+ foreach ( $actions as $action => $parameters ) {
+ $lines[] = AbuseFilter::formatAction( $action, $parameters );
+ }
+
+ if ( !count( $lines ) ) {
+ $lines[] = '';
+ }
+
+ return $lines;
+ }
+
+ /**
+ * @param string $msg
+ * @return string
+ */
+ function getHeaderRow( $msg ) {
+ $html = $this->msg( $msg )->parse();
+ $html = Xml::tags( 'th', [ 'colspan' => 3 ], $html );
+ $html = Xml::tags( 'tr', [ 'class' => 'mw-abusefilter-diff-header' ], $html );
+
+ return $html;
+ }
+
+ /**
+ * @param string $msg
+ * @param array|string $old
+ * @param array|string $new
+ * @return string
+ */
+ function getDiffRow( $msg, $old, $new ) {
+ if ( !is_array( $old ) ) {
+ $old = explode( "\n", preg_replace( "/\\\r\\\n?/", "\n", $old ) );
+ }
+ if ( !is_array( $new ) ) {
+ $new = explode( "\n", preg_replace( "/\\\r\\\n?/", "\n", $new ) );
+ }
+
+ $diffEngine = new DifferenceEngine( $this->getContext() );
+
+ $diffEngine->showDiffStyle();
+
+ $diff = new Diff( $old, $new );
+ $formatter = new TableDiffFormatterFullContext();
+ $formattedDiff = $diffEngine->addHeader( $formatter->format( $diff ), '', '' );
+
+ return Xml::tags( 'tr', null,
+ Xml::tags( 'th', null, $this->msg( $msg )->parse() ) .
+ Xml::tags( 'td', [ 'colspan' => 2 ], $formattedDiff )
+ ) . "\n";
+ }
+}
diff --git a/www/wiki/extensions/AbuseFilter/includes/Views/AbuseFilterViewEdit.php b/www/wiki/extensions/AbuseFilter/includes/Views/AbuseFilterViewEdit.php
new file mode 100644
index 00000000..9f4db06e
--- /dev/null
+++ b/www/wiki/extensions/AbuseFilter/includes/Views/AbuseFilterViewEdit.php
@@ -0,0 +1,1252 @@
+<?php
+
+class AbuseFilterViewEdit extends AbuseFilterView {
+ /**
+ * @param SpecialAbuseFilter $page
+ * @param array $params
+ */
+ function __construct( $page, $params ) {
+ parent::__construct( $page, $params );
+ $this->mFilter = $page->mFilter;
+ $this->mHistoryID = $page->mHistoryID;
+ }
+
+ /**
+ * Check whether a filter is allowed to use a tag
+ *
+ * @param string $tag Tag name
+ * @return Status
+ */
+ protected function isAllowedTag( $tag ) {
+ $tagNameStatus = ChangeTags::isTagNameValid( $tag );
+
+ if ( !$tagNameStatus->isGood() ) {
+ return $tagNameStatus;
+ }
+
+ $finalStatus = Status::newGood();
+
+ $canAddStatus =
+ ChangeTags::canAddTagsAccompanyingChange(
+ [ $tag ]
+ );
+
+ if ( $canAddStatus->isGood() ) {
+ return $finalStatus;
+ }
+
+ $alreadyDefinedTags = [];
+ AbuseFilterHooks::onListDefinedTags( $alreadyDefinedTags );
+
+ if ( in_array( $tag, $alreadyDefinedTags, true ) ) {
+ return $finalStatus;
+ }
+
+ $canCreateTagStatus = ChangeTags::canCreateTag( $tag );
+ if ( $canCreateTagStatus->isGood() ) {
+ return $finalStatus;
+ }
+
+ $finalStatus->fatal( 'abusefilter-edit-bad-tags' );
+ return $finalStatus;
+ }
+
+ function show() {
+ $user = $this->getUser();
+ $out = $this->getOutput();
+ $request = $this->getRequest();
+ $out->setPageTitle( $this->msg( 'abusefilter-edit' ) );
+ $out->addHelpLink( 'Extension:AbuseFilter/Rules format' );
+
+ $filter = $this->mFilter;
+ $history_id = $this->mHistoryID;
+ if ( $this->mHistoryID ) {
+ $dbr = wfGetDB( DB_REPLICA );
+ $row = $dbr->selectRow(
+ 'abuse_filter_history',
+ 'afh_id',
+ [
+ 'afh_filter' => $filter,
+ ],
+ __METHOD__,
+ [ 'ORDER BY' => 'afh_timestamp DESC' ]
+ );
+ // change $history_id to null if it's current version id
+ if ( $row->afh_id === $this->mHistoryID ) {
+ $history_id = null;
+ }
+ }
+
+ // Add default warning messages
+ $this->exposeWarningMessages();
+
+ if ( $filter == 'new' && !$this->canEdit() ) {
+ $out->addWikiMsg( 'abusefilter-edit-notallowed' );
+ return;
+ }
+
+ $editToken = $request->getVal( 'wpEditToken' );
+ $tokenMatches = $user->matchEditToken(
+ $editToken, [ 'abusefilter', $filter ], $request );
+
+ if ( $tokenMatches && $this->canEdit() ) {
+ // Check syntax
+ $syntaxerr = AbuseFilter::checkSyntax( $request->getVal( 'wpFilterRules' ) );
+ if ( $syntaxerr !== true ) {
+ $out->addHTML(
+ $this->buildFilterEditor(
+ $this->msg(
+ 'abusefilter-edit-badsyntax',
+ [ $syntaxerr[0] ]
+ )->parseAsBlock(),
+ $filter, $history_id
+ )
+ );
+ return;
+ }
+ // Check for missing required fields (title and pattern)
+ $missing = [];
+ if ( !$request->getVal( 'wpFilterRules' ) ||
+ trim( $request->getVal( 'wpFilterRules' ) ) === '' ) {
+ $missing[] = $this->msg( 'abusefilter-edit-field-conditions' )->escaped();
+ }
+ if ( !$request->getVal( 'wpFilterDescription' ) ) {
+ $missing[] = $this->msg( 'abusefilter-edit-field-description' )->escaped();
+ }
+ if ( count( $missing ) !== 0 ) {
+ $missing = $this->getLanguage()->commaList( $missing );
+ $out->addHTML(
+ $this->buildFilterEditor(
+ $this->msg(
+ 'abusefilter-edit-missingfields',
+ $missing
+ )->parseAsBlock(),
+ $filter, $history_id
+ )
+ );
+ return;
+ }
+
+ $dbw = wfGetDB( DB_MASTER );
+
+ list( $newRow, $actions ) = $this->loadRequest( $filter );
+
+ $differences = AbuseFilter::compareVersions(
+ [ $newRow, $actions ],
+ [ $newRow->mOriginalRow, $newRow->mOriginalActions ]
+ );
+
+ // Don't allow adding a new global rule, or updating a
+ // rule that is currently global, without permissions.
+ if ( !$this->canEditFilter( $newRow ) || !$this->canEditFilter( $newRow->mOriginalRow ) ) {
+ $out->addWikiMsg( 'abusefilter-edit-notallowed-global' );
+ return;
+ }
+
+ // Don't allow custom messages on global rules
+ if ( $newRow->af_global == 1 &&
+ $request->getVal( 'wpFilterWarnMessage' ) !== 'abusefilter-warning'
+ ) {
+ $out->addWikiMsg( 'abusefilter-edit-notallowed-global-custom-msg' );
+ return;
+ }
+
+ $origActions = $newRow->mOriginalActions;
+ $wasGlobal = (bool)$newRow->mOriginalRow->af_global;
+
+ unset( $newRow->mOriginalRow );
+ unset( $newRow->mOriginalActions );
+
+ // Check for non-changes
+ if ( !count( $differences ) ) {
+ $out->redirect( $this->getTitle()->getLocalURL() );
+ return;
+ }
+
+ // Check for restricted actions
+ global $wgAbuseFilterRestrictions;
+ if ( count( array_intersect_key(
+ array_filter( $wgAbuseFilterRestrictions ),
+ array_merge(
+ array_filter( $actions ),
+ array_filter( $origActions )
+ )
+ ) )
+ && !$user->isAllowed( 'abusefilter-modify-restricted' )
+ ) {
+ $out->addHTML(
+ $this->buildFilterEditor(
+ $this->msg( 'abusefilter-edit-restricted' )->parseAsBlock(),
+ $this->mFilter,
+ $history_id
+ )
+ );
+ return;
+ }
+
+ // If we've activated the 'tag' option, check the arguments for validity.
+ if ( !empty( $actions['tag'] ) ) {
+ foreach ( $actions['tag']['parameters'] as $tag ) {
+ $status = $this->isAllowedTag( $tag );
+
+ if ( !$status->isGood() ) {
+ $out->addHTML(
+ $this->buildFilterEditor(
+ $status->getMessage()->parseAsBlock(),
+ $this->mFilter,
+ $history_id
+ )
+ );
+ return;
+ }
+ }
+ }
+
+ $newRow = get_object_vars( $newRow ); // Convert from object to array
+
+ // Set last modifier.
+ $newRow['af_timestamp'] = $dbw->timestamp( wfTimestampNow() );
+ $newRow['af_user'] = $user->getId();
+ $newRow['af_user_text'] = $user->getName();
+
+ $dbw->startAtomic( __METHOD__ );
+
+ // Insert MAIN row.
+ if ( $filter == 'new' ) {
+ $new_id = $dbw->nextSequenceValue( 'abuse_filter_af_id_seq' );
+ $is_new = true;
+ } else {
+ $new_id = $this->mFilter;
+ $is_new = false;
+ }
+
+ // Reset throttled marker, if we're re-enabling it.
+ $newRow['af_throttled'] = $newRow['af_throttled'] && !$newRow['af_enabled'];
+ $newRow['af_id'] = $new_id; // ID.
+
+ // T67807
+ // integer 1's & 0's might be better understood than booleans
+ $newRow['af_enabled'] = (int)$newRow['af_enabled'];
+ $newRow['af_hidden'] = (int)$newRow['af_hidden'];
+ $newRow['af_throttled'] = (int)$newRow['af_throttled'];
+ $newRow['af_deleted'] = (int)$newRow['af_deleted'];
+ $newRow['af_global'] = (int)$newRow['af_global'];
+
+ $dbw->replace( 'abuse_filter', [ 'af_id' ], $newRow, __METHOD__ );
+
+ if ( $is_new ) {
+ $new_id = $dbw->insertId();
+ }
+
+ // Actions
+ global $wgAbuseFilterActions;
+ $deadActions = [];
+ $actionsRows = [];
+ foreach ( array_filter( $wgAbuseFilterActions ) as $action => $_ ) {
+ // Check if it's set
+ $enabled = isset( $actions[$action] ) && (bool)$actions[$action];
+
+ if ( $enabled ) {
+ $parameters = $actions[$action]['parameters'];
+
+ $thisRow = [
+ 'afa_filter' => $new_id,
+ 'afa_consequence' => $action,
+ 'afa_parameters' => implode( "\n", $parameters )
+ ];
+ $actionsRows[] = $thisRow;
+ } else {
+ $deadActions[] = $action;
+ }
+ }
+
+ // Create a history row
+ $afh_row = [];
+
+ foreach ( AbuseFilter::$history_mappings as $af_col => $afh_col ) {
+ $afh_row[$afh_col] = $newRow[$af_col];
+ }
+
+ // Actions
+ $displayActions = [];
+ foreach ( $actions as $action ) {
+ $displayActions[$action['action']] = $action['parameters'];
+ }
+ $afh_row['afh_actions'] = serialize( $displayActions );
+
+ $afh_row['afh_changed_fields'] = implode( ',', $differences );
+
+ // Flags
+ $flags = [];
+ if ( $newRow['af_hidden'] ) {
+ $flags[] = 'hidden';
+ }
+ if ( $newRow['af_enabled'] ) {
+ $flags[] = 'enabled';
+ }
+ if ( $newRow['af_deleted'] ) {
+ $flags[] = 'deleted';
+ }
+ if ( $newRow['af_global'] ) {
+ $flags[] = 'global';
+ }
+
+ $afh_row['afh_flags'] = implode( ',', $flags );
+
+ $afh_row['afh_filter'] = $new_id;
+ $afh_row['afh_id'] = $dbw->nextSequenceValue( 'abuse_filter_af_id_seq' );
+
+ // Do the update
+ $dbw->insert( 'abuse_filter_history', $afh_row, __METHOD__ );
+ $history_id = $dbw->insertId();
+ if ( $filter != 'new' ) {
+ $dbw->delete(
+ 'abuse_filter_action',
+ [ 'afa_filter' => $filter ],
+ __METHOD__
+ );
+ }
+ $dbw->insert( 'abuse_filter_action', $actionsRows, __METHOD__ );
+
+ $dbw->endAtomic( __METHOD__ );
+
+ // Invalidate cache if this was a global rule
+ if ( $wasGlobal || $newRow['af_global'] ) {
+ $group = 'default';
+ if ( isset( $newRow['af_group'] ) && $newRow['af_group'] != '' ) {
+ $group = $newRow['af_group'];
+ }
+
+ $globalRulesKey = AbuseFilter::getGlobalRulesKey( $group );
+ ObjectCache::getMainWANInstance()->touchCheckKey( $globalRulesKey );
+ }
+
+ // Logging
+ $subtype = $filter === 'new' ? 'create' : 'modify';
+ $logEntry = new ManualLogEntry( 'abusefilter', $subtype );
+ $logEntry->setPerformer( $user );
+ $logEntry->setTarget( $this->getTitle( $new_id ) );
+ $logEntry->setParameters( [
+ 'historyId' => $history_id,
+ 'newId' => $new_id
+ ] );
+ $logid = $logEntry->insert();
+ $logEntry->publish( $logid );
+
+ // Purge the tag list cache so the fetchAllTags hook applies tag changes
+ if ( isset( $actions['tag'] ) ) {
+ AbuseFilterHooks::purgeTagCache();
+ }
+
+ AbuseFilter::resetFilterProfile( $new_id );
+
+ $out->redirect(
+ $this->getTitle()->getLocalURL(
+ [
+ 'result' => 'success',
+ 'changedfilter' => $new_id,
+ 'changeid' => $history_id,
+ ]
+ )
+ );
+ } else {
+ if ( $tokenMatches ) {
+ // lost rights meanwhile
+ $out->addWikiMsg( 'abusefilter-edit-notallowed' );
+ }
+
+ if ( $history_id ) {
+ $out->addWikiMsg(
+ 'abusefilter-edit-oldwarning', $history_id, $filter );
+ }
+
+ $out->addHTML( $this->buildFilterEditor( null, $filter, $history_id ) );
+
+ if ( $history_id ) {
+ $out->addWikiMsg(
+ 'abusefilter-edit-oldwarning', $history_id, $filter );
+ }
+ }
+ }
+
+ /**
+ * Builds the full form for edit filters.
+ * Loads data either from the database or from the HTTP request.
+ * The request takes precedence over the database
+ * @param string $error An error message to show above the filter box.
+ * @param int $filter The filter ID
+ * @param int $history_id The history ID of the filter, if applicable. Otherwise null
+ * @return bool|string False if there is a failure building the editor,
+ * otherwise the HTML text for the editor.
+ */
+ function buildFilterEditor( $error, $filter, $history_id = null ) {
+ if ( $filter === null ) {
+ return false;
+ }
+
+ // Build the edit form
+ $out = $this->getOutput();
+ $lang = $this->getLanguage();
+ $user = $this->getUser();
+
+ // Load from request OR database.
+ list( $row, $actions ) = $this->loadRequest( $filter, $history_id );
+
+ if ( !$row ) {
+ $out->addWikiMsg( 'abusefilter-edit-badfilter' );
+ $out->addHTML( $this->linkRenderer->makeLink( $this->getTitle(),
+ $this->msg( 'abusefilter-return' )->text() ) );
+ return false;
+ }
+
+ $out->addSubtitle( $this->msg(
+ $filter === 'new' ? 'abusefilter-edit-subtitle-new' : 'abusefilter-edit-subtitle',
+ $this->getLanguage()->formatNum( $filter ), $history_id
+ )->parse() );
+
+ // Hide hidden filters.
+ if ( ( ( isset( $row->af_hidden ) && $row->af_hidden ) ||
+ AbuseFilter::filterHidden( $filter ) )
+ && !$this->canViewPrivate() ) {
+ return $this->msg( 'abusefilter-edit-denied' )->escaped();
+ }
+
+ $output = '';
+ if ( $error ) {
+ $out->addHTML( "<span class=\"error\">$error</span>" );
+ }
+
+ // Read-only attribute
+ $readOnlyAttrib = [];
+ $cbReadOnlyAttrib = []; // For checkboxes
+
+ $styleAttrib = [ 'style' => 'width:95%' ];
+
+ if ( !$this->canEditFilter( $row ) ) {
+ $readOnlyAttrib['readonly'] = 'readonly';
+ $cbReadOnlyAttrib['disabled'] = 'disabled';
+ }
+
+ $fields = [];
+
+ $fields['abusefilter-edit-id'] =
+ $this->mFilter == 'new' ?
+ $this->msg( 'abusefilter-edit-new' )->escaped() :
+ $lang->formatNum( $filter );
+ $fields['abusefilter-edit-description'] =
+ Xml::input(
+ 'wpFilterDescription',
+ 45,
+ isset( $row->af_public_comments ) ? $row->af_public_comments : '',
+ array_merge( $readOnlyAttrib, $styleAttrib )
+ );
+
+ global $wgAbuseFilterValidGroups;
+ if ( count( $wgAbuseFilterValidGroups ) > 1 ) {
+ $groupSelector = new XmlSelect(
+ 'wpFilterGroup',
+ 'mw-abusefilter-edit-group-input',
+ 'default'
+ );
+
+ if ( isset( $row->af_group ) && $row->af_group ) {
+ $groupSelector->setDefault( $row->af_group );
+ }
+
+ foreach ( $wgAbuseFilterValidGroups as $group ) {
+ $groupSelector->addOption( AbuseFilter::nameGroup( $group ), $group );
+ }
+
+ if ( !empty( $readOnlyAttrib ) ) {
+ $groupSelector->setAttribute( 'disabled', 'disabled' );
+ }
+
+ $fields['abusefilter-edit-group'] = $groupSelector->getHTML();
+ }
+
+ // Hit count display
+ if ( !empty( $row->af_hit_count ) && $user->isAllowed( 'abusefilter-log-detail' ) ) {
+ $count_display = $this->msg( 'abusefilter-hitcount' )
+ ->numParams( (int)$row->af_hit_count )->text();
+ $hitCount = $this->linkRenderer->makeKnownLink(
+ SpecialPage::getTitleFor( 'AbuseLog' ),
+ $count_display,
+ [],
+ [ 'wpSearchFilter' => $row->af_id ]
+ );
+
+ $fields['abusefilter-edit-hitcount'] = $hitCount;
+ }
+
+ if ( $filter !== 'new' ) {
+ // Statistics
+ global $wgAbuseFilterProfile;
+ $stash = ObjectCache::getMainStashInstance();
+ $matches_count = (int)$stash->get( AbuseFilter::filterMatchesKey( $filter ) );
+ $total = (int)$stash->get( AbuseFilter::filterUsedKey( $row->af_group ) );
+
+ if ( $total > 0 ) {
+ $matches_percent = sprintf( '%.2f', 100 * $matches_count / $total );
+ if ( $wgAbuseFilterProfile ) {
+ list( $timeProfile, $condProfile ) = AbuseFilter::getFilterProfile( $filter );
+ $fields['abusefilter-edit-status-label'] = $this->msg( 'abusefilter-edit-status-profile' )
+ ->numParams( $total, $matches_count, $matches_percent, $timeProfile, $condProfile )
+ ->escaped();
+ } else {
+ $fields['abusefilter-edit-status-label'] = $this->msg( 'abusefilter-edit-status' )
+ ->numParams( $total, $matches_count, $matches_percent )
+ ->parse();
+ }
+ }
+ }
+
+ $fields['abusefilter-edit-rules'] = AbuseFilter::buildEditBox(
+ $row->af_pattern,
+ 'wpFilterRules',
+ true,
+ $this->canEditFilter( $row )
+ );
+ $fields['abusefilter-edit-notes'] = Xml::textarea(
+ 'wpFilterNotes',
+ ( isset( $row->af_comments ) ? $row->af_comments . "\n" : "\n" ),
+ 40, 15,
+ $readOnlyAttrib
+ );
+
+ // Build checkboxes
+ $checkboxes = [ 'hidden', 'enabled', 'deleted' ];
+ $flags = '';
+
+ global $wgAbuseFilterIsCentral;
+ if ( $wgAbuseFilterIsCentral ) {
+ $checkboxes[] = 'global';
+ }
+
+ if ( isset( $row->af_throttled ) && $row->af_throttled ) {
+ global $wgAbuseFilterRestrictions;
+
+ $filterActions = explode( ',', $row->af_actions );
+ $throttledActions = array_intersect_key(
+ array_flip( $filterActions ),
+ array_filter( $wgAbuseFilterRestrictions )
+ );
+
+ if ( $throttledActions ) {
+ $throttledActions = array_map(
+ function ( $filterAction ) {
+ return $this->msg( 'abusefilter-action-' . $filterAction )->text();
+ },
+ array_keys( $throttledActions )
+ );
+
+ $flags .= $out->parse(
+ Html::warningBox(
+ $this->msg( 'abusefilter-edit-throttled-warning' )
+ ->plaintextParams( $lang->commaList( $throttledActions ) )
+ ->escaped()
+ )
+ );
+ }
+ }
+
+ foreach ( $checkboxes as $checkboxId ) {
+ // Messages that can be used here:
+ // * abusefilter-edit-enabled
+ // * abusefilter-edit-deleted
+ // * abusefilter-edit-hidden
+ // * abusefilter-edit-global
+ $message = "abusefilter-edit-$checkboxId";
+ $dbField = "af_$checkboxId";
+ $postVar = 'wpFilter' . ucfirst( $checkboxId );
+
+ if ( $checkboxId == 'global' && !$this->canEditGlobal() ) {
+ $cbReadOnlyAttrib['disabled'] = 'disabled';
+ }
+
+ $checkbox = Xml::checkLabel(
+ $this->msg( $message )->text(),
+ $postVar,
+ $postVar,
+ isset( $row->$dbField ) ? $row->$dbField : false,
+ $cbReadOnlyAttrib
+ );
+ $checkbox = Xml::tags( 'p', null, $checkbox );
+ $flags .= $checkbox;
+ }
+
+ $fields['abusefilter-edit-flags'] = $flags;
+ $tools = '';
+
+ if ( $filter != 'new' ) {
+ if ( $user->isAllowed( 'abusefilter-revert' ) ) {
+ $tools .= Xml::tags(
+ 'p', null,
+ $this->linkRenderer->makeLink(
+ $this->getTitle( "revert/$filter" ),
+ new HtmlArmor( $this->msg( 'abusefilter-edit-revert' )->parse() )
+ )
+ );
+ }
+
+ if ( $this->canEdit() ) {
+ // Test link
+ $tools .= Xml::tags(
+ 'p', null,
+ $this->linkRenderer->makeLink(
+ $this->getTitle( "test/$filter" ),
+ new HtmlArmor( $this->msg( 'abusefilter-edit-test-link' )->parse() )
+ )
+ );
+ }
+ // Last modification details
+ $userLink =
+ Linker::userLink( $row->af_user, $row->af_user_text ) .
+ Linker::userToolLinks( $row->af_user, $row->af_user_text );
+ $userName = $row->af_user_text;
+ $fields['abusefilter-edit-lastmod'] =
+ $this->msg( 'abusefilter-edit-lastmod-text' )
+ ->rawParams(
+ $lang->timeanddate( $row->af_timestamp, true ),
+ $userLink,
+ $lang->date( $row->af_timestamp, true ),
+ $lang->time( $row->af_timestamp, true ),
+ $userName
+ )->parse();
+ $history_display = new HtmlArmor( $this->msg( 'abusefilter-edit-viewhistory' )->parse() );
+ $fields['abusefilter-edit-history'] =
+ $this->linkRenderer->makeKnownLink( $this->getTitle( 'history/' . $filter ), $history_display );
+ }
+
+ // Add export
+ $exportText = FormatJson::encode( [ 'row' => $row, 'actions' => $actions ] );
+ $tools .= Xml::tags( 'a', [ 'href' => '#', 'id' => 'mw-abusefilter-export-link' ],
+ $this->msg( 'abusefilter-edit-export' )->parse() );
+ $tools .= Xml::element( 'textarea',
+ [ 'readonly' => 'readonly', 'id' => 'mw-abusefilter-export' ],
+ $exportText
+ );
+
+ $fields['abusefilter-edit-tools'] = $tools;
+
+ $form = Xml::buildForm( $fields );
+ $form = Xml::fieldset( $this->msg( 'abusefilter-edit-main' )->text(), $form );
+ $form .= Xml::fieldset(
+ $this->msg( 'abusefilter-edit-consequences' )->text(),
+ $this->buildConsequenceEditor( $row, $actions )
+ );
+
+ if ( $this->canEditFilter( $row ) ) {
+ $form .= Xml::submitButton(
+ $this->msg( 'abusefilter-edit-save' )->text(),
+ [ 'accesskey' => 's' ]
+ );
+ $form .= Html::hidden(
+ 'wpEditToken',
+ $user->getEditToken( [ 'abusefilter', $filter ] )
+ );
+ }
+
+ $form = Xml::tags( 'form',
+ [
+ 'action' => $this->getTitle( $filter )->getFullURL(),
+ 'method' => 'post'
+ ],
+ $form
+ );
+
+ $output .= $form;
+
+ return $output;
+ }
+
+ /**
+ * Builds the "actions" editor for a given filter.
+ * @param stdClass $row A row from the abuse_filter table.
+ * @param array $actions Array of rows from the abuse_filter_action table
+ * corresponding to the abuse filter held in $row.
+ * @return HTML text for an action editor.
+ */
+ function buildConsequenceEditor( $row, $actions ) {
+ global $wgAbuseFilterActions;
+
+ $enabledActions = array_filter( $wgAbuseFilterActions );
+
+ $setActions = [];
+ foreach ( $enabledActions as $action => $_ ) {
+ $setActions[$action] = array_key_exists( $action, $actions );
+ }
+
+ $output = '';
+
+ foreach ( $enabledActions as $action => $_ ) {
+ MediaWiki\suppressWarnings();
+ $params = $actions[$action]['parameters'];
+ MediaWiki\restoreWarnings();
+ $output .= $this->buildConsequenceSelector(
+ $action, $setActions[$action], $params, $row );
+ }
+
+ return $output;
+ }
+
+ /**
+ * @param string $action The action to build an editor for
+ * @param bool $set Whether or not the action is activated
+ * @param array $parameters Action parameters
+ * @param stdClass $row abuse_filter row object
+ * @return string
+ */
+ function buildConsequenceSelector( $action, $set, $parameters, $row ) {
+ global $wgAbuseFilterActions, $wgMainCacheType;
+
+ if ( empty( $wgAbuseFilterActions[$action] ) ) {
+ return '';
+ }
+
+ $readOnlyAttrib = [];
+ $cbReadOnlyAttrib = []; // For checkboxes
+
+ if ( !$this->canEditFilter( $row ) ) {
+ $readOnlyAttrib['readonly'] = 'readonly';
+ $cbReadOnlyAttrib['disabled'] = 'disabled';
+ }
+
+ switch ( $action ) {
+ case 'throttle':
+ // Throttling is only available via object caching
+ if ( $wgMainCacheType === CACHE_NONE ) {
+ return '';
+ }
+ $throttleSettings = Xml::checkLabel(
+ $this->msg( 'abusefilter-edit-action-throttle' )->text(),
+ 'wpFilterActionThrottle',
+ "mw-abusefilter-action-checkbox-$action",
+ $set,
+ [ 'class' => 'mw-abusefilter-action-checkbox' ] + $cbReadOnlyAttrib );
+ $throttleFields = [];
+
+ if ( $set ) {
+ array_shift( $parameters );
+ $throttleRate = explode( ',', $parameters[0] );
+ $throttleCount = $throttleRate[0];
+ $throttlePeriod = $throttleRate[1];
+
+ $throttleGroups = implode( "\n", array_slice( $parameters, 1 ) );
+ } else {
+ $throttleCount = 3;
+ $throttlePeriod = 60;
+
+ $throttleGroups = "user\n";
+ }
+
+ $throttleFields['abusefilter-edit-throttle-count'] =
+ Xml::input( 'wpFilterThrottleCount', 20, $throttleCount, $readOnlyAttrib );
+ $throttleFields['abusefilter-edit-throttle-period'] =
+ $this->msg( 'abusefilter-edit-throttle-seconds' )
+ ->rawParams( Xml::input( 'wpFilterThrottlePeriod', 20, $throttlePeriod,
+ $readOnlyAttrib )
+ )->parse();
+ $throttleFields['abusefilter-edit-throttle-groups'] =
+ Xml::textarea( 'wpFilterThrottleGroups', $throttleGroups . "\n",
+ 40, 5, $readOnlyAttrib );
+ $throttleSettings .=
+ Xml::tags(
+ 'div',
+ [ 'id' => 'mw-abusefilter-throttle-parameters' ],
+ Xml::buildForm( $throttleFields )
+ );
+ return $throttleSettings;
+ case 'warn':
+ global $wgAbuseFilterDefaultWarningMessage;
+ $output = '';
+ $checkbox = Xml::checkLabel(
+ $this->msg( 'abusefilter-edit-action-warn' )->text(),
+ 'wpFilterActionWarn',
+ "mw-abusefilter-action-checkbox-$action",
+ $set,
+ [ 'class' => 'mw-abusefilter-action-checkbox' ] + $cbReadOnlyAttrib );
+ $output .= Xml::tags( 'p', null, $checkbox );
+ if ( $set ) {
+ $warnMsg = $parameters[0];
+ } elseif (
+ $row &&
+ isset( $row->af_group ) && $row->af_group &&
+ isset( $wgAbuseFilterDefaultWarningMessage[$row->af_group] )
+ ) {
+ $warnMsg = $wgAbuseFilterDefaultWarningMessage[$row->af_group];
+ } else {
+ $warnMsg = 'abusefilter-warning';
+ }
+
+ $warnFields['abusefilter-edit-warn-message'] =
+ $this->getExistingSelector( $warnMsg, !empty( $readOnlyAttrib ) );
+ $warnFields['abusefilter-edit-warn-other-label'] =
+ Xml::input(
+ 'wpFilterWarnMessageOther',
+ 45,
+ $warnMsg,
+ [ 'id' => 'mw-abusefilter-warn-message-other' ] + $cbReadOnlyAttrib
+ );
+
+ $previewButton = Xml::element(
+ 'input',
+ [
+ 'type' => 'button',
+ 'id' => 'mw-abusefilter-warn-preview-button',
+ 'value' => $this->msg( 'abusefilter-edit-warn-preview' )->text()
+ ]
+ );
+ $editButton = '';
+ if ( $this->getUser()->isAllowed( 'editinterface' ) ) {
+ $editButton .= ' ' . Xml::element(
+ 'input',
+ [
+ 'type' => 'button',
+ 'id' => 'mw-abusefilter-warn-edit-button',
+ 'value' => $this->msg( 'abusefilter-edit-warn-edit' )->text()
+ ]
+ );
+ }
+ $previewHolder = Xml::element(
+ 'div',
+ [ 'id' => 'mw-abusefilter-warn-preview' ], ''
+ );
+ $warnFields['abusefilter-edit-warn-actions'] =
+ Xml::tags( 'p', null, $previewButton . $editButton ) . "\n$previewHolder";
+ $output .=
+ Xml::tags(
+ 'div',
+ [ 'id' => 'mw-abusefilter-warn-parameters' ],
+ Xml::buildForm( $warnFields )
+ );
+ return $output;
+ case 'tag':
+ if ( $set ) {
+ $tags = $parameters;
+ } else {
+ $tags = [];
+ }
+ $output = '';
+
+ $checkbox = Xml::checkLabel(
+ $this->msg( 'abusefilter-edit-action-tag' )->text(),
+ 'wpFilterActionTag',
+ "mw-abusefilter-action-checkbox-$action",
+ $set,
+ [ 'class' => 'mw-abusefilter-action-checkbox' ] + $cbReadOnlyAttrib
+ );
+ $output .= Xml::tags( 'p', null, $checkbox );
+
+ $tagFields['abusefilter-edit-tag-tag'] =
+ Xml::textarea( 'wpFilterTags', implode( "\n", $tags ), 40, 5, $readOnlyAttrib );
+ $output .=
+ Xml::tags( 'div',
+ [ 'id' => 'mw-abusefilter-tag-parameters' ],
+ Xml::buildForm( $tagFields )
+ );
+ return $output;
+ case 'block':
+ global $wgBlockAllowsUTEdit, $wgAbuseFilterBlockDuration,
+ $wgAbuseFilterAnonBlockDuration;
+
+ if ( $set && count( $parameters ) === 3 ) {
+ // Both blocktalk and custom block durations available
+ $blockTalk = $parameters[0];
+ $defaultAnonDuration = $parameters[1];
+ $defaultUserDuration = $parameters[2];
+ } else {
+ if ( $set && count( $parameters ) === 1 ) {
+ // Only blocktalk available
+ $blockTalk = $parameters[0];
+ }
+ if ( $wgAbuseFilterAnonBlockDuration ) {
+ $defaultAnonDuration = $wgAbuseFilterAnonBlockDuration;
+ } else {
+ $defaultAnonDuration = $wgAbuseFilterBlockDuration;
+ }
+ $defaultUserDuration = $wgAbuseFilterBlockDuration;
+ }
+ $suggestedBlocks = SpecialBlock::getSuggestedDurations();
+ $suggestedBlocks = self::normalizeBlocks( $suggestedBlocks );
+
+ $output = '';
+ $checkbox = Xml::checkLabel(
+ $this->msg( 'abusefilter-edit-action-block' )->text(),
+ 'wpFilterActionBlock',
+ "mw-abusefilter-action-checkbox-block",
+ $set,
+ [ 'class' => 'mw-abusefilter-action-checkbox' ] + $cbReadOnlyAttrib );
+ $output .= Xml::tags( 'p', null, $checkbox );
+ if ( $wgBlockAllowsUTEdit === true ) {
+ $talkCheckbox =
+ Xml::checkLabel(
+ $this->msg( 'abusefilter-edit-action-blocktalk' )->text(),
+ 'wpFilterBlockTalk',
+ 'mw-abusefilter-action-checkbox-blocktalk',
+ isset( $blockTalk ) && $blockTalk == 'blocktalk',
+ [ 'class' => 'mw-abusefilter-action-checkbox' ] + $cbReadOnlyAttrib
+ );
+ }
+
+ $anonDuration = new XmlSelect(
+ 'wpBlockAnonDuration',
+ false,
+ 'default'
+ );
+ $anonDuration->addOptions( $suggestedBlocks );
+
+ $userDuration = new XmlSelect(
+ 'wpBlockUserDuration',
+ false,
+ 'default'
+ );
+ $userDuration->addOptions( $suggestedBlocks );
+
+ // Set defaults
+ $anonDuration->setDefault( $defaultAnonDuration );
+ $userDuration->setDefault( $defaultUserDuration );
+
+ if ( !$this->canEditFilter( $row ) ) {
+ $anonDuration->setAttribute( 'disabled', 'disabled' );
+ $userDuration->setAttribute( 'disabled', 'disabled' );
+ }
+
+ if ( $wgBlockAllowsUTEdit === true ) {
+ $durations['abusefilter-edit-block-options'] = $talkCheckbox;
+ }
+ $durations['abusefilter-edit-block-anon-durations'] = $anonDuration->getHTML();
+ $durations['abusefilter-edit-block-user-durations'] = $userDuration->getHTML();
+
+ $rawOutput = Xml::buildForm( $durations );
+
+ $output .= Xml::tags(
+ 'div',
+ [ 'id' => 'mw-abusefilter-block-parameters' ],
+ $rawOutput
+ );
+
+ return $output;
+
+ default:
+ // Give grep a chance to find the usages:
+ // abusefilter-edit-action-warn, abusefilter-edit-action-disallow
+ // abusefilter-edit-action-blockautopromote
+ // abusefilter-edit-action-degroup, abusefilter-edit-action-throttle
+ // abusefilter-edit-action-rangeblock, abusefilter-edit-action-tag
+ $message = 'abusefilter-edit-action-' . $action;
+ $form_field = 'wpFilterAction' . ucfirst( $action );
+ $status = $set;
+
+ $thisAction = Xml::checkLabel(
+ $this->msg( $message )->text(),
+ $form_field,
+ "mw-abusefilter-action-checkbox-$action",
+ $status,
+ [ 'class' => 'mw-abusefilter-action-checkbox' ] + $cbReadOnlyAttrib
+ );
+ $thisAction = Xml::tags( 'p', null, $thisAction );
+ return $thisAction;
+ }
+ }
+
+ /**
+ * @param string $warnMsg
+ * @param bool $readOnly
+ * @return string
+ */
+ function getExistingSelector( $warnMsg, $readOnly = false ) {
+ $existingSelector = new XmlSelect(
+ 'wpFilterWarnMessage',
+ 'mw-abusefilter-warn-message-existing',
+ $warnMsg == 'abusefilter-warning' ? 'abusefilter-warning' : 'other'
+ );
+
+ $existingSelector->addOption( 'abusefilter-warning' );
+
+ if ( $readOnly ) {
+ $existingSelector->setAttribute( 'disabled', 'disabled' );
+ } else {
+ // Find other messages.
+ $dbr = wfGetDB( DB_REPLICA );
+ $res = $dbr->select(
+ 'page',
+ [ 'page_title' ],
+ [
+ 'page_namespace' => 8,
+ 'page_title LIKE ' . $dbr->addQuotes( 'Abusefilter-warning%' )
+ ],
+ __METHOD__
+ );
+
+ $lang = $this->getLanguage();
+ foreach ( $res as $row ) {
+ if ( $lang->lcfirst( $row->page_title ) == $lang->lcfirst( $warnMsg ) ) {
+ $existingSelector->setDefault( $lang->lcfirst( $warnMsg ) );
+ }
+
+ if ( $row->page_title != 'Abusefilter-warning' ) {
+ $existingSelector->addOption( $lang->lcfirst( $row->page_title ) );
+ }
+ }
+ }
+
+ $existingSelector->addOption( $this->msg( 'abusefilter-edit-warn-other' )->text(), 'other' );
+
+ return $existingSelector->getHTML();
+ }
+
+ /**
+ * @ToDo: Maybe we should also check if global values belong to $durations
+ * and determine the right point to add them if missing.
+ *
+ * @param array $durations
+ * @return array
+ */
+ protected static function normalizeBlocks( $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.
+ foreach ( $durations as &$duration ) {
+ $currentDuration = SpecialBlock::parseExpiryInput( $duration );
+ $anonDuration = SpecialBlock::parseExpiryInput( $wgAbuseFilterAnonBlockDuration );
+ $userDuration = SpecialBlock::parseExpiryInput( $wgAbuseFilterBlockDuration );
+
+ if ( $duration !== $wgAbuseFilterBlockDuration &&
+ $currentDuration === $userDuration ) {
+ $duration = $wgAbuseFilterBlockDuration;
+
+ } elseif ( $duration !== $wgAbuseFilterAnonBlockDuration &&
+ $currentDuration === $anonDuration ) {
+ $duration = $wgAbuseFilterAnonBlockDuration;
+ }
+ }
+
+ return $durations;
+ }
+
+ /**
+ * Loads filter data from the database by ID.
+ * @param int $id The filter's ID number
+ * @return array|null Either an associative array representing the filter,
+ * or NULL if the filter does not exist.
+ */
+ function loadFilterData( $id ) {
+ if ( $id == 'new' ) {
+ $obj = new stdClass;
+ $obj->af_pattern = '';
+ $obj->af_enabled = 1;
+ $obj->af_hidden = 0;
+ $obj->af_global = 0;
+ $obj->af_throttled = 0;
+ return [ $obj, [] ];
+ }
+
+ // Load from master to avoid unintended reversions where there's replication lag.
+ $dbr = $this->getRequest()->wasPosted()
+ ? wfGetDB( DB_MASTER )
+ : wfGetDB( DB_REPLICA );
+
+ // Load certain fields only. This prevents a condition seen on Wikimedia where
+ // a schema change adding a new field caused that extra field to be selected.
+ // Since the selected row may be inserted back into the database, this will cause
+ // an SQL error if, say, one server has the updated schema but another does not.
+ $loadFields = [
+ 'af_id',
+ 'af_pattern',
+ 'af_user',
+ 'af_user_text',
+ 'af_timestamp',
+ 'af_enabled',
+ 'af_comments',
+ 'af_public_comments',
+ 'af_hidden',
+ 'af_hit_count',
+ 'af_throttled',
+ 'af_deleted',
+ 'af_actions',
+ 'af_global',
+ 'af_group',
+ ];
+
+ // Load the main row
+ $row = $dbr->selectRow( 'abuse_filter', $loadFields, [ 'af_id' => $id ], __METHOD__ );
+
+ if ( !isset( $row ) || !isset( $row->af_id ) || !$row->af_id ) {
+ return null;
+ }
+
+ // Load the actions
+ $actions = [];
+ $res = $dbr->select( 'abuse_filter_action',
+ '*',
+ [ 'afa_filter' => $id ],
+ __METHOD__
+ );
+
+ foreach ( $res as $actionRow ) {
+ $thisAction = [];
+ $thisAction['action'] = $actionRow->afa_consequence;
+ $thisAction['parameters'] = array_filter( explode( "\n", $actionRow->afa_parameters ) );
+
+ $actions[$actionRow->afa_consequence] = $thisAction;
+ }
+
+ return [ $row, $actions ];
+ }
+
+ /**
+ * Load filter data to show in the edit view.
+ * Either from the HTTP request or from the filter/history_id given.
+ * The HTTP request always takes precedence.
+ * Includes caching.
+ * @param int $filter The filter ID being requested.
+ * @param int $history_id If any, the history ID being requested.
+ * @return Array with filter data if available, otherwise null.
+ * The first element contains the abuse_filter database row,
+ * the second element is an array of related abuse_filter_action rows.
+ */
+ function loadRequest( $filter, $history_id = null ) {
+ static $row = null;
+ static $actions = null;
+ $request = $this->getRequest();
+
+ if ( !is_null( $actions ) && !is_null( $row ) ) {
+ return [ $row, $actions ];
+ } elseif ( $request->wasPosted() ) {
+ # Nothing, we do it all later
+ } elseif ( $history_id ) {
+ return $this->loadHistoryItem( $history_id );
+ } else {
+ return $this->loadFilterData( $filter );
+ }
+
+ // We need some details like last editor
+ list( $row, $origActions ) = $this->loadFilterData( $filter );
+
+ $row->mOriginalRow = clone $row;
+ $row->mOriginalActions = $origActions;
+
+ // Check for importing
+ $import = $request->getVal( 'wpImportText' );
+ if ( $import ) {
+ $data = FormatJson::decode( $import );
+
+ $importRow = $data->row;
+ $actions = wfObjectToArray( $data->actions );
+
+ $copy = [
+ 'af_public_comments',
+ 'af_pattern',
+ 'af_comments',
+ 'af_deleted',
+ 'af_enabled',
+ 'af_hidden',
+ ];
+
+ foreach ( $copy as $name ) {
+ $row->$name = $importRow->$name;
+ }
+ } else {
+ $textLoads = [
+ 'af_public_comments' => 'wpFilterDescription',
+ 'af_pattern' => 'wpFilterRules',
+ 'af_comments' => 'wpFilterNotes',
+ ];
+
+ foreach ( $textLoads as $col => $field ) {
+ $row->$col = trim( $request->getVal( $field ) );
+ }
+
+ $row->af_group = $request->getVal( 'wpFilterGroup', 'default' );
+
+ $row->af_deleted = $request->getBool( 'wpFilterDeleted' );
+ $row->af_enabled = $request->getBool( 'wpFilterEnabled' ) && !$row->af_deleted;
+ $row->af_hidden = $request->getBool( 'wpFilterHidden' );
+ global $wgAbuseFilterIsCentral;
+ $row->af_global = $request->getBool( 'wpFilterGlobal' ) && $wgAbuseFilterIsCentral;
+
+ // Actions
+ global $wgAbuseFilterActions;
+ $actions = [];
+ foreach ( array_filter( $wgAbuseFilterActions ) as $action => $_ ) {
+ // Check if it's set
+ $enabled = $request->getBool( 'wpFilterAction' . ucfirst( $action ) );
+
+ if ( $enabled ) {
+ $parameters = [];
+
+ if ( $action == 'throttle' ) {
+ // We need to load the parameters
+ $throttleCount = $request->getIntOrNull( 'wpFilterThrottleCount' );
+ $throttlePeriod = $request->getIntOrNull( 'wpFilterThrottlePeriod' );
+ $throttleGroups = explode( "\n",
+ trim( $request->getText( 'wpFilterThrottleGroups' ) ) );
+
+ $parameters[0] = $this->mFilter; // For now, anyway
+ $parameters[1] = "$throttleCount,$throttlePeriod";
+ $parameters = array_merge( $parameters, $throttleGroups );
+ } elseif ( $action == 'warn' ) {
+ $specMsg = $request->getVal( 'wpFilterWarnMessage' );
+
+ if ( $specMsg == 'other' ) {
+ $specMsg = $request->getVal( 'wpFilterWarnMessageOther' );
+ }
+
+ $parameters[0] = $specMsg;
+ } elseif ( $action == 'block' ) {
+ $parameters[0] = $request->getCheck( 'wpFilterBlockTalk' ) ?
+ 'blocktalk' : 'noTalkBlockSet';
+ $parameters[1] = $request->getVal( 'wpBlockAnonDuration' );
+ $parameters[2] = $request->getVal( 'wpBlockUserDuration' );
+ } elseif ( $action == 'tag' ) {
+ $parameters = explode( "\n", trim( $request->getText( 'wpFilterTags' ) ) );
+ }
+
+ $thisAction = [ 'action' => $action, 'parameters' => $parameters ];
+ $actions[$action] = $thisAction;
+ }
+ }
+ }
+
+ $row->af_actions = implode( ',', array_keys( array_filter( $actions ) ) );
+
+ return [ $row, $actions ];
+ }
+
+ /**
+ * Loads historical data in a form that the editor can understand.
+ * @param int $id History ID
+ * @return array|bool False if the history ID is not valid, otherwise array in the usual format:
+ * First element contains the abuse_filter row (as it was).
+ * Second element contains an array of abuse_filter_action rows.
+ */
+ function loadHistoryItem( $id ) {
+ $dbr = wfGetDB( DB_REPLICA );
+
+ // Load the row.
+ $row = $dbr->selectRow( 'abuse_filter_history',
+ '*',
+ [ 'afh_id' => $id ],
+ __METHOD__
+ );
+
+ if ( !$row ) {
+ return false;
+ }
+
+ return AbuseFilter::translateFromHistory( $row );
+ }
+
+ /**
+ * @return null
+ */
+ protected function exposeWarningMessages() {
+ global $wgOut, $wgAbuseFilterDefaultWarningMessage;
+ $wgOut->addJsConfigVars(
+ 'wgAbuseFilterDefaultWarningMessage',
+ $wgAbuseFilterDefaultWarningMessage
+ );
+ }
+}
diff --git a/www/wiki/extensions/AbuseFilter/includes/Views/AbuseFilterViewExamine.php b/www/wiki/extensions/AbuseFilter/includes/Views/AbuseFilterViewExamine.php
new file mode 100644
index 00000000..73cb2d10
--- /dev/null
+++ b/www/wiki/extensions/AbuseFilter/includes/Views/AbuseFilterViewExamine.php
@@ -0,0 +1,219 @@
+<?php
+
+class AbuseFilterViewExamine extends AbuseFilterView {
+ public static $examineType = null;
+ public static $examineId = null;
+
+ public $mCounter, $mSearchUser, $mSearchPeriodStart, $mSearchPeriodEnd;
+ public $mTestFilter;
+
+ function show() {
+ $out = $this->getOutput();
+ $out->setPageTitle( $this->msg( 'abusefilter-examine' ) );
+ $out->addWikiMsg( 'abusefilter-examine-intro' );
+
+ $this->loadParameters();
+
+ // Check if we've got a subpage
+ if ( count( $this->mParams ) > 1 && is_numeric( $this->mParams[1] ) ) {
+ $this->showExaminerForRC( $this->mParams[1] );
+ } elseif ( count( $this->mParams ) > 2
+ && $this->mParams[1] == 'log'
+ && is_numeric( $this->mParams[2] )
+ ) {
+ $this->showExaminerForLogEntry( $this->mParams[2] );
+ } else {
+ $this->showSearch();
+ }
+ }
+
+ function showSearch() {
+ $RCMaxAge = $this->getConfig()->get( 'RCMaxAge' );
+ $min = wfTimestamp( TS_ISO_8601, time() - $RCMaxAge );
+ $max = wfTimestampNow();
+ $formDescriptor = [
+ 'SearchUser' => [
+ 'label-message' => 'abusefilter-test-user',
+ 'type' => 'user',
+ 'default' => $this->mSearchUser,
+ ],
+ 'SearchPeriodStart' => [
+ 'label-message' => 'abusefilter-test-period-start',
+ 'type' => 'datetime',
+ 'default' => $this->mSearchPeriodStart,
+ 'min' => $min,
+ 'max' => $max,
+ ],
+ 'SearchPeriodEnd' => [
+ 'label-message' => 'abusefilter-test-period-end',
+ 'type' => 'datetime',
+ 'default' => $this->mSearchPeriodEnd,
+ 'min' => $min,
+ 'max' => $max,
+ ],
+ ];
+ $htmlForm = HTMLForm::factory( 'ooui', $formDescriptor, $this->getContext() );
+ $htmlForm->setWrapperLegendMsg( 'abusefilter-examine-legend' )
+ ->addHiddenField( 'submit', 1 )
+ ->setSubmitTextMsg( 'abusefilter-examine-submit' )
+ ->setMethod( 'get' )
+ ->prepareForm()
+ ->displayForm( false );
+
+ if ( $this->mSubmit ) {
+ $this->showResults();
+ }
+ }
+
+ function showResults() {
+ $changesList = new AbuseFilterChangesList( $this->getSkin(), $this->mTestFilter );
+ $output = $changesList->beginRecentChangesList();
+ $this->mCounter = 1;
+
+ $pager = new AbuseFilterExaminePager( $this, $changesList );
+
+ $output .= $pager->getNavigationBar() .
+ $pager->getBody() .
+ $pager->getNavigationBar();
+
+ $output .= $changesList->endRecentChangesList();
+
+ $this->getOutput()->addHTML( $output );
+ }
+
+ function showExaminerForRC( $rcid ) {
+ // Get data
+ $dbr = wfGetDB( DB_REPLICA );
+ $rcQuery = RecentChange::getQueryInfo();
+ $row = $dbr->selectRow(
+ $rcQuery['tables'],
+ $rcQuery['fields'],
+ [ 'rc_id' => $rcid ],
+ __METHOD__,
+ [],
+ $rcQuery['joins']
+ );
+ $out = $this->getOutput();
+ if ( !$row ) {
+ $out->addWikiMsg( 'abusefilter-examine-notfound' );
+ return;
+ }
+
+ if ( !ChangesList::userCan( RecentChange::newFromRow( $row ), Revision::SUPPRESSED_ALL ) ) {
+ $out->addWikiMsg( 'abusefilter-log-details-hidden-implicit' );
+ return;
+ }
+
+ self::$examineType = 'rc';
+ self::$examineId = $rcid;
+
+ $vars = AbuseFilter::getVarsFromRCRow( $row );
+ $out->addJsConfigVars( 'wgAbuseFilterVariables', $vars->dumpAllVars( true ) );
+ $this->showExaminer( $vars );
+ }
+
+ function showExaminerForLogEntry( $logid ) {
+ // Get data
+ $dbr = wfGetDB( DB_REPLICA );
+ $row = $dbr->selectRow(
+ 'abuse_filter_log',
+ [ 'afl_filter', 'afl_deleted', 'afl_var_dump' ],
+ [ 'afl_id' => $logid ],
+ __METHOD__
+ );
+ $out = $this->getOutput();
+
+ if ( !$row ) {
+ $out->addWikiMsg( 'abusefilter-examine-notfound' );
+ return;
+ }
+
+ self::$examineType = 'log';
+ self::$examineId = $logid;
+
+ if ( !SpecialAbuseLog::canSeeDetails( $row->afl_filter ) ) {
+ $out->addWikiMsg( 'abusefilter-log-cannot-see-details' );
+ return;
+ }
+
+ if ( $row->afl_deleted && !SpecialAbuseLog::canSeeHidden() ) {
+ $out->addWikiMsg( 'abusefilter-log-details-hidden' );
+ return;
+ }
+
+ if ( SpecialAbuseLog::isHidden( $row ) === 'implicit' ) {
+ $rev = Revision::newFromId( $row->afl_rev_id );
+ if ( !$rev->userCan( Revision::SUPPRESSED_ALL, $this->getUser() ) ) {
+ $out->addWikiMsg( 'abusefilter-log-details-hidden-implicit' );
+ return;
+ }
+ }
+ $vars = AbuseFilter::loadVarDump( $row->afl_var_dump );
+ $out->addJsConfigVars( 'wgAbuseFilterVariables', $vars->dumpAllVars( true ) );
+ $this->showExaminer( $vars );
+ }
+
+ function showExaminer( $vars ) {
+ $output = $this->getOutput();
+ $output->enableOOUI();
+
+ if ( !$vars ) {
+ $output->addWikiMsg( 'abusefilter-examine-incompatible' );
+ return;
+ }
+
+ if ( $vars instanceof AbuseFilterVariableHolder ) {
+ $vars = $vars->exportAllVars();
+ }
+
+ $html = '';
+
+ $output->addModules( 'ext.abuseFilter.examine' );
+
+ // Add test bit
+ if ( $this->getUser()->isAllowed( 'abusefilter-modify' ) ) {
+ $tester = Xml::tags( 'h2', null, $this->msg( 'abusefilter-examine-test' )->parse() );
+ $tester .= AbuseFilter::buildEditBox( $this->mTestFilter, 'wpTestFilter', false );
+ $tester .= AbuseFilter::buildFilterLoader();
+ $html .= Xml::tags( 'div', [ 'id' => 'mw-abusefilter-examine-editor' ], $tester );
+ $html .= Xml::tags( 'p',
+ null,
+ new OOUI\ButtonInputWidget(
+ [
+ 'label' => $this->msg( 'abusefilter-examine-test-button' )->text(),
+ 'id' => 'mw-abusefilter-examine-test'
+ ]
+ ) .
+ Xml::element( 'div',
+ [
+ 'id' => 'mw-abusefilter-syntaxresult',
+ 'style' => 'display: none;'
+ ], '&#160;'
+ )
+ );
+ }
+
+ // Variable dump
+ $html .= Xml::tags(
+ 'h2',
+ null,
+ $this->msg( 'abusefilter-examine-vars' )->parse()
+ );
+ $html .= AbuseFilter::buildVarDumpTable( $vars, $this->getContext() );
+
+ $output->addHTML( $html );
+ }
+
+ function loadParameters() {
+ $request = $this->getRequest();
+ $this->mSearchPeriodStart = $request->getText( 'wpSearchPeriodStart' );
+ $this->mSearchPeriodEnd = $request->getText( 'wpSearchPeriodEnd' );
+ $this->mSubmit = $request->getCheck( 'submit' );
+ $this->mTestFilter = $request->getText( 'testfilter' );
+
+ // Normalise username
+ $searchUsername = $request->getText( 'wpSearchUser' );
+ $userTitle = Title::newFromText( $searchUsername, NS_USER );
+ $this->mSearchUser = $userTitle ? $userTitle->getText() : '';
+ }
+}
diff --git a/www/wiki/extensions/AbuseFilter/includes/Views/AbuseFilterViewHistory.php b/www/wiki/extensions/AbuseFilter/includes/Views/AbuseFilterViewHistory.php
new file mode 100644
index 00000000..f98241c0
--- /dev/null
+++ b/www/wiki/extensions/AbuseFilter/includes/Views/AbuseFilterViewHistory.php
@@ -0,0 +1,85 @@
+<?php
+
+class AbuseFilterViewHistory extends AbuseFilterView {
+ function __construct( $page, $params ) {
+ parent::__construct( $page, $params );
+ $this->mFilter = $page->mFilter;
+ }
+
+ function show() {
+ $out = $this->getOutput();
+ $filter = $this->getRequest()->getText( 'filter' ) ?: $this->mFilter;
+
+ if ( $filter ) {
+ $out->setPageTitle( $this->msg( 'abusefilter-history' )->numParams( $filter ) );
+ } else {
+ $out->setPageTitle( $this->msg( 'abusefilter-filter-log' ) );
+ }
+
+ # Check perms. abusefilter-modify is a superset of abusefilter-view-private
+ if ( $filter && AbuseFilter::filterHidden( $filter )
+ && !$this->getUser()->isAllowedAny( 'abusefilter-modify', 'abusefilter-view-private' )
+ ) {
+ $out->addWikiMsg( 'abusefilter-history-error-hidden' );
+ return;
+ }
+
+ # Useful links
+ $links = [];
+ if ( $filter ) {
+ $links['abusefilter-history-backedit'] = $this->getTitle( $filter );
+ }
+
+ foreach ( $links as $msg => $title ) {
+ $links[$msg] = $this->linkRenderer->makeLink(
+ $title,
+ new HtmlArmor( $this->msg( $msg )->parse() )
+ );
+ }
+
+ $backlinks = $this->getLanguage()->pipeList( $links );
+ $out->addHTML( Xml::tags( 'p', null, $backlinks ) );
+
+ # For user
+ $user = User::getCanonicalName( $this->getRequest()->getText( 'user' ), 'valid' );
+ if ( $user ) {
+ $out->addSubtitle(
+ $this->msg(
+ 'abusefilter-history-foruser',
+ Linker::userLink( 1 /* We don't really need to get a user ID */, $user ),
+ $user // For GENDER
+ )->text()
+ );
+ }
+
+ $formDescriptor = [
+ 'user' => [
+ 'type' => 'user',
+ 'name' => 'user',
+ 'default' => $user,
+ 'size' => '45',
+ 'label-message' => 'abusefilter-history-select-user'
+ ],
+ 'filter' => [
+ 'type' => 'text',
+ 'name' => 'filter',
+ 'default' => $filter,
+ 'size' => '45',
+ 'label-message' => 'abusefilter-history-select-filter'
+ ],
+ ];
+
+ $htmlForm = HTMLForm::factory( 'ooui', $formDescriptor, $this->getContext() );
+ $htmlForm->setSubmitTextMsg( 'abusefilter-history-select-submit' )
+ ->setWrapperLegendMsg( 'abusefilter-history-select-legend' )
+ ->setAction( $this->getTitle( 'history' )->getLocalURL() )
+ ->setMethod( 'get' )
+ ->prepareForm()
+ ->displayForm( false );
+
+ $pager = new AbuseFilterHistoryPager( $filter, $this, $user, $this->linkRenderer );
+ $table = $pager->getBody();
+
+ $out->addHTML( $pager->getNavigationBar() . $table . $pager->getNavigationBar() );
+ }
+}
diff --git a/www/wiki/extensions/AbuseFilter/includes/Views/AbuseFilterViewImport.php b/www/wiki/extensions/AbuseFilter/includes/Views/AbuseFilterViewImport.php
new file mode 100644
index 00000000..6bd4c269
--- /dev/null
+++ b/www/wiki/extensions/AbuseFilter/includes/Views/AbuseFilterViewImport.php
@@ -0,0 +1,24 @@
+<?php
+
+class AbuseFilterViewImport extends AbuseFilterView {
+ function show() {
+ $out = $this->getOutput();
+ if ( !$this->getUser()->isAllowed( 'abusefilter-modify' ) ) {
+ $out->addWikiMsg( 'abusefilter-edit-notallowed' );
+ return;
+ }
+ $url = SpecialPage::getTitleFor( 'AbuseFilter', 'new' )->getFullURL();
+
+ $out->addWikiMsg( 'abusefilter-import-intro' );
+
+ $formDescriptor = [
+ 'ImportText' => [
+ 'type' => 'textarea',
+ ]
+ ];
+ $htmlform = HTMLForm::factory( 'ooui', $formDescriptor, $this->getContext() )
+ ->setSubmitTextMsg( 'abusefilter-import-submit' )
+ ->setAction( $url )
+ ->show();
+ }
+}
diff --git a/www/wiki/extensions/AbuseFilter/includes/Views/AbuseFilterViewList.php b/www/wiki/extensions/AbuseFilter/includes/Views/AbuseFilterViewList.php
new file mode 100644
index 00000000..715fa536
--- /dev/null
+++ b/www/wiki/extensions/AbuseFilter/includes/Views/AbuseFilterViewList.php
@@ -0,0 +1,267 @@
+<?php
+
+/**
+ * The default view used in Special:AbuseFilter
+ */
+class AbuseFilterViewList extends AbuseFilterView {
+ function show() {
+ global $wgAbuseFilterCentralDB, $wgAbuseFilterIsCentral;
+
+ $out = $this->getOutput();
+ $request = $this->getRequest();
+
+ // Status info...
+ $this->showStatus();
+
+ $out->addWikiMsg( 'abusefilter-intro' );
+
+ // New filter button
+ if ( $this->canEdit() ) {
+ $out->enableOOUI();
+ $link = new OOUI\ButtonWidget( [
+ 'label' => $this->msg( 'abusefilter-new' )->text(),
+ 'href' => $this->getTitle( 'new' )->getFullURL(),
+ ] );
+ $out->addHTML( $link );
+ }
+
+ // Options.
+ $conds = [];
+ $deleted = $request->getVal( 'deletedfilters' );
+ $hidedisabled = $request->getBool( 'hidedisabled' );
+ $defaultscope = 'all';
+ if ( isset( $wgAbuseFilterCentralDB ) && !$wgAbuseFilterIsCentral ) {
+ // Show on remote wikis as default only local filters
+ $defaultscope = 'local';
+ }
+ $scope = $request->getVal( 'rulescope', $defaultscope );
+
+ $searchEnabled = $this->canViewPrivate() && !( isset( $wgAbuseFilterCentralDB ) &&
+ !$wgAbuseFilterIsCentral && $scope == 'global' );
+
+ if ( $searchEnabled ) {
+ $querypattern = $request->getVal( 'querypattern' );
+ $searchmode = $request->getVal( 'searchoption', 'LIKE' );
+ } else {
+ $querypattern = '';
+ $searchmode = '';
+ }
+
+ if ( $deleted == 'show' ) {
+ # Nothing
+ } elseif ( $deleted == 'only' ) {
+ $conds['af_deleted'] = 1;
+ } else { # hide, or anything else.
+ $conds['af_deleted'] = 0;
+ $deleted = 'hide';
+ }
+ if ( $hidedisabled ) {
+ $conds['af_deleted'] = 0;
+ $conds['af_enabled'] = 1;
+ }
+
+ if ( $scope == 'local' ) {
+ $conds['af_global'] = 0;
+ } elseif ( $scope == 'global' ) {
+ $conds['af_global'] = 1;
+ }
+
+ $dbr = wfGetDB( DB_REPLICA );
+
+ if ( $querypattern !== '' ) {
+ if ( $searchmode !== 'LIKE' ) {
+ // Check regex pattern validity
+ Wikimedia\suppressWarnings();
+ $validreg = preg_match( '/' . $querypattern . '/', null );
+ Wikimedia\restoreWarnings();
+
+ if ( $validreg === false ) {
+ $out->wrapWikiMsg(
+ '<div class="errorbox">$1</div>',
+ 'abusefilter-list-regexerror'
+ );
+ $this->showList(
+ [ 'af_deleted' => 0 ],
+ compact( 'deleted', 'hidedisabled', 'querypattern', 'searchmode', 'scope', 'searchEnabled' )
+ );
+ return;
+ }
+ if ( $searchmode === 'RLIKE' ) {
+ $conds[] = 'af_pattern RLIKE ' .
+ $dbr->addQuotes( $querypattern );
+ } else {
+ $conds[] = 'LOWER( CAST( af_pattern AS char ) ) RLIKE ' .
+ strtolower( $dbr->addQuotes( $querypattern ) );
+ }
+ } else {
+ // Build like query escaping tokens and encapsulating in % to search everywhere
+ $conds[] = 'LOWER( CAST( af_pattern AS char ) ) ' .
+ $dbr->buildLike(
+ $dbr->anyString(),
+ strtolower( $querypattern ),
+ $dbr->anyString()
+ );
+ }
+ }
+
+ $this->showList(
+ $conds,
+ compact( 'deleted', 'hidedisabled', 'querypattern', 'searchmode', 'scope', 'searchEnabled' )
+ );
+ }
+
+ function showList( $conds = [ 'af_deleted' => 0 ], $optarray = [] ) {
+ global $wgAbuseFilterCentralDB, $wgAbuseFilterIsCentral;
+
+ $this->getOutput()->addHTML(
+ Xml::element( 'h2', null, $this->msg( 'abusefilter-list' )->parse() )
+ );
+
+ $deleted = $optarray['deleted'];
+ $hidedisabled = $optarray['hidedisabled'];
+ $scope = $optarray['scope'];
+ $searchEnabled = $optarray['searchEnabled'];
+ $querypattern = $optarray['querypattern'];
+ $searchmode = $optarray['searchmode'];
+
+ if ( isset( $wgAbuseFilterCentralDB ) && !$wgAbuseFilterIsCentral && $scope == 'global' ) {
+ $pager = new GlobalAbuseFilterPager(
+ $this,
+ $conds,
+ $this->linkRenderer
+ );
+ } else {
+ $pager = new AbuseFilterPager(
+ $this,
+ $conds,
+ $this->linkRenderer,
+ [ $querypattern, $searchmode ]
+ );
+ }
+
+ # Options form
+ $formDescriptor = [];
+ $formDescriptor['deletedfilters'] = [
+ 'name' => 'deletedfilters',
+ 'type' => 'radio',
+ 'flatlist' => true,
+ 'label-message' => 'abusefilter-list-options-deleted',
+ 'options-messages' => [
+ 'abusefilter-list-options-deleted-show' => 'show',
+ 'abusefilter-list-options-deleted-hide' => 'hide',
+ 'abusefilter-list-options-deleted-only' => 'only',
+ ],
+ 'default' => $deleted,
+ ];
+
+ if ( isset( $wgAbuseFilterCentralDB ) ) {
+ $optionsMsg = [
+ 'abusefilter-list-options-scope-local' => 'local',
+ 'abusefilter-list-options-scope-global' => 'global',
+ ];
+ if ( $wgAbuseFilterIsCentral ) {
+ // For central wiki: add third scope option
+ $optionsMsg['abusefilter-list-options-scope-all'] = 'all';
+ }
+ $formDescriptor['rulescope'] = [
+ 'name' => 'rulescope',
+ 'type' => 'radio',
+ 'flatlist' => true,
+ 'label-message' => 'abusefilter-list-options-scope',
+ 'options-messages' => $optionsMsg,
+ 'default' => $scope,
+ ];
+ }
+
+ $formDescriptor['info'] = [
+ 'type' => 'info',
+ 'default' => $this->msg( 'abusefilter-list-options-disabled' )->parse(),
+ ];
+
+ $formDescriptor['hidedisabled'] = [
+ 'name' => 'hidedisabled',
+ 'type' => 'check',
+ 'label-message' => 'abusefilter-list-options-hidedisabled',
+ 'selected' => $hidedisabled,
+ ];
+
+ // ToDo: Since this is only for saving space, we should convert it
+ // to use a 'hide-if'
+ if ( $searchEnabled ) {
+ $formDescriptor['querypattern'] = [
+ 'name' => 'querypattern',
+ 'type' => 'text',
+ 'label-message' => 'abusefilter-list-options-searchfield',
+ 'placeholder' => $this->msg( 'abusefilter-list-options-searchpattern' )->text(),
+ 'default' => $querypattern
+ ];
+
+ $formDescriptor['searchoption'] = [
+ 'name' => 'searchoption',
+ 'type' => 'radio',
+ 'flatlist' => true,
+ 'label-message' => 'abusefilter-list-options-searchoptions',
+ 'options-messages' => [
+ 'abusefilter-list-options-search-like' => 'LIKE',
+ 'abusefilter-list-options-search-rlike' => 'RLIKE',
+ 'abusefilter-list-options-search-irlike' => 'IRLIKE',
+ ],
+ 'default' => $searchmode
+ ];
+ }
+
+ $formDescriptor['limit'] = [
+ 'name' => 'limit',
+ 'type' => 'select',
+ 'label-message' => 'abusefilter-list-limit',
+ 'options' => $pager->getLimitSelectList(),
+ 'default' => $pager->getLimit(),
+ ];
+
+ HTMLForm::factory( 'ooui', $formDescriptor, $this->getContext() )
+ ->addHiddenField( 'title', $this->getTitle()->getPrefixedDBkey() )
+ ->setAction( $this->getTitle()->getFullURL() )
+ ->setWrapperLegendMsg( 'abusefilter-list-options' )
+ ->setSubmitTextMsg( 'abusefilter-list-options-submit' )
+ ->setMethod( 'get' )
+ ->prepareForm()
+ ->displayForm( false );
+
+ $output =
+ $pager->getNavigationBar() .
+ $pager->getBody() .
+ $pager->getNavigationBar();
+
+ $this->getOutput()->addHTML( $output );
+ }
+
+ function showStatus() {
+ global $wgAbuseFilterConditionLimit, $wgAbuseFilterValidGroups;
+
+ $stash = ObjectCache::getMainStashInstance();
+ $overflow_count = (int)$stash->get( AbuseFilter::filterLimitReachedKey() );
+ $match_count = (int)$stash->get( AbuseFilter::filterMatchesKey() );
+ $total_count = 0;
+ foreach ( $wgAbuseFilterValidGroups as $group ) {
+ $total_count += (int)$stash->get( AbuseFilter::filterUsedKey( $group ) );
+ }
+
+ if ( $total_count > 0 ) {
+ $overflow_percent = sprintf( "%.2f", 100 * $overflow_count / $total_count );
+ $match_percent = sprintf( "%.2f", 100 * $match_count / $total_count );
+
+ $status = $this->msg( 'abusefilter-status' )
+ ->numParams(
+ $total_count,
+ $overflow_count,
+ $overflow_percent,
+ $wgAbuseFilterConditionLimit,
+ $match_count,
+ $match_percent
+ )->parse();
+
+ $status = Xml::tags( 'div', [ 'class' => 'mw-abusefilter-status' ], $status );
+ $this->getOutput()->addHTML( $status );
+ }
+ }
+}
diff --git a/www/wiki/extensions/AbuseFilter/includes/Views/AbuseFilterViewRevert.php b/www/wiki/extensions/AbuseFilter/includes/Views/AbuseFilterViewRevert.php
new file mode 100644
index 00000000..ef3773d7
--- /dev/null
+++ b/www/wiki/extensions/AbuseFilter/includes/Views/AbuseFilterViewRevert.php
@@ -0,0 +1,299 @@
+<?php
+
+class AbuseFilterViewRevert extends AbuseFilterView {
+ public $origPeriodStart, $origPeriodEnd, $mPeriodStart, $mPeriodEnd;
+ public $mReason;
+
+ function show() {
+ $lang = $this->getLanguage();
+ $filter = $this->mPage->mFilter;
+
+ $user = $this->getUser();
+ $out = $this->getOutput();
+
+ if ( !$user->isAllowed( 'abusefilter-revert' ) ) {
+ throw new PermissionsError( 'abusefilter-revert' );
+ }
+
+ $this->loadParameters();
+
+ if ( $this->attemptRevert() ) {
+ return;
+ }
+
+ $out->addWikiMsg( 'abusefilter-revert-intro', Message::numParam( $filter ) );
+ $out->setPageTitle( $this->msg( 'abusefilter-revert-title' )->numParams( $filter ) );
+
+ // First, the search form. Limit dates to avoid huge queries
+ $RCMaxAge = $this->getConfig()->get( 'RCMaxAge' );
+ $min = wfTimestamp( TS_ISO_8601, time() - $RCMaxAge );
+ $max = wfTimestampNow();
+ $filterLink =
+ $this->linkRenderer->makeLink(
+ SpecialPage::getTitleFor( 'AbuseFilter', intval( $filter ) ),
+ $lang->formatNum( intval( $filter ) )
+ );
+ $searchFields = [];
+ $searchFields['filterid'] = [
+ 'type' => 'info',
+ 'default' => $filterLink,
+ 'raw' => true,
+ 'label-message' => 'abusefilter-revert-filter'
+ ];
+ $searchFields['periodstart'] = [
+ 'type' => 'datetime',
+ 'name' => 'wpPeriodStart',
+ 'default' => $this->origPeriodStart,
+ 'label-message' => 'abusefilter-revert-periodstart',
+ 'min' => $min,
+ 'max' => $max
+ ];
+ $searchFields['periodend'] = [
+ 'type' => 'datetime',
+ 'name' => 'wpPeriodEnd',
+ 'default' => $this->origPeriodEnd,
+ 'label-message' => 'abusefilter-revert-periodend',
+ 'min' => $min,
+ 'max' => $max
+ ];
+
+ HTMLForm::factory( 'ooui', $searchFields, $this->getContext() )
+ ->addHiddenField( 'submit', 1 )
+ ->setAction( $this->getTitle( "revert/$filter" )->getLocalURL() )
+ ->setWrapperLegendMsg( 'abusefilter-revert-search-legend' )
+ ->setSubmitTextMsg( 'abusefilter-revert-search' )
+ ->setMethod( 'post' )
+ ->prepareForm()
+ ->displayForm( false );
+
+ if ( $this->mSubmit ) {
+ // Add a summary of everything that will be reversed.
+ $out->addWikiMsg( 'abusefilter-revert-preview-intro' );
+
+ // Look up all of them.
+ $results = $this->doLookup();
+ $list = [];
+
+ foreach ( $results as $result ) {
+ $displayActions = array_map(
+ [ 'AbuseFilter', 'getActionDisplay' ],
+ $result['actions'] );
+
+ $msg = $this->msg( 'abusefilter-revert-preview-item' )
+ ->rawParams(
+ $lang->timeanddate( $result['timestamp'], true ),
+ Linker::userLink( $result['userid'], $result['user'] ),
+ $result['action'],
+ $this->linkRenderer->makeLink( $result['title'] ),
+ $lang->commaList( $displayActions ),
+ $this->linkRenderer->makeLink(
+ SpecialPage::getTitleFor( 'AbuseLog' ),
+ $this->msg( 'abusefilter-log-detailslink' )->text(),
+ [],
+ [ 'details' => $result['id'] ]
+ )
+ )->params( $result['user'] )->parse();
+ $list[] = Xml::tags( 'li', null, $msg );
+ }
+
+ $out->addHTML( Xml::tags( 'ul', null, implode( "\n", $list ) ) );
+
+ // Add a button down the bottom.
+ $confirmForm = [];
+ $confirmForm['edittoken'] = [
+ 'type' => 'hidden',
+ 'name' => 'editToken',
+ 'default' => $user->getEditToken( "abusefilter-revert-$filter" )
+ ];
+ $confirmForm['title'] = [
+ 'type' => 'hidden',
+ 'name' => 'title',
+ 'default' => $this->getTitle( "revert/$filter" )->getPrefixedDBkey()
+ ];
+ $confirmForm['wpPeriodStart'] = [
+ 'type' => 'hidden',
+ 'name' => 'wpPeriodStart',
+ 'default' => $this->origPeriodStart
+ ];
+ $confirmForm['wpPeriodEnd'] = [
+ 'type' => 'hidden',
+ 'name' => 'wpPeriodEnd',
+ 'default' => $this->origPeriodEnd
+ ];
+ $confirmForm['reason'] = [
+ 'type' => 'text',
+ 'label-message' => 'abusefilter-revert-reasonfield',
+ 'name' => 'wpReason',
+ 'id' => 'wpReason',
+ ];
+ HTMLForm::factory( 'ooui', $confirmForm, $this->getContext() )
+ ->setAction( $this->getTitle( "revert/$filter" )->getLocalURL() )
+ ->setWrapperLegendMsg( 'abusefilter-revert-confirm-legend' )
+ ->setSubmitTextMsg( 'abusefilter-revert-confirm' )
+ ->setMethod( 'post' )
+ ->prepareForm()
+ ->displayForm( false );
+
+ }
+ }
+
+ function doLookup() {
+ $periodStart = $this->mPeriodStart;
+ $periodEnd = $this->mPeriodEnd;
+ $filter = $this->mPage->mFilter;
+
+ $conds = [ 'afl_filter' => $filter ];
+
+ $dbr = wfGetDB( DB_REPLICA );
+
+ if ( $periodStart ) {
+ $conds[] = 'afl_timestamp>' . $dbr->addQuotes( $dbr->timestamp( $periodStart ) );
+ }
+ if ( $periodEnd ) {
+ $conds[] = 'afl_timestamp<' . $dbr->addQuotes( $dbr->timestamp( $periodEnd ) );
+ }
+
+ // Database query.
+ $res = $dbr->select( 'abuse_filter_log', '*', $conds, __METHOD__ );
+
+ $results = [];
+ foreach ( $res as $row ) {
+ // Don't revert if there was no action, or the action was global
+ if ( !$row->afl_actions || $row->afl_wiki != null ) {
+ continue;
+ }
+
+ $actions = explode( ',', $row->afl_actions );
+ $reversibleActions = [ 'block', 'blockautopromote', 'degroup' ];
+ $currentReversibleActions = array_intersect( $actions, $reversibleActions );
+ if ( count( $currentReversibleActions ) ) {
+ $results[] = [
+ 'id' => $row->afl_id,
+ 'actions' => $currentReversibleActions,
+ 'user' => $row->afl_user_text,
+ 'userid' => $row->afl_user,
+ 'vars' => AbuseFilter::loadVarDump( $row->afl_var_dump ),
+ 'title' => Title::makeTitle( $row->afl_namespace, $row->afl_title ),
+ 'action' => $row->afl_action,
+ 'timestamp' => $row->afl_timestamp
+ ];
+ }
+ }
+
+ return $results;
+ }
+
+ function loadParameters() {
+ $request = $this->getRequest();
+
+ $this->origPeriodStart = $request->getText( 'wpPeriodStart' );
+ $this->mPeriodStart = strtotime( $this->origPeriodStart );
+ $this->origPeriodEnd = $request->getText( 'wpPeriodEnd' );
+ $this->mPeriodEnd = strtotime( $this->origPeriodEnd );
+ $this->mSubmit = $request->getVal( 'submit' );
+ $this->mReason = $request->getVal( 'wpReason' );
+ }
+
+ function attemptRevert() {
+ $filter = $this->mPage->mFilter;
+ $token = $this->getRequest()->getVal( 'editToken' );
+ if ( !$this->getUser()->matchEditToken( $token, "abusefilter-revert-$filter" ) ) {
+ return false;
+ }
+
+ $results = $this->doLookup();
+ foreach ( $results as $result ) {
+ $actions = $result['actions'];
+ foreach ( $actions as $action ) {
+ $this->revertAction( $action, $result );
+ }
+ }
+ $this->getOutput()->wrapWikiMsg(
+ '<p class="success">$1</p>',
+ [
+ 'abusefilter-revert-success',
+ $filter,
+ $this->getLanguage()->formatNum( $filter )
+ ]
+ );
+
+ return true;
+ }
+
+ /**
+ * @param string $action
+ * @param array $result
+ * @return bool
+ * @throws MWException
+ */
+ function revertAction( $action, $result ) {
+ switch ( $action ) {
+ case 'block':
+ $block = Block::newFromTarget( $result['user'] );
+ if ( !( $block && $block->getBy() == AbuseFilter::getFilterUser()->getId() ) ) {
+ // Not blocked by abuse filter
+ return false;
+ }
+ $block->delete();
+ $logEntry = new ManualLogEntry( 'block', 'unblock' );
+ $logEntry->setTarget( Title::makeTitle( NS_USER, $result['user'] ) );
+ $logEntry->setComment(
+ $this->msg(
+ 'abusefilter-revert-reason', $this->mPage->mFilter, $this->mReason
+ )->inContentLanguage()->text()
+ );
+ $logEntry->setPerformer( $this->getUser() );
+ $logEntry->publish( $logEntry->insert() );
+ return true;
+ case 'blockautopromote':
+ ObjectCache::getMainStashInstance()->delete(
+ AbuseFilter::autoPromoteBlockKey( User::newFromId( $result['userid'] ) )
+ );
+ return true;
+ case 'degroup':
+ // Pull the user's groups from the vars.
+ $oldGroups = $result['vars']['USER_GROUPS'];
+ $oldGroups = explode( ',', $oldGroups );
+ $oldGroups = array_diff(
+ $oldGroups,
+ array_intersect( $oldGroups, User::getImplicitGroups() )
+ );
+
+ $rows = [];
+ foreach ( $oldGroups as $group ) {
+ $rows[] = [
+ 'ug_user' => $result['userid'],
+ 'ug_group' => $group
+ ];
+ }
+
+ // Cheat a little bit. User::addGroup repeatedly is too slow.
+ $user = User::newFromId( $result['userid'] );
+ $currentGroups = $user->getGroups();
+ $newGroups = array_merge( $oldGroups, $currentGroups );
+
+ // Don't do anything if there are no groups to add.
+ if ( !count( array_diff( $newGroups, $currentGroups ) ) ) {
+ return false;
+ }
+
+ $dbw = wfGetDB( DB_MASTER );
+ $dbw->insert( 'user_groups', $rows, __METHOD__, [ 'IGNORE' ] );
+ $user->invalidateCache();
+
+ $log = new LogPage( 'rights' );
+ $log->addEntry( 'rights', $user->getUserPage(),
+ $this->msg(
+ 'abusefilter-revert-reason',
+ $this->mPage->mFilter,
+ $this->mReason
+ )->inContentLanguage()->text(),
+ [ implode( ',', $currentGroups ), implode( ',', $newGroups ) ]
+ );
+
+ return true;
+ }
+
+ throw new MWException( 'Invalid action' . $action );
+ }
+}
diff --git a/www/wiki/extensions/AbuseFilter/includes/Views/AbuseFilterViewTestBatch.php b/www/wiki/extensions/AbuseFilter/includes/Views/AbuseFilterViewTestBatch.php
new file mode 100644
index 00000000..47c4be84
--- /dev/null
+++ b/www/wiki/extensions/AbuseFilter/includes/Views/AbuseFilterViewTestBatch.php
@@ -0,0 +1,207 @@
+<?php
+
+class AbuseFilterViewTestBatch extends AbuseFilterView {
+ // Hard-coded for now.
+ protected static $mChangeLimit = 100;
+
+ public $mShowNegative, $mTestPeriodStart, $mTestPeriodEnd, $mTestPage;
+ public $mTestUser;
+
+ function show() {
+ $out = $this->getOutput();
+
+ AbuseFilter::disableConditionLimit();
+
+ if ( !$this->getUser()->isAllowed( 'abusefilter-modify' ) ) {
+ $out->addWikiMsg( 'abusefilter-mustbeeditor' );
+ return;
+ }
+
+ $this->loadParameters();
+
+ $out->setPageTitle( $this->msg( 'abusefilter-test' ) );
+ $out->addWikiMsg( 'abusefilter-test-intro', self::$mChangeLimit );
+ $out->enableOOUI();
+
+ $output = '';
+ $output .=
+ AbuseFilter::buildEditBox(
+ $this->mFilter,
+ 'wpTestFilter',
+ true,
+ true,
+ true
+ ) . "\n";
+
+ $output .= AbuseFilter::buildFilterLoader();
+ $output = Xml::tags( 'div', [ 'id' => 'mw-abusefilter-test-editor' ], $output );
+
+ $RCMaxAge = $this->getConfig()->get( 'RCMaxAge' );
+ $min = wfTimestamp( TS_ISO_8601, time() - $RCMaxAge );
+ $max = wfTimestampNow();
+
+ // Search form
+ $formFields = [];
+ $formFields['wpTestUser'] = [
+ 'name' => 'wpTestUser',
+ 'type' => 'user',
+ 'ipallowed' => true,
+ 'label-message' => 'abusefilter-test-user',
+ 'default' => $this->mTestUser
+ ];
+ $formFields['wpTestPeriodStart'] = [
+ 'name' => 'wpTestPeriodStart',
+ 'type' => 'datetime',
+ 'label-message' => 'abusefilter-test-period-start',
+ 'default' => $this->mTestPeriodStart,
+ 'min' => $min,
+ 'max' => $max
+ ];
+ $formFields['wpTestPeriodEnd'] = [
+ 'name' => 'wpTestPeriodEnd',
+ 'type' => 'datetime',
+ 'label-message' => 'abusefilter-test-period-end',
+ 'default' => $this->mTestPeriodEnd,
+ 'min' => $min,
+ 'max' => $max
+ ];
+ $formFields['wpTestPage'] = [
+ 'name' => 'wpTestPage',
+ 'type' => 'title',
+ 'label-message' => 'abusefilter-test-page',
+ 'default' => $this->mTestPage,
+ 'creatable' => true
+ ];
+ $formFields['wpShowNegative'] = [
+ 'name' => 'wpShowNegative',
+ 'type' => 'check',
+ 'label-message' => 'abusefilter-test-shownegative',
+ 'selected' => $this->mShowNegative
+ ];
+
+ $htmlForm = HTMLForm::factory( 'ooui', $formFields, $this->getContext() )
+ ->addHiddenField( 'title', $this->getTitle( 'test' )->getPrefixedDBkey() )
+ ->setId( 'wpFilterForm' )
+ ->setWrapperLegendMsg( 'abusefilter-list-options' )
+ ->setAction( $this->getTitle( 'test' )->getLocalURL() )
+ ->setSubmitTextMsg( 'abusefilter-test-submit' )
+ ->setMethod( 'post' )
+ ->prepareForm();
+ $htmlForm = $htmlForm->getHTML( $htmlForm );
+
+ $output = Xml::fieldset( $this->msg( 'abusefilter-test-legend' )->text(), $output . $htmlForm );
+ $out->addHTML( $output );
+
+ if ( $this->getRequest()->wasPosted() ) {
+ $this->doTest();
+ }
+ }
+
+ /**
+ * @fixme this is similar to AbuseFilterExaminePager::getQueryInfo
+ */
+ function doTest() {
+ // Quick syntax check.
+ $out = $this->getOutput();
+ $result = AbuseFilter::checkSyntax( $this->mFilter );
+ if ( $result !== true ) {
+ $out->addWikiMsg( 'abusefilter-test-syntaxerr' );
+ return;
+ }
+ $dbr = wfGetDB( DB_REPLICA );
+
+ $conds = [];
+
+ if ( (string)$this->mTestUser !== '' ) {
+ $conds[] = ActorMigration::newMigration()->getWhere(
+ $dbr, 'rc_user', User::newFromName( $this->mTestUser, false )
+ )['conds'];
+ }
+
+ if ( $this->mTestPeriodStart ) {
+ $conds[] = 'rc_timestamp >= ' .
+ $dbr->addQuotes( $dbr->timestamp( strtotime( $this->mTestPeriodStart ) ) );
+ }
+ if ( $this->mTestPeriodEnd ) {
+ $conds[] = 'rc_timestamp <= ' .
+ $dbr->addQuotes( $dbr->timestamp( strtotime( $this->mTestPeriodEnd ) ) );
+ }
+ if ( $this->mTestPage ) {
+ $title = Title::newFromText( $this->mTestPage );
+ if ( $title instanceof Title ) {
+ $conds['rc_namespace'] = $title->getNamespace();
+ $conds['rc_title'] = $title->getDBkey();
+ } else {
+ $out->addWikiMsg( 'abusefilter-test-badtitle' );
+ return;
+ }
+ }
+
+ $conds[] = $this->buildTestConditions( $dbr );
+
+ // Get our ChangesList
+ $changesList = new AbuseFilterChangesList( $this->getSkin(), $this->mFilter );
+ $output = $changesList->beginRecentChangesList();
+
+ $rcQuery = RecentChange::getQueryInfo();
+ $res = $dbr->select(
+ $rcQuery['tables'],
+ $rcQuery['fields'],
+ array_filter( $conds ),
+ __METHOD__,
+ [ 'LIMIT' => self::$mChangeLimit, 'ORDER BY' => 'rc_timestamp desc' ],
+ $rcQuery['joins']
+ );
+
+ $counter = 1;
+
+ foreach ( $res as $row ) {
+ $vars = AbuseFilter::getVarsFromRCRow( $row );
+
+ if ( !$vars ) {
+ continue;
+ }
+
+ $result = AbuseFilter::checkConditions( $this->mFilter, $vars );
+
+ if ( $result || $this->mShowNegative ) {
+ // Stash result in RC item
+ $rc = RecentChange::newFromRow( $row );
+ $rc->filterResult = $result;
+ $rc->counter = $counter++;
+ $output .= $changesList->recentChangesLine( $rc, false );
+ }
+ }
+
+ $output .= $changesList->endRecentChangesList();
+
+ $out->addHTML( $output );
+ }
+
+ function loadParameters() {
+ $request = $this->getRequest();
+
+ $this->mFilter = $request->getText( 'wpTestFilter' );
+ $this->mShowNegative = $request->getBool( 'wpShowNegative' );
+ $testUsername = $request->getText( 'wpTestUser' );
+ $this->mTestPeriodEnd = $request->getText( 'wpTestPeriodEnd' );
+ $this->mTestPeriodStart = $request->getText( 'wpTestPeriodStart' );
+ $this->mTestPage = $request->getText( 'wpTestPage' );
+
+ if ( !$this->mFilter
+ && count( $this->mParams ) > 1
+ && is_numeric( $this->mParams[1] )
+ ) {
+ $dbr = wfGetDB( DB_REPLICA );
+ $this->mFilter = $dbr->selectField( 'abuse_filter',
+ 'af_pattern',
+ [ 'af_id' => $this->mParams[1] ],
+ __METHOD__
+ );
+ }
+
+ // Normalise username
+ $userTitle = Title::newFromText( $testUsername, NS_USER );
+ $this->mTestUser = $userTitle ? $userTitle->getText() : null;
+ }
+}
diff --git a/www/wiki/extensions/AbuseFilter/includes/Views/AbuseFilterViewTools.php b/www/wiki/extensions/AbuseFilter/includes/Views/AbuseFilterViewTools.php
new file mode 100644
index 00000000..cff4b22f
--- /dev/null
+++ b/www/wiki/extensions/AbuseFilter/includes/Views/AbuseFilterViewTools.php
@@ -0,0 +1,56 @@
+<?php
+
+class AbuseFilterViewTools extends AbuseFilterView {
+ function show() {
+ $out = $this->getOutput();
+ $user = $this->getUser();
+ $request = $this->getRequest();
+
+ if ( !$user->isAllowed( 'abusefilter-modify' ) ) {
+ $out->addWikiMsg( 'abusefilter-mustbeeditor' );
+ return;
+ }
+
+ // Header
+ $out->addWikiMsg( 'abusefilter-tools-text' );
+
+ // Expression evaluator
+ $eval = '';
+ $eval .= AbuseFilter::buildEditBox( $request->getText( 'wpTestExpr' ), 'wpTestExpr' );
+
+ $eval .= Xml::tags( 'p', null,
+ Xml::element( 'input',
+ [
+ 'type' => 'button',
+ 'id' => 'mw-abusefilter-submitexpr',
+ 'value' => $this->msg( 'abusefilter-tools-submitexpr' )->text() ]
+ )
+ );
+ $eval .= Xml::element( 'p', [ 'id' => 'mw-abusefilter-expr-result' ], ' ' );
+
+ $eval = Xml::fieldset( $this->msg( 'abusefilter-tools-expr' )->text(), $eval );
+ $out->addHTML( $eval );
+
+ $out->addModules( 'ext.abuseFilter.tools' );
+
+ // Hacky little box to re-enable autoconfirmed if it got disabled
+ $rac = '';
+ $rac .= Xml::inputLabel(
+ $this->msg( 'abusefilter-tools-reautoconfirm-user' )->text(),
+ 'wpReAutoconfirmUser',
+ 'reautoconfirm-user',
+ 45
+ );
+ $rac .= '&#160;';
+ $rac .= Xml::element(
+ 'input',
+ [
+ 'type' => 'button',
+ 'id' => 'mw-abusefilter-reautoconfirmsubmit',
+ 'value' => $this->msg( 'abusefilter-tools-reautoconfirm-submit' )->text()
+ ]
+ );
+ $rac = Xml::fieldset( $this->msg( 'abusefilter-tools-reautoconfirm' )->text(), $rac );
+ $out->addHTML( $rac );
+ }
+}