summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorBrian Evans <grknight@gentoo.org>2019-04-11 11:08:13 -0400
committerBrian Evans <grknight@gentoo.org>2019-04-11 11:08:13 -0400
commite6f63b37820d165b55e4c9bf262b3d6d92e28c67 (patch)
treedf5db2f24d45da64a8e3d90104cc5021dff3bf9c /AbuseFilter/includes
parentDrop Flow extension (diff)
downloadextensions-e6f63b37820d165b55e4c9bf262b3d6d92e28c67.tar.gz
extensions-e6f63b37820d165b55e4c9bf262b3d6d92e28c67.tar.bz2
extensions-e6f63b37820d165b55e4c9bf262b3d6d92e28c67.zip
Upgrade AbuseFilter to 1.32
Signed-off-by: Brian Evans <grknight@gentoo.org>
Diffstat (limited to 'AbuseFilter/includes')
-rw-r--r--AbuseFilter/includes/AFComputedVariable.php102
-rw-r--r--AbuseFilter/includes/AbuseFilter.php (renamed from AbuseFilter/includes/AbuseFilter.class.php)1417
-rw-r--r--AbuseFilter/includes/AbuseFilterChangesList.php123
-rw-r--r--AbuseFilter/includes/AbuseFilterHooks.php (renamed from AbuseFilter/includes/AbuseFilter.hooks.php)292
-rw-r--r--AbuseFilter/includes/AbuseFilterModifyLogFormatter.php9
-rw-r--r--AbuseFilter/includes/AbuseFilterPreAuthenticationProvider.php13
-rw-r--r--AbuseFilter/includes/AbuseFilterSuppressLogFormatter.php16
-rw-r--r--AbuseFilter/includes/AbuseFilterVariableHolder.php78
-rw-r--r--AbuseFilter/includes/AbuseLogHitFormatter.php7
-rw-r--r--AbuseFilter/includes/TableDiffFormatterFullContext.php36
-rw-r--r--AbuseFilter/includes/Views/AbuseFilterView.php315
-rw-r--r--AbuseFilter/includes/Views/AbuseFilterViewDiff.php194
-rw-r--r--AbuseFilter/includes/Views/AbuseFilterViewEdit.php1213
-rw-r--r--AbuseFilter/includes/Views/AbuseFilterViewExamine.php178
-rw-r--r--AbuseFilter/includes/Views/AbuseFilterViewHistory.php269
-rw-r--r--AbuseFilter/includes/Views/AbuseFilterViewImport.php5
-rw-r--r--AbuseFilter/includes/Views/AbuseFilterViewList.php583
-rw-r--r--AbuseFilter/includes/Views/AbuseFilterViewRevert.php220
-rw-r--r--AbuseFilter/includes/Views/AbuseFilterViewTestBatch.php196
-rw-r--r--AbuseFilter/includes/Views/AbuseFilterViewTools.php69
-rw-r--r--AbuseFilter/includes/api/ApiAbuseFilterCheckMatch.php21
-rw-r--r--AbuseFilter/includes/api/ApiAbuseFilterCheckSyntax.php10
-rw-r--r--AbuseFilter/includes/api/ApiAbuseFilterEvalExpression.php8
-rw-r--r--AbuseFilter/includes/api/ApiAbuseFilterUnblockAutopromote.php22
-rw-r--r--AbuseFilter/includes/api/ApiQueryAbuseFilters.php12
-rw-r--r--AbuseFilter/includes/api/ApiQueryAbuseLog.php57
-rw-r--r--AbuseFilter/includes/pagers/AbuseFilterExaminePager.php82
-rw-r--r--AbuseFilter/includes/pagers/AbuseFilterHistoryPager.php229
-rw-r--r--AbuseFilter/includes/pagers/AbuseFilterPager.php296
-rw-r--r--AbuseFilter/includes/pagers/AbuseLogPager.php89
-rw-r--r--AbuseFilter/includes/pagers/GlobalAbuseFilterPager.php81
-rw-r--r--AbuseFilter/includes/parser/AFPData.php210
-rw-r--r--AbuseFilter/includes/parser/AFPParserState.php4
-rw-r--r--AbuseFilter/includes/parser/AFPToken.php10
-rw-r--r--AbuseFilter/includes/parser/AFPTreeNode.php29
-rw-r--r--AbuseFilter/includes/parser/AFPTreeParser.php27
-rw-r--r--AbuseFilter/includes/parser/AFPUserVisibleException.php15
-rw-r--r--AbuseFilter/includes/parser/AbuseFilterCachingParser.php69
-rw-r--r--AbuseFilter/includes/parser/AbuseFilterParser.php714
-rw-r--r--AbuseFilter/includes/parser/AbuseFilterTokenizer.php39
-rw-r--r--AbuseFilter/includes/special/SpecialAbuseFilter.php21
-rw-r--r--AbuseFilter/includes/special/SpecialAbuseLog.php748
42 files changed, 5304 insertions, 2824 deletions
diff --git a/AbuseFilter/includes/AFComputedVariable.php b/AbuseFilter/includes/AFComputedVariable.php
index 45d635bb..15847598 100644
--- a/AbuseFilter/includes/AFComputedVariable.php
+++ b/AbuseFilter/includes/AFComputedVariable.php
@@ -1,15 +1,19 @@
<?php
+use Wikimedia\Rdbms\Database;
+use MediaWiki\MediaWikiServices;
+use MediaWiki\Logger\LoggerFactory;
+
class AFComputedVariable {
public $mMethod, $mParameters;
public static $userCache = [];
public static $articleCache = [];
/**
- * @param $method
- * @param $parameters
+ * @param string $method
+ * @param array $parameters
*/
- function __construct( $method, $parameters ) {
+ public function __construct( $method, $parameters ) {
$this->mMethod = $method;
$this->mParameters = $parameters;
}
@@ -19,11 +23,11 @@ class AFComputedVariable {
*
*
* @param string $wikitext
- * @param WikiPage $article
+ * @param Article $article
*
* @return object
*/
- function parseNonEditWikitext( $wikitext, $article ) {
+ public function parseNonEditWikitext( $wikitext, $article ) {
static $cache = [];
$cacheKey = md5( $wikitext ) . ':' . $article->getTitle()->getPrefixedText();
@@ -47,10 +51,10 @@ class AFComputedVariable {
* in case a user name is given as argument. Nowadays user objects are passed
* directly but many old log entries rely on this.
*
- * @param $user string|User
+ * @param string|User $user
* @return User
*/
- static function getUserObject( $user ) {
+ public static function getUserObject( $user ) {
if ( $user instanceof User ) {
$username = $user->getName();
} else {
@@ -59,7 +63,8 @@ class AFComputedVariable {
return self::$userCache[$username];
}
- wfDebug( "Couldn't find user $username in cache\n" );
+ $logger = LoggerFactory::getInstance( 'AbuseFilter' );
+ $logger->debug( "Couldn't find user $username in cache" );
}
if ( count( self::$userCache ) > 1000 ) {
@@ -67,7 +72,7 @@ class AFComputedVariable {
}
if ( $user instanceof User ) {
- $userCache[$username] = $user;
+ self::$userCache[$username] = $user;
return $user;
}
@@ -86,11 +91,11 @@ class AFComputedVariable {
}
/**
- * @param $namespace
- * @param $title Title
+ * @param int $namespace
+ * @param string $title
* @return Article
*/
- static function articleFromTitle( $namespace, $title ) {
+ public static function articleFromTitle( $namespace, $title ) {
if ( isset( self::$articleCache["$namespace:$title"] ) ) {
return self::$articleCache["$namespace:$title"];
}
@@ -99,7 +104,8 @@ class AFComputedVariable {
self::$articleCache = [];
}
- wfDebug( "Creating article object for $namespace:$title in cache\n" );
+ $logger = LoggerFactory::getInstance( 'AbuseFilter' );
+ $logger->debug( "Creating article object for $namespace:$title in cache" );
// TODO: use WikiPage instead!
$t = Title::makeTitle( $namespace, $title );
@@ -109,11 +115,11 @@ class AFComputedVariable {
}
/**
- * @param WikiPage $article
+ * @param Article $article
* @return array
*/
- static function getLinksFromDB( $article ) {
- // Stolen from ConfirmEdit
+ public static function getLinksFromDB( $article ) {
+ // Stolen from ConfirmEdit, SimpleCaptcha::getLinksFromTracker
$id = $article->getId();
if ( !$id ) {
return [];
@@ -134,12 +140,12 @@ class AFComputedVariable {
}
/**
- * @param $vars AbuseFilterVariableHolder
+ * @param AbuseFilterVariableHolder $vars
* @return AFPData|array|int|mixed|null|string
* @throws MWException
* @throws AFPException
*/
- function compute( $vars ) {
+ public function compute( $vars ) {
$parameters = $this->mParameters;
$result = null;
@@ -151,6 +157,8 @@ class AFComputedVariable {
switch ( $this->mMethod ) {
case 'diff':
+ // Currently unused. Kept for backwards compatibility since it remains
+ // as mMethod for old variables. A fallthrough would instead change old results.
$text1Var = $parameters['oldtext-var'];
$text2Var = $parameters['newtext-var'];
$text1 = $vars->getVar( $text1Var )->toString();
@@ -159,6 +167,21 @@ class AFComputedVariable {
$format = new UnifiedDiffFormatter();
$result = $format->format( $diffs );
break;
+ case 'diff-array':
+ // Introduced with T74329 to uniform the diff to MW's standard one.
+ // The difference with 'diff' method is noticeable when one of the
+ // $text is empty: it'll be treated as **really** empty, instead of
+ // an empty string.
+ $text1Var = $parameters['oldtext-var'];
+ $text2Var = $parameters['newtext-var'];
+ $text1 = $vars->getVar( $text1Var )->toString();
+ $text2 = $vars->getVar( $text2Var )->toString();
+ $text1 = $text1 === '' ? [] : explode( "\n", $text1 );
+ $text2 = $text2 === '' ? [] : explode( "\n", $text2 );
+ $diffs = new Diff( $text1, $text2 );
+ $format = new UnifiedDiffFormatter();
+ $result = $format->format( $diffs );
+ break;
case 'diff-split':
$diff = $vars->getVar( $parameters['diff-var'] )->toString();
$line_prefix = $parameters['line-prefix'];
@@ -186,7 +209,6 @@ class AFComputedVariable {
if ( $article->getContentModel() === CONTENT_MODEL_WIKITEXT ) {
$textVar = $parameters['text-var'];
- // XXX: Use prepareContentForEdit. But we need a Content object for that.
$new_text = $vars->getVar( $textVar )->toString();
$content = ContentHandler::makeContent( $new_text, $article->getTitle() );
$editInfo = $article->prepareContentForEdit( $content );
@@ -203,11 +225,12 @@ class AFComputedVariable {
$parameters['title']
);
+ $logger = LoggerFactory::getInstance( 'AbuseFilter' );
if ( $vars->getVar( 'context' )->toString() == 'filter' ) {
$links = $this->getLinksFromDB( $article );
- wfDebug( "AbuseFilter: loading old links from DB\n" );
+ $logger->debug( 'Loading old links from DB' );
} elseif ( $article->getContentModel() === CONTENT_MODEL_WIKITEXT ) {
- wfDebug( "AbuseFilter: loading old links from Parser\n" );
+ $logger->debug( 'Loading old links from Parser' );
$textVar = $parameters['text-var'];
$wikitext = $vars->getVar( $textVar )->toString();
@@ -351,6 +374,18 @@ class AFComputedVariable {
$registration = $obj->getRegistration();
$result = wfTimestamp( TS_UNIX, $asOf ) - wfTimestampOrNull( TS_UNIX, $registration );
break;
+ case 'page-age':
+ $title = Title::makeTitle( $parameters['namespace'], $parameters['title'] );
+
+ $firstRevisionTime = $title->getEarliestRevTime();
+ if ( !$firstRevisionTime ) {
+ $result = 0;
+ break;
+ }
+
+ $asOf = $parameters['asof'];
+ $result = wfTimestamp( TS_UNIX, $asOf ) - wfTimestampOrNull( TS_UNIX, $firstRevisionTime );
+ break;
case 'user-groups':
// Deprecated but needed by old log entries
$user = $parameters['user'];
@@ -362,10 +397,16 @@ class AFComputedVariable {
$result = strlen( $s );
break;
case 'subtract':
+ // Currently unused, kept for backwards compatibility for old filters.
$v1 = $vars->getVar( $parameters['val1-var'] )->toFloat();
$v2 = $vars->getVar( $parameters['val2-var'] )->toFloat();
$result = $v1 - $v2;
break;
+ case 'subtract-int':
+ $v1 = $vars->getVar( $parameters['val1-var'] )->toInt();
+ $v2 = $vars->getVar( $parameters['val2-var'] )->toInt();
+ $result = $v1 - $v2;
+ break;
case 'revision-text-by-id':
$rev = Revision::newFromId( $parameters['revid'] );
$result = AbuseFilter::revisionToString( $rev );
@@ -390,33 +431,36 @@ class AFComputedVariable {
/**
* @param Title $title
- * @return string[] List of the last 10 (unique) authors from $title
+ * @return string[] Usernames of the last 10 (unique) authors from $title
*/
public static function getLastPageAuthors( Title $title ) {
if ( !$title->exists() ) {
return [];
}
- $cache = ObjectCache::getMainWANInstance();
+ $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
+ $fname = __METHOD__;
return $cache->getWithSetCallback(
$cache->makeKey( 'last-10-authors', 'revision', $title->getLatestRevID() ),
$cache::TTL_MINUTE,
- function ( $oldValue, &$ttl, array &$setOpts ) use ( $title ) {
+ function ( $oldValue, &$ttl, array &$setOpts ) use ( $title, $fname ) {
$dbr = wfGetDB( DB_REPLICA );
$setOpts += Database::getCacheSetOptions( $dbr );
// Get the last 100 edit authors with a trivial query (avoid T116557)
+ $revQuery = Revision::getQueryInfo();
$revAuthors = $dbr->selectFieldValues(
- 'revision',
- 'rev_user_text',
+ $revQuery['tables'],
+ $revQuery['fields']['rev_user_text'],
[ 'rev_page' => $title->getArticleID() ],
- __METHOD__,
+ $fname,
// Some pages have < 10 authors but many revisions (e.g. bot pages)
[ 'ORDER BY' => 'rev_timestamp DESC',
'LIMIT' => 100,
// Force index per T116557
- 'USE INDEX' => 'page_timestamp',
- ]
+ 'USE INDEX' => [ 'revision' => 'page_timestamp' ],
+ ],
+ $revQuery['joins']
);
// Get the last 10 distinct authors within this set of edits
$users = [];
diff --git a/AbuseFilter/includes/AbuseFilter.class.php b/AbuseFilter/includes/AbuseFilter.php
index 3ce5cdb4..2566c25e 100644
--- a/AbuseFilter/includes/AbuseFilter.class.php
+++ b/AbuseFilter/includes/AbuseFilter.php
@@ -1,7 +1,10 @@
<?php
+use MediaWiki\Linker\LinkRenderer;
use MediaWiki\Logger\LoggerFactory;
+use MediaWiki\Session\SessionManager;
use MediaWiki\MediaWikiServices;
+use Wikimedia\Rdbms\IDatabase;
/**
* This class contains most of the business logic of AbuseFilter. It consists of mostly
@@ -18,7 +21,8 @@ class AbuseFilter {
public static $condCount = 0;
/** @var array Map of (action ID => string[]) */
- public static $tagsToSet = []; // FIXME: avoid global state here
+ // FIXME: avoid global state here
+ public static $tagsToSet = [];
public static $history_mappings = [
'af_pattern' => 'afh_pattern',
@@ -42,7 +46,9 @@ class AbuseFilter {
],
'op-comparison' => [
'==' => 'equal',
+ '===' => 'equal-strict',
'!=' => 'notequal',
+ '!==' => 'notequal-strict',
'<' => 'lt',
'>' => 'gt',
'<=' => 'lte',
@@ -69,20 +75,26 @@ class AbuseFilter {
'lcase(string)' => 'lcase',
'ucase(string)' => 'ucase',
'ccnorm(string)' => 'ccnorm',
+ 'ccnorm_contains_any(haystack,needle1,needle2,..)' => 'ccnorm-contains-any',
+ 'ccnorm_contains_all(haystack,needle1,needle2,..)' => 'ccnorm-contains-all',
'rmdoubles(string)' => 'rmdoubles',
'specialratio(string)' => 'specialratio',
'norm(string)' => 'norm',
'count(needle,haystack)' => 'count',
'rcount(needle,haystack)' => 'rcount',
+ 'get_matches(needle,haystack)' => 'get_matches',
'rmwhitespace(text)' => 'rmwhitespace',
'rmspecials(text)' => 'rmspecials',
'ip_in_range(ip, range)' => 'ip_in_range',
- 'contains_any(haystack,needle1,needle2,needle3)' => 'contains-any',
+ 'contains_any(haystack,needle1,needle2,...)' => 'contains-any',
+ 'contains_all(haystack,needle1,needle2,...)' => 'contains-all',
+ 'equals_to_any(haystack,needle1,needle2,...)' => 'equals-to-any',
'substr(subject, offset, length)' => 'substr',
'strpos(haystack, needle)' => 'strpos',
'str_replace(subject, search, replace)' => 'str_replace',
'rescape(string)' => 'rescape',
'set_var(var,value)' => 'set_var',
+ 'sanitize(string)' => 'sanitize',
],
'vars' => [
'timestamp' => 'timestamp',
@@ -97,19 +109,21 @@ class AbuseFilter {
'old_content_model' => 'old-content-model',
'removed_lines' => 'removedlines',
'summary' => 'summary',
- 'article_articleid' => 'article-id',
- 'article_namespace' => 'article-ns',
- 'article_text' => 'article-text',
- 'article_prefixedtext' => 'article-prefixedtext',
- // 'article_views' => 'article-views', # May not be enabled, defined in getBuilderValues()
- 'moved_from_articleid' => 'movedfrom-id',
+ 'page_id' => 'page-id',
+ 'page_namespace' => 'page-ns',
+ 'page_title' => 'page-title',
+ 'page_prefixedtitle' => 'page-prefixedtitle',
+ 'page_age' => 'page-age',
+ 'moved_from_id' => 'movedfrom-id',
'moved_from_namespace' => 'movedfrom-ns',
- 'moved_from_text' => 'movedfrom-text',
- 'moved_from_prefixedtext' => 'movedfrom-prefixedtext',
- 'moved_to_articleid' => 'movedto-id',
+ 'moved_from_title' => 'movedfrom-title',
+ 'moved_from_prefixedtitle' => 'movedfrom-prefixedtitle',
+ 'moved_from_age' => 'movedfrom-age',
+ 'moved_to_id' => 'movedto-id',
'moved_to_namespace' => 'movedto-ns',
- 'moved_to_text' => 'movedto-text',
- 'moved_to_prefixedtext' => 'movedto-prefixedtext',
+ 'moved_to_title' => 'movedto-title',
+ 'moved_to_prefixedtitle' => 'movedto-prefixedtitle',
+ 'moved_to_age' => 'movedto-age',
'user_editcount' => 'user-editcount',
'user_age' => 'user-age',
'user_name' => 'user-name',
@@ -127,14 +141,24 @@ class AbuseFilter {
'added_lines_pst' => 'addedlines-pst',
'new_text' => 'new-text-stripped',
'new_html' => 'new-html',
- 'article_restrictions_edit' => 'restrictions-edit',
- 'article_restrictions_move' => 'restrictions-move',
- 'article_restrictions_create' => 'restrictions-create',
- 'article_restrictions_upload' => 'restrictions-upload',
- 'article_recent_contributors' => 'recent-contributors',
- 'article_first_contributor' => 'first-contributor',
- // 'old_text' => 'old-text-stripped', # Disabled, performance
- // 'old_html' => 'old-html', # Disabled, performance
+ 'page_restrictions_edit' => 'restrictions-edit',
+ 'page_restrictions_move' => 'restrictions-move',
+ 'page_restrictions_create' => 'restrictions-create',
+ 'page_restrictions_upload' => 'restrictions-upload',
+ 'page_recent_contributors' => 'recent-contributors',
+ 'page_first_contributor' => 'first-contributor',
+ 'moved_from_restrictions_edit' => 'movedfrom-restrictions-edit',
+ 'moved_from_restrictions_move' => 'movedfrom-restrictions-move',
+ 'moved_from_restrictions_create' => 'movedfrom-restrictions-create',
+ 'moved_from_restrictions_upload' => 'movedfrom-restrictions-upload',
+ 'moved_from_recent_contributors' => 'movedfrom-recent-contributors',
+ 'moved_from_first_contributor' => 'movedfrom-first-contributor',
+ 'moved_to_restrictions_edit' => 'movedto-restrictions-edit',
+ 'moved_to_restrictions_move' => 'movedto-restrictions-move',
+ 'moved_to_restrictions_create' => 'movedto-restrictions-create',
+ 'moved_to_restrictions_upload' => 'movedto-restrictions-upload',
+ 'moved_to_recent_contributors' => 'movedto-recent-contributors',
+ 'moved_to_first_contributor' => 'movedto-first-contributor',
'old_links' => 'old-links',
'minor_edit' => 'minor-edit',
'file_sha1' => 'file-sha1',
@@ -147,13 +171,43 @@ class AbuseFilter {
],
];
+ /** @var array Old vars which aren't in use anymore */
+ public static $disabledVars = [
+ 'old_text' => 'old-text-stripped',
+ 'old_html' => 'old-html'
+ ];
+
+ public static $deprecatedVars = [
+ 'article_text' => 'page_title',
+ 'article_prefixedtext' => 'page_prefixedtitle',
+ 'article_namespace' => 'page_namespace',
+ 'article_articleid' => 'page_id',
+ 'article_restrictions_edit' => 'page_restrictions_edit',
+ 'article_restrictions_move' => 'page_restrictions_move',
+ 'article_restrictions_create' => 'page_restrictions_create',
+ 'article_restrictions_upload' => 'page_restrictions_upload',
+ 'article_recent_contributors' => 'page_recent_contributors',
+ 'article_first_contributor' => 'page_first_contributor',
+ 'moved_from_text' => 'moved_from_title',
+ 'moved_from_prefixedtext' => 'moved_from_prefixedtitle',
+ 'moved_from_articleid' => 'moved_from_id',
+ 'moved_to_text' => 'moved_to_title',
+ 'moved_to_prefixedtext' => 'moved_to_prefixedtitle',
+ 'moved_to_articleid' => 'moved_to_id',
+ ];
+
public static $editboxName = null;
/**
* @param IContextSource $context
* @param string $pageType
+ * @param LinkRenderer $linkRenderer
*/
- public static function addNavigationLinks( IContextSource $context, $pageType ) {
+ public static function addNavigationLinks(
+ IContextSource $context,
+ $pageType,
+ LinkRenderer $linkRenderer
+ ) {
$linkDefs = [
'home' => 'Special:AbuseFilter',
'recentchanges' => 'Special:AbuseFilter/history',
@@ -161,21 +215,25 @@ class AbuseFilter {
'log' => 'Special:AbuseLog',
];
- if ( $context->getUser()->isAllowed( 'abusefilter-modify' ) ) {
+ if ( $context->getUser()->isAllowedAny( 'abusefilter-modify', 'abusefilter-view-private' ) ) {
$linkDefs = array_merge( $linkDefs, [
'test' => 'Special:AbuseFilter/test',
- 'tools' => 'Special:AbuseFilter/tools',
- 'import' => 'Special:AbuseFilter/import',
+ 'tools' => 'Special:AbuseFilter/tools'
+ ] );
+ }
+
+ if ( $context->getUser()->isAllowed( 'abusefilter-modify' ) ) {
+ $linkDefs = array_merge( $linkDefs, [
+ 'import' => 'Special:AbuseFilter/import'
] );
}
- // Save some translator work
+ // Re-use the message
$msgOverrides = [
'recentchanges' => 'abusefilter-filter-log',
];
$links = [];
- $linkRenderer = MediaWikiServices::getInstance()->getLinkRenderer();
foreach ( $linkDefs as $name => $page ) {
// Give grep a chance to find the usages:
@@ -197,7 +255,9 @@ class AbuseFilter {
}
}
- $linkStr = $context->msg( 'parentheses', $context->getLanguage()->pipeList( $links ) )->text();
+ $linkStr = $context->msg( 'parentheses' )
+ ->rawParams( $context->getLanguage()->pipeList( $links ) )
+ ->text();
$linkStr = $context->msg( 'abusefilter-topnav' )->parse() . " $linkStr";
$linkStr = Xml::tags( 'div', [ 'class' => 'mw-abusefilter-navigation' ], $linkStr );
@@ -206,7 +266,6 @@ class AbuseFilter {
}
/**
- * @static
* @param User $user
* @return AbuseFilterVariableHolder
*/
@@ -267,16 +326,30 @@ class AbuseFilter {
}
$realValues = self::$builderValues;
- global $wgDisableCounters;
- if ( !$wgDisableCounters ) {
- $realValues['vars']['article_views'] = 'article-views';
- }
+
Hooks::run( 'AbuseFilter-builder', [ &$realValues ] );
return $realValues;
}
/**
+ * @return array
+ */
+ public static function getDeprecatedVariables() {
+ static $deprecatedVars = null;
+
+ if ( $deprecatedVars ) {
+ return $deprecatedVars;
+ }
+
+ $deprecatedVars = self::$deprecatedVars;
+
+ Hooks::run( 'AbuseFilter-deprecatedVariables', [ &$deprecatedVars ] );
+
+ return $deprecatedVars;
+ }
+
+ /**
* @param string $filter
* @return bool
*/
@@ -294,7 +367,7 @@ class AbuseFilter {
}
if ( $filter === 'new' ) {
return false;
- };
+ }
$hidden = $dbr->selectField(
'abuse_filter',
'af_hidden',
@@ -319,8 +392,10 @@ class AbuseFilter {
}
}
+ /**
+ * For use in batch scripts and the like
+ */
public static function disableConditionLimit() {
- // For use in batch scripts and the like
self::$condLimitEnabled = false;
}
@@ -336,21 +411,11 @@ class AbuseFilter {
return $vars;
}
- $vars->setVar( $prefix . '_ARTICLEID', $title->getArticleID() );
+ $vars->setVar( $prefix . '_ID', $title->getArticleID() );
$vars->setVar( $prefix . '_NAMESPACE', $title->getNamespace() );
- $vars->setVar( $prefix . '_TEXT', $title->getText() );
- $vars->setVar( $prefix . '_PREFIXEDTEXT', $title->getPrefixedText() );
-
- global $wgDisableCounters;
- if ( !$wgDisableCounters && !$title->isSpecialPage() ) {
- // Support: HitCounters extension
- // XXX: This should be part of the extension (T159069)
- if ( method_exists( 'HitCounters\HitCounters', 'getCount' ) ) {
- $vars->setVar( $prefix . '_VIEWS', HitCounters\HitCounters::getCount( $title ) );
- }
- }
+ $vars->setVar( $prefix . '_TITLE', $title->getText() );
+ $vars->setVar( $prefix . '_PREFIXEDTITLE', $title->getPrefixedText() );
- // Use restrictions.
global $wgRestrictionTypes;
foreach ( $wgRestrictionTypes as $action ) {
$vars->setLazyLoadVar( "{$prefix}_restrictions_$action", 'get-page-restrictions',
@@ -367,6 +432,13 @@ class AbuseFilter {
'namespace' => $title->getNamespace()
] );
+ $vars->setLazyLoadVar( "{$prefix}_age", 'page-age',
+ [
+ 'title' => $title->getText(),
+ 'namespace' => $title->getNamespace(),
+ 'asof' => wfTimestampNow()
+ ] );
+
$vars->setLazyLoadVar( "{$prefix}_first_contributor", 'load-first-author',
[
'title' => $title->getText(),
@@ -379,8 +451,9 @@ class AbuseFilter {
}
/**
- * @param $filter
- * @return mixed
+ * @param string $filter
+ * @return true|array True when successful, otherwise a two-element array with exception message
+ * and character position of the syntax error
*/
public static function checkSyntax( $filter ) {
global $wgAbuseFilterParserClass;
@@ -392,11 +465,10 @@ class AbuseFilter {
}
/**
- * @param $expr
- * @param array $vars
+ * @param string $expr
* @return string
*/
- public static function evaluateExpression( $expr, $vars = [] ) {
+ public static function evaluateExpression( $expr ) {
global $wgAbuseFilterParserClass;
if ( self::checkSyntax( $expr ) !== true ) {
@@ -404,7 +476,7 @@ class AbuseFilter {
}
/** @var $parser AbuseFilterParser */
- $parser = new $wgAbuseFilterParserClass( $vars );
+ $parser = new $wgAbuseFilterParserClass;
return $parser->evaluateExpression( $expr );
}
@@ -432,10 +504,10 @@ class AbuseFilter {
try {
$result = $parser->parse( $conds, self::$condCount );
} catch ( Exception $excep ) {
- // Sigh.
$result = false;
- wfDebugLog( 'AbuseFilter', 'AbuseFilter parser error: ' . $excep->getMessage() . "\n" );
+ $logger = LoggerFactory::getInstance( 'AbuseFilter' );
+ $logger->debug( 'AbuseFilter parser error: ' . $excep->getMessage() );
if ( !$ignoreError ) {
throw $excep;
@@ -450,19 +522,36 @@ class AbuseFilter {
*
* @param AbuseFilterVariableHolder $vars
* @param string $group The filter's group (as defined in $wgAbuseFilterValidGroups)
+ * @param Title|null $title
+ * @param string $mode 'execute' for edits and logs, 'stash' for cached matches
*
* @return bool[] Map of (integer filter ID => bool)
*/
- public static function checkAllFilters( $vars, $group = 'default' ) {
+ public static function checkAllFilters(
+ $vars,
+ $group = 'default',
+ Title $title = null,
+ $mode = 'execute'
+ ) {
global $wgAbuseFilterCentralDB, $wgAbuseFilterIsCentral;
+ global $wgAbuseFilterConditionLimit;
- // Fetch from the database.
+ // Ensure that we start fresh, see T193374
+ self::$condCount = 0;
+
+ // Fetch filters to check from the database.
$filter_matched = [];
$dbr = wfGetDB( DB_REPLICA );
+ $fields = [
+ 'af_id',
+ 'af_pattern',
+ 'af_public_comments',
+ 'af_timestamp'
+ ];
$res = $dbr->select(
'abuse_filter',
- '*',
+ $fields,
[
'af_enabled' => 1,
'af_deleted' => 0,
@@ -472,7 +561,7 @@ class AbuseFilter {
);
foreach ( $res as $row ) {
- $filter_matched[$row->af_id] = self::checkFilter( $row, $vars, true );
+ $filter_matched[$row->af_id] = self::checkFilter( $row, $vars, $title, '', $mode );
}
if ( $wgAbuseFilterCentralDB && !$wgAbuseFilterIsCentral ) {
@@ -483,16 +572,17 @@ class AbuseFilter {
$res = ObjectCache::getMainWANInstance()->getWithSetCallback(
$globalRulesKey,
WANObjectCache::TTL_INDEFINITE,
- function () use ( $group, $fname ) {
+ function () use ( $group, $fname, $fields ) {
global $wgAbuseFilterCentralDB;
- $fdb = wfGetLB( $wgAbuseFilterCentralDB )->getConnectionRef(
+ $lbFactory = MediaWikiServices::getInstance()->getDBLoadBalancerFactory();
+ $fdb = $lbFactory->getMainLB( $wgAbuseFilterCentralDB )->getConnectionRef(
DB_REPLICA, [], $wgAbuseFilterCentralDB
);
return iterator_to_array( $fdb->select(
'abuse_filter',
- '*',
+ $fields,
[
'af_enabled' => 1,
'af_deleted' => 0,
@@ -510,45 +600,61 @@ class AbuseFilter {
foreach ( $res as $row ) {
$filter_matched['global-' . $row->af_id] =
- self::checkFilter( $row, $vars, true, 'global-' );
+ self::checkFilter( $row, $vars, $title, 'global-', $mode );
}
}
- // Update statistics, and disable filters which are over-blocking.
- self::recordStats( $filter_matched, $group );
+ if ( $title instanceof Title && self::$condCount > $wgAbuseFilterConditionLimit ) {
+ $actionID = implode( '-', [
+ $title->getPrefixedText(),
+ $vars->getVar( 'user_name' )->toString(),
+ $vars->getVar( 'action' )->toString()
+ ] );
+ self::bufferTagsToSetByAction( [ $actionID => [ 'abusefilter-condition-limit' ] ] );
+ }
+
+ if ( $mode === 'execute' ) {
+ // Update statistics, and disable filters which are over-blocking.
+ self::recordStats( $filter_matched, $group );
+ }
return $filter_matched;
}
/**
- * @static
* @param stdClass $row
* @param AbuseFilterVariableHolder $vars
- * @param bool $profile
+ * @param Title|null $title
* @param string $prefix
+ * @param string $mode 'execute' for edits and logs, 'stash' for cached matches
* @return bool
*/
- public static function checkFilter( $row, $vars, $profile = false, $prefix = '' ) {
- global $wgAbuseFilterProfile;
+ public static function checkFilter(
+ $row,
+ $vars,
+ Title $title = null,
+ $prefix = '',
+ $mode = 'execute'
+ ) {
+ global $wgAbuseFilterProfile, $wgAbuseFilterRuntimeProfile,
+ $wgAbuseFilterSlowFilterRuntimeLimit;
$filterID = $prefix . $row->af_id;
- $startConds = $startTime = null;
- if ( $profile && $wgAbuseFilterProfile ) {
- $startConds = self::$condCount;
- $startTime = microtime( true );
- }
+ // Record data to be used if profiling is enabled and mode is 'execute'
+ $startConds = self::$condCount;
+ $startTime = microtime( true );
// Store the row somewhere convenient
self::$filterCache[$filterID] = $row;
- // Check conditions...
$pattern = trim( $row->af_pattern );
if (
self::checkConditions(
$pattern,
$vars,
- true /* ignore errors */
+ // Ignore errors
+ true
)
) {
// Record match.
@@ -558,19 +664,51 @@ class AbuseFilter {
$result = false;
}
- if ( $profile && $wgAbuseFilterProfile ) {
- $endTime = microtime( true );
- $endConds = self::$condCount;
+ $timeTaken = microtime( true ) - $startTime;
+ $condsUsed = self::$condCount - $startConds;
- $timeTaken = $endTime - $startTime;
- $condsUsed = $endConds - $startConds;
+ if ( $wgAbuseFilterProfile && $mode === 'execute' ) {
self::recordProfilingResult( $row->af_id, $timeTaken, $condsUsed );
}
+ $runtime = $timeTaken * 1000;
+ if ( $mode === 'execute' && $wgAbuseFilterRuntimeProfile &&
+ $runtime > $wgAbuseFilterSlowFilterRuntimeLimit ) {
+ self::recordSlowFilter( $filterID, $runtime, $condsUsed, $result, $title );
+ }
+
return $result;
}
/**
+ * Logs slow filter's runtime data for later analysis
+ *
+ * @param string $filterId
+ * @param float $runtime
+ * @param int $totalConditions
+ * @param bool $matched
+ * @param Title|null $title
+ */
+ private static function recordSlowFilter(
+ $filterId, $runtime, $totalConditions, $matched, Title $title = null
+ ) {
+ $title = $title ? $title->getPrefixedText() : '';
+
+ $logger = LoggerFactory::getInstance( 'AbuseFilterSlow' );
+ $logger->info(
+ 'Edit filter {filter_id} on {wiki} is taking longer than expected',
+ [
+ 'wiki' => wfWikiID(),
+ 'filter_id' => $filterId,
+ 'title' => $title,
+ 'runtime' => $runtime,
+ 'matched' => $matched,
+ 'total_conditions' => $totalConditions
+ ]
+ );
+ }
+
+ /**
* @param int $filter
*/
public static function resetFilterProfile( $filter ) {
@@ -631,8 +769,10 @@ class AbuseFilter {
return [ 0, 0 ];
}
- $timeProfile = ( $curTotal / $curCount ) * 1000; // 1000 ms in a sec
- $timeProfile = round( $timeProfile, 2 ); // Return in ms, rounded to 2dp
+ // 1000 ms in a sec
+ $timeProfile = ( $curTotal / $curCount ) * 1000;
+ // Return in ms, rounded to 2dp
+ $timeProfile = round( $timeProfile, 2 );
$condProfile = ( $curConds / $curCount );
$condProfile = round( $condProfile, 0 );
@@ -648,7 +788,7 @@ class AbuseFilter {
* @return string|bool
*/
public static function decodeGlobalName( $filter ) {
- if ( strpos( $filter, 'global-' ) == 0 ) {
+ if ( strpos( $filter, 'global-' ) === 0 ) {
return substr( $filter, strlen( 'global-' ) );
}
@@ -692,7 +832,7 @@ class AbuseFilter {
}
/**
- * @param DatabaseBase $dbr
+ * @param IDatabase $dbr
* @param string[] $filters
* @param string $prefix
* @return array[]
@@ -718,14 +858,13 @@ class AbuseFilter {
if ( $row->af_throttled
&& !empty( $wgAbuseFilterRestrictions[$row->afa_consequence] )
) {
- # Don't do the action
+ // Don't do the action
} elseif ( $row->afa_filter != $row->af_id ) {
- // We probably got a NULL, as it's a LEFT JOIN.
- // Don't add it.
+ // We probably got a NULL, as it's a LEFT JOIN. Don't add it.
} else {
$actionsByFilter[$prefix . $row->afa_filter][$row->afa_consequence] = [
'action' => $row->afa_consequence,
- 'parameters' => explode( "\n", $row->afa_parameters )
+ 'parameters' => array_filter( explode( "\n", $row->afa_parameters ) )
];
}
}
@@ -734,34 +873,36 @@ class AbuseFilter {
}
/**
- * Executes a list of actions.
+ * Executes a set of actions.
*
* @param string[] $filters
* @param Title $title
* @param AbuseFilterVariableHolder $vars
+ * @param User $user
* @return Status returns the operation's status. $status->isOK() will return true if
* there were no actions taken, false otherwise. $status->getValue() will return
* an array listing the actions taken. $status->getErrors() etc. will provide
* the errors and warnings to be shown to the user to explain the actions.
*/
- public static function executeFilterActions( $filters, $title, $vars ) {
+ public static function executeFilterActions( $filters, $title, $vars, User $user ) {
global $wgMainCacheType;
$actionsByFilter = self::getConsequencesForFilters( $filters );
$actionsTaken = array_fill_keys( $filters, [] );
$messages = [];
+ // Accumulator to track max block to issue
+ $maxExpiry = -1;
- global $wgOut, $wgAbuseFilterDisallowGlobalLocalBlocks, $wgAbuseFilterRestrictions;
+ global $wgAbuseFilterDisallowGlobalLocalBlocks, $wgAbuseFilterRestrictions,
+ $wgAbuseFilterBlockDuration, $wgAbuseFilterAnonBlockDuration;
foreach ( $actionsByFilter as $filter => $actions ) {
// Special-case handling for warnings.
- $parsed_public_comments = $wgOut->parseInline(
- self::getFilter( $filter )->af_public_comments
- );
+ $filter_public_comments = self::getFilter( $filter )->af_public_comments;
$global_filter = self::decodeGlobalName( $filter ) !== false;
- // If the filter is throttled and throttling is available via object
+ // If the filter has "throttle" enabled and throttling is available via object
// caching, check to see if the user has hit the throttle.
if ( !empty( $actions['throttle'] ) && $wgMainCacheType !== CACHE_NONE ) {
$parameters = $actions['throttle']['parameters'];
@@ -789,42 +930,82 @@ class AbuseFilter {
if ( !empty( $actions['warn'] ) ) {
$parameters = $actions['warn']['parameters'];
- $warnKey = 'abusefilter-warned-' . md5( $title->getPrefixedText() ) . '-' . $filter;
+ $action = $vars->getVar( 'action' )->toString();
+ // Generate a unique key to determine whether the user has already been warned.
+ // We'll warn again if one of these changes: session, page, triggered filter or action
+ $warnKey = 'abusefilter-warned-' . md5( $title->getPrefixedText() ) .
+ '-' . $filter . '-' . $action;
// Make sure the session is started prior to using it
- if ( session_id() === '' ) {
- wfSetupSession();
- }
+ $session = SessionManager::getGlobalSession();
+ $session->persist();
- if ( !isset( $_SESSION[$warnKey] ) || !$_SESSION[$warnKey] ) {
- $_SESSION[$warnKey] = true;
+ if ( !isset( $session[$warnKey] ) || !$session[$warnKey] ) {
+ $session[$warnKey] = true;
// Threaten them a little bit
- if ( !empty( $parameters[0] ) && strlen( $parameters[0] ) ) {
+ if ( isset( $parameters[0] ) ) {
$msg = $parameters[0];
} else {
$msg = 'abusefilter-warning';
}
- $messages[] = [ $msg, $parsed_public_comments, $filter ];
+ $messages[] = [ $msg, $filter_public_comments, $filter ];
$actionsTaken[$filter][] = 'warn';
- continue; // Don't do anything else.
+ // Don't do anything else.
+ continue;
} else {
// We already warned them
- $_SESSION[$warnKey] = false;
+ $session[$warnKey] = false;
}
unset( $actions['warn'] );
}
- // prevent double warnings
+ // Prevent double warnings
if ( count( array_intersect_key( $actions, array_filter( $wgAbuseFilterRestrictions ) ) ) > 0 &&
!empty( $actions['disallow'] )
) {
unset( $actions['disallow'] );
}
+ // Find out the max expiry to issue the longest triggered block.
+ // Need to check here since methods like user->getBlock() aren't available
+ if ( !empty( $actions['block'] ) ) {
+ $parameters = $actions['block']['parameters'];
+
+ if ( count( $parameters ) === 3 ) {
+ // New type of filters with custom block
+ if ( $user->isAnon() ) {
+ $expiry = $parameters[1];
+ } else {
+ $expiry = $parameters[2];
+ }
+ } else {
+ // Old type with fixed expiry
+ if ( $user->isAnon() && $wgAbuseFilterAnonBlockDuration !== null ) {
+ // The user isn't logged in and the anon block duration
+ // doesn't default to $wgAbuseFilterBlockDuration.
+ $expiry = $wgAbuseFilterAnonBlockDuration;
+ } else {
+ $expiry = $wgAbuseFilterBlockDuration;
+ }
+ }
+
+ $currentExpiry = SpecialBlock::parseExpiryInput( $expiry );
+ if ( $currentExpiry > SpecialBlock::parseExpiryInput( $maxExpiry ) ) {
+ // Save the parameters to issue the block with
+ $maxExpiry = $expiry;
+ $blockValues = [
+ self::getFilter( $filter )->af_public_comments,
+ $filter,
+ is_array( $parameters ) && in_array( 'blocktalk', $parameters )
+ ];
+ }
+ unset( $actions['block'] );
+ }
+
// Do the rest of the actions
foreach ( $actions as $action => $info ) {
$newMsg = self::takeConsequenceAction(
@@ -833,7 +1014,8 @@ class AbuseFilter {
$title,
$vars,
self::getFilter( $filter )->af_public_comments,
- $filter
+ $filter,
+ $user
);
if ( $newMsg !== null ) {
@@ -843,6 +1025,30 @@ class AbuseFilter {
}
}
+ // Since every filter has been analysed, we now know what the
+ // longest block duration is, so we can issue the block if
+ // maxExpiry has been changed.
+ if ( $maxExpiry !== -1 ) {
+ self::doAbuseFilterBlock(
+ [
+ 'desc' => $blockValues[0],
+ 'number' => $blockValues[1]
+ ],
+ $user->getName(),
+ $maxExpiry,
+ true,
+ $blockValues[2]
+ );
+ $message = [
+ 'abusefilter-blocked-display',
+ $blockValues[0],
+ $blockValues[1]
+ ];
+ // Manually add the message. If we're here, there is one.
+ $messages[] = $message;
+ $actionsTaken[ $blockValues[1] ][] = 'block';
+ }
+
return self::buildStatus( $actionsTaken, $messages );
}
@@ -852,7 +1058,7 @@ class AbuseFilter {
*
* @param array[] $actionsTaken associative array mapping each filter to the list if
* actions taken because of that filter.
- * @param array[] $messages a list if arrays, where each array contains a message key
+ * @param array[] $messages a list of arrays, where each array contains a message key
* followed by any message parameters.
*
* @return Status
@@ -861,7 +1067,7 @@ class AbuseFilter {
$status = Status::newGood( $actionsTaken );
foreach ( $messages as $msg ) {
- call_user_func_array( [ $status, 'fatal' ], $msg );
+ $status->fatal( ...$msg );
}
return $status;
@@ -871,27 +1077,14 @@ class AbuseFilter {
* @param AbuseFilterVariableHolder $vars
* @param Title $title
* @param string $group The filter's group (as defined in $wgAbuseFilterValidGroups)
- * @param User $user The user performing the action; defaults to $wgUser
+ * @param User $user The user performing the action
* @param string $mode Use 'execute' to run filters and log or 'stash' to only cache matches
* @return Status
*/
public static function filterAction(
- $vars, $title, $group = 'default', $user = null, $mode = 'execute'
+ AbuseFilterVariableHolder $vars, Title $title, $group, User $user, $mode = 'execute'
) {
- global $wgUser, $wgTitle, $wgRequest, $wgAbuseFilterRuntimeProfile;
-
- $context = RequestContext::getMain();
- $oldContextTitle = $context->getTitle();
-
- $oldWgTitle = $wgTitle;
-
- if ( !$wgTitle ) {
- $wgTitle = SpecialPage::getTitleFor( 'AbuseFilter' );
- }
-
- if ( !$user ) {
- $user = $wgUser;
- }
+ global $wgRequest, $wgAbuseFilterRuntimeProfile, $wgAbuseFilterLogIP;
$logger = LoggerFactory::getInstance( 'StashEdit' );
$statsd = MediaWikiServices::getInstance()->getStatsdDataFactory();
@@ -927,7 +1120,7 @@ class AbuseFilter {
$statsd->increment( 'abusefilter.check-stash.hit' );
}
} else {
- $filter_matched = self::checkAllFilters( $vars, $group );
+ $filter_matched = self::checkAllFilters( $vars, $group, $title, $mode );
if ( $isForEdit && $mode !== 'stash' ) {
$logger->info( __METHOD__ . ": cache miss for '$title' (key $stashKey)." );
$statsd->increment( 'abusefilter.check-stash.miss' );
@@ -969,11 +1162,11 @@ class AbuseFilter {
if ( count( $matched_filters ) == 0 ) {
$status = Status::newGood();
} else {
- $status = self::executeFilterActions( $matched_filters, $title, $vars );
+ $status = self::executeFilterActions( $matched_filters, $title, $vars, $user );
$actions_taken = $status->getValue();
$action = $vars->getVar( 'ACTION' )->toString();
- // If $wgUser isn't safe to load (e.g. a failure during
+ // If $user isn't safe to load (e.g. a failure during
// AbortAutoAccount), create a dummy anonymous user instead.
$user = $user->isSafeToLoad() ? $user : new User;
@@ -981,10 +1174,12 @@ class AbuseFilter {
$log_template = [
'afl_user' => $user->getId(),
'afl_user_text' => $user->getName(),
- 'afl_timestamp' => wfGetDB( DB_REPLICA )->timestamp( wfTimestampNow() ),
+ 'afl_timestamp' => wfGetDB( DB_REPLICA )->timestamp(),
'afl_namespace' => $title->getNamespace(),
'afl_title' => $title->getDBkey(),
- 'afl_ip' => $wgRequest->getIP()
+ 'afl_action' => $action,
+ // DB field is not null, so nothing
+ 'afl_ip' => ( $wgAbuseFilterLogIP ) ? $wgRequest->getIP() : ""
];
// Hack to avoid revealing IPs of people creating accounts
@@ -992,19 +1187,7 @@ class AbuseFilter {
$log_template['afl_user_text'] = $vars->getVar( 'accountname' )->toString();
}
- self::addLogEntries( $actions_taken, $log_template, $action, $vars, $group );
- }
-
- // Bug 53498: If we screwed around with $wgTitle, reset it so the title
- // is correctly picked up from the request later. Do the same for the
- // main RequestContext, because that might have picked up the bogus
- // title from $wgTitle.
- if ( $wgTitle !== $oldWgTitle ) {
- $wgTitle = $oldWgTitle;
- }
-
- if ( $context->getTitle() !== $oldContextTitle && $oldContextTitle instanceof Title ) {
- $context->setTitle( $oldContextTitle );
+ self::addLogEntries( $actions_taken, $log_template, $vars, $group );
}
return $status;
@@ -1022,18 +1205,31 @@ class AbuseFilter {
if ( $globalIndex ) {
// Global wiki filter
if ( !$wgAbuseFilterCentralDB ) {
- return null; // not enabled
+ return null;
}
$id = $globalIndex;
- $lb = wfGetLB( $wgAbuseFilterCentralDB );
+ $lbFactory = MediaWikiServices::getInstance()->getDBLoadBalancerFactory();
+ $lb = $lbFactory->getMainLB( $wgAbuseFilterCentralDB );
$dbr = $lb->getConnectionRef( DB_REPLICA, [], $wgAbuseFilterCentralDB );
} else {
// Local wiki filter
$dbr = wfGetDB( DB_REPLICA );
}
- $row = $dbr->selectRow( 'abuse_filter', '*', [ 'af_id' => $id ], __METHOD__ );
+ $fields = [
+ 'af_id',
+ 'af_pattern',
+ 'af_public_comments',
+ 'af_timestamp'
+ ];
+
+ $row = $dbr->selectRow(
+ 'abuse_filter',
+ $fields,
+ [ 'af_id' => $id ],
+ __METHOD__
+ );
self::$filterCache[$id] = $row ?: null;
}
@@ -1052,11 +1248,17 @@ class AbuseFilter {
) {
$inputVars = $vars->exportNonLazyVars();
// Exclude noisy fields that have superficial changes
- unset( $inputVars['old_html'] );
- unset( $inputVars['new_html'] );
- unset( $inputVars['user_age'] );
- unset( $inputVars['timestamp'] );
- unset( $inputVars['_VIEWS'] );
+ $excludedVars = [
+ 'old_html' => true,
+ 'new_html' => true,
+ 'user_age' => true,
+ 'timestamp' => true,
+ 'page_age' => true,
+ 'moved_from_age' => true,
+ 'moved_to_age' => true
+ ];
+
+ $inputVars = array_diff_key( $inputVars, $excludedVars );
ksort( $inputVars );
$hash = md5( serialize( $inputVars ) );
@@ -1072,14 +1274,10 @@ class AbuseFilter {
/**
* @param array[] $actions_taken
* @param array $log_template
- * @param string $action
* @param AbuseFilterVariableHolder $vars
* @param string $group The filter's group (as defined in $wgAbuseFilterValidGroups)
- * @return mixed
*/
- public static function addLogEntries( $actions_taken, $log_template, $action,
- $vars, $group = 'default'
- ) {
+ public static function addLogEntries( $actions_taken, $log_template, $vars, $group = 'default' ) {
$dbw = wfGetDB( DB_MASTER );
$central_log_template = [
@@ -1095,17 +1293,11 @@ class AbuseFilter {
$globalIndex = self::decodeGlobalName( $filter );
$thisLog = $log_template;
$thisLog['afl_filter'] = $filter;
- $thisLog['afl_action'] = $action;
$thisLog['afl_actions'] = implode( ',', $actions );
// Don't log if we were only throttling.
if ( $thisLog['afl_actions'] != 'throttle' ) {
$log_rows[] = $thisLog;
-
- if ( !$globalIndex ) {
- $logged_local_filters[] = $filter;
- }
-
// Global logging
if ( $globalIndex ) {
$title = Title::makeTitle( $thisLog['afl_namespace'], $thisLog['afl_title'] );
@@ -1116,6 +1308,8 @@ class AbuseFilter {
$central_log_rows[] = $centralLog;
$logged_global_filters[] = $globalIndex;
+ } else {
+ $logged_local_filters[] = $filter;
}
}
}
@@ -1126,7 +1320,8 @@ class AbuseFilter {
// Only store the var dump if we're actually going to add log rows.
$var_dump = self::storeVarDump( $vars );
- $var_dump = "stored-text:$var_dump"; // To distinguish from stuff stored directly
+ // To distinguish from stuff stored directly
+ $var_dump = "stored-text:$var_dump";
$stash = ObjectCache::getMainStashInstance();
@@ -1147,7 +1342,6 @@ class AbuseFilter {
$user = User::newFromId( $data['afl_user'] );
$user->setName( $data['afl_user_text'] );
$entry->setPerformer( $user );
- // Set action target
$entry->setTarget( Title::makeTitle( $data['afl_namespace'], $data['afl_title'] ) );
// Additional info
$entry->setParameters( [
@@ -1159,7 +1353,7 @@ class AbuseFilter {
// Send data to CheckUser if installed and we
// aren't already sending a notification to recentchanges
- if ( is_callable( 'CheckUserHooks::updateCheckUserData' )
+ if ( ExtensionRegistry::getInstance()->isLoaded( 'CheckUser' )
&& strpos( $wgAbuseFilterNotifications, 'rc' ) === false
) {
$rc = $entry->getRecentChange();
@@ -1205,7 +1399,7 @@ class AbuseFilter {
foreach ( $central_log_rows as $row ) {
$fdb->insert( 'abuse_filter_log', $row, __METHOD__ );
- $global_log_ids[] = $dbw->insertId();
+ $global_log_ids[] = $fdb->insertId();
}
$fdb->onTransactionPreCommitOrIdle(
@@ -1254,7 +1448,7 @@ class AbuseFilter {
}
}
- // Store to ES if applicable
+ // Store to ExternalStore if applicable
global $wgDefaultExternalStore, $wgAbuseFilterCentralDB;
if ( $wgDefaultExternalStore ) {
if ( $global ) {
@@ -1292,12 +1486,12 @@ class AbuseFilter {
* Retrieve a var dump from External Storage or the text table
* Some of this code is stolen from Revision::loadText et al
*
- * @param $stored_dump
+ * @param string $stored_dump
*
- * @return object|AbuseFilterVariableHolder|bool
+ * @return array|object|AbuseFilterVariableHolder|bool
*/
public static function loadVarDump( $stored_dump ) {
- // Back-compat
+ // Backward compatibility
if ( substr( $stored_dump, 0, strlen( 'stored-text:' ) ) !== 'stored-text:' ) {
$data = unserialize( $stored_dump );
if ( is_array( $data ) ) {
@@ -1346,6 +1540,10 @@ class AbuseFilter {
foreach ( $vars as $key => $value ) {
$obj->setVar( $key, $value );
}
+ // If old variable names are used, make sure to keep them
+ if ( count( array_intersect_key( self::getDeprecatedVariables(), $obj->mVars ) ) !== 0 ) {
+ $obj->mVarsVersion = 1;
+ }
}
return $obj;
@@ -1358,25 +1556,21 @@ class AbuseFilter {
* @param AbuseFilterVariableHolder $vars
* @param string $rule_desc
* @param int|string $rule_number
+ * @param User $user
*
* @return array|null a message describing the action that was taken,
* or null if no action was taken. The message is given as an array
* containing the message key followed by any message parameters.
- *
- * @note: Returning the message as an array instead of a Message object is
- * needed for compatibility with MW 1.20: we will be constructing a
- * Status object from these messages, and before 1.21, Status did
- * not accept Message objects to be added directly.
*/
public static function takeConsequenceAction( $action, $parameters, $title,
- $vars, $rule_desc, $rule_number ) {
+ $vars, $rule_desc, $rule_number, User $user ) {
global $wgAbuseFilterCustomActionsHandlers, $wgRequest;
$message = null;
switch ( $action ) {
case 'disallow':
- if ( strlen( $parameters[0] ) ) {
+ if ( isset( $parameters[0] ) ) {
$message = [ $parameters[0], $rule_desc, $rule_number ];
} else {
// Generic message.
@@ -1387,40 +1581,22 @@ class AbuseFilter {
];
}
break;
+ case 'rangeblock':
+ global $wgAbuseFilterRangeBlockSize, $wgBlockCIDRLimit;
- case 'block':
- global $wgAbuseFilterBlockDuration, $wgAbuseFilterAnonBlockDuration, $wgUser;
- if ( $wgUser->isAnon() && $wgAbuseFilterAnonBlockDuration !== null ) {
- // The user isn't logged in and the anon block duration
- // doesn't default to $wgAbuseFilterBlockDuration.
- $expiry = $wgAbuseFilterAnonBlockDuration;
+ $ip = $wgRequest->getIP();
+ if ( IP::isIPv6( $ip ) ) {
+ $CIDRsize = max( $wgAbuseFilterRangeBlockSize['IPv6'], $wgBlockCIDRLimit['IPv6'] );
} else {
- $expiry = $wgAbuseFilterBlockDuration;
+ $CIDRsize = max( $wgAbuseFilterRangeBlockSize['IPv4'], $wgBlockCIDRLimit['IPv4'] );
}
-
- self::doAbuseFilterBlock(
- [
- 'desc' => $rule_desc,
- 'number' => $rule_number
- ],
- $wgUser->getName(),
- $expiry,
- true
- );
-
- $message = [
- 'abusefilter-blocked-display',
- $rule_desc,
- $rule_number
- ];
- break;
- case 'rangeblock':
+ $blockCIDR = $ip . '/' . $CIDRsize;
self::doAbuseFilterBlock(
[
'desc' => $rule_desc,
'number' => $rule_number
],
- IP::sanitizeRange( $wgRequest->getIP() . '/16' ),
+ IP::sanitizeRange( $blockCIDR ),
'1 week',
false
);
@@ -1432,13 +1608,15 @@ class AbuseFilter {
];
break;
case 'degroup':
- global $wgUser;
- if ( !$wgUser->isAnon() ) {
- // Remove all groups from the user. Ouch.
- $groups = $wgUser->getGroups();
+ if ( !$user->isAnon() ) {
+ // Remove all groups from the user.
+ $groups = $user->getGroups();
+ // Make sure that the stored var dump contains user groups, since we may
+ // need them if reverting this degroup via Special:AbuseFilter/revert
+ $vars->setVar( 'user_groups', $groups );
foreach ( $groups as $group ) {
- $wgUser->removeGroup( $group );
+ $user->removeGroup( $group );
}
$message = [
@@ -1452,31 +1630,30 @@ class AbuseFilter {
break;
}
- // Log it.
- $log = new LogPage( 'rights' );
-
- $log->addEntry( 'rights',
- $wgUser->getUserPage(),
+ $logEntry = new ManualLogEntry( 'rights', 'rights' );
+ $logEntry->setPerformer( self::getFilterUser() );
+ $logEntry->setTarget( $user->getUserPage() );
+ $logEntry->setComment(
wfMessage(
'abusefilter-degroupreason',
$rule_desc,
$rule_number
- )->inContentLanguage()->text(),
- [
- implode( ', ', $groups ),
- ''
- ],
- self::getFilterUser()
+ )->inContentLanguage()->text()
);
+ $logEntry->setParameters( [
+ '4::oldgroups' => $groups,
+ '5::newgroups' => []
+ ] );
+ $logEntry->publish( $logEntry->insert() );
}
break;
case 'blockautopromote':
- global $wgUser;
- if ( !$wgUser->isAnon() ) {
- $blockPeriod = (int)mt_rand( 3 * 86400, 7 * 86400 ); // Block for 3-7 days.
+ if ( !$user->isAnon() ) {
+ // Block for 3-7 days.
+ $blockPeriod = (int)mt_rand( 3 * 86400, 7 * 86400 );
ObjectCache::getMainStashInstance()->set(
- self::autoPromoteBlockKey( $wgUser ), true, $blockPeriod
+ self::autoPromoteBlockKey( $user ), true, $blockPeriod
);
$message = [
@@ -1487,16 +1664,17 @@ class AbuseFilter {
}
break;
+ case 'block':
+ // Do nothing, handled at the end of executeFilterActions. Here for completeness.
+ break;
case 'flag':
// Do nothing. Here for completeness.
break;
case 'tag':
// Mark with a tag on recentchanges.
- global $wgUser;
-
$actionID = implode( '-', [
- $title->getPrefixedText(), $wgUser->getName(),
+ $title->getPrefixedText(), $user->getName(),
$vars->getVar( 'ACTION' )->toString()
] );
@@ -1520,7 +1698,8 @@ class AbuseFilter {
$message = [ $msg ];
}
} else {
- wfDebugLog( 'AbuseFilter', "Unrecognised action $action" );
+ $logger = LoggerFactory::getInstance( 'AbuseFilter' );
+ $logger->debug( "Unrecognised action $action" );
}
}
@@ -1531,11 +1710,14 @@ class AbuseFilter {
* @param array[] $tagsByAction Map of (integer => string[])
*/
private static function bufferTagsToSetByAction( array $tagsByAction ) {
- foreach ( $tagsByAction as $actionID => $tags ) {
- if ( !isset( self::$tagsToSet[$actionID] ) ) {
- self::$tagsToSet[$actionID] = $tags;
- } else {
- self::$tagsToSet[$actionID] = array_merge( self::$tagsToSet[$actionID], $tags );
+ global $wgAbuseFilterActions;
+ if ( isset( $wgAbuseFilterActions['tag'] ) && $wgAbuseFilterActions['tag'] ) {
+ foreach ( $tagsByAction as $actionID => $tags ) {
+ if ( !isset( self::$tagsToSet[$actionID] ) ) {
+ self::$tagsToSet[$actionID] = $tags;
+ } else {
+ self::$tagsToSet[$actionID] = array_merge( self::$tagsToSet[$actionID], $tags );
+ }
}
}
}
@@ -1546,8 +1728,15 @@ class AbuseFilter {
* @param string $target
* @param string $expiry
* @param bool $isAutoBlock
+ * @param bool $preventEditOwnUserTalk
*/
- protected static function doAbuseFilterBlock( array $rule, $target, $expiry, $isAutoBlock ) {
+ protected static function doAbuseFilterBlock(
+ array $rule,
+ $target,
+ $expiry,
+ $isAutoBlock,
+ $preventEditOwnUserTalk = false
+ ) {
$filterUser = self::getFilterUser();
$reason = wfMessage(
'abusefilter-blockreason',
@@ -1561,7 +1750,7 @@ class AbuseFilter {
$block->isHardblock( false );
$block->isAutoblocking( $isAutoBlock );
$block->prevents( 'createaccount', true );
- $block->prevents( 'editownusertalk', false );
+ $block->prevents( 'editownusertalk', $preventEditOwnUserTalk );
$block->mExpiry = SpecialBlock::parseExpiryInput( $expiry );
$success = $block->insert();
@@ -1577,6 +1766,9 @@ class AbuseFilter {
// Conditionally added same as SpecialBlock
$flags[] = 'noautoblock';
}
+ if ( $preventEditOwnUserTalk === true ) {
+ $flags[] = 'nousertalk';
+ }
$logParams['6::flags'] = implode( ',', $flags );
$logEntry = new ManualLogEntry( 'block', 'block' );
@@ -1591,8 +1783,8 @@ class AbuseFilter {
}
/**
- * @param $throttleId
- * @param $types
+ * @param string $throttleId
+ * @param string $types
* @param Title $title
* @param string $rateCount
* @param string $ratePeriod
@@ -1606,27 +1798,30 @@ class AbuseFilter {
$key = self::throttleKey( $throttleId, $types, $title, $global );
$count = intval( $stash->get( $key ) );
- wfDebugLog( 'AbuseFilter', "Got value $count for throttle key $key\n" );
+ $logger = LoggerFactory::getInstance( 'AbuseFilter' );
+ $logger->debug( "Got value $count for throttle key $key" );
if ( $count > 0 ) {
$stash->incr( $key );
$count++;
- wfDebugLog( 'AbuseFilter', "Incremented throttle key $key" );
+ $logger->debug( "Incremented throttle key $key" );
} else {
- wfDebugLog( 'AbuseFilter', "Added throttle key $key with value 1" );
+ $logger->debug( "Added throttle key $key with value 1" );
$stash->add( $key, 1, $ratePeriod );
$count = 1;
}
if ( $count > $rateCount ) {
- wfDebugLog( 'AbuseFilter', "Throttle $key hit value $count -- maximum is $rateCount." );
+ $logger->debug( "Throttle $key hit value $count -- maximum is $rateCount." );
- return true; // THROTTLED
+ // THROTTLED
+ return true;
}
- wfDebugLog( 'AbuseFilter', "Throttle $key not hit!" );
+ $logger->debug( "Throttle $key not hit!" );
- return false; // NOT THROTTLED
+ // NOT THROTTLED
+ return false;
}
/**
@@ -1669,7 +1864,7 @@ class AbuseFilter {
}
/**
- * @param $throttleId
+ * @param string $throttleId
* @param string $type
* @param Title $title
* @param bool $global
@@ -1732,14 +1927,13 @@ class AbuseFilter {
* @param string $group The filter's group (as defined in $wgAbuseFilterValidGroups)
*/
public static function recordStats( $filters, $group = 'default' ) {
- global $wgAbuseFilterConditionLimit;
+ global $wgAbuseFilterConditionLimit, $wgAbuseFilterProfileActionsCap;
$stash = ObjectCache::getMainStashInstance();
// Figure out if we've triggered overflows and blocks.
$overflow_triggered = ( self::$condCount > $wgAbuseFilterConditionLimit );
- // Store some keys...
$overflow_key = self::filterLimitReachedKey();
$total_key = self::filterUsedKey( $group );
@@ -1747,7 +1941,7 @@ class AbuseFilter {
$storage_period = self::$statsStoragePeriod;
- if ( !$total || $total > 10000 ) {
+ if ( !$total || $total > $wgAbuseFilterProfileActionsCap ) {
// This is for if the total doesn't exist, or has gone past 10,000.
// Recreate all the keys at the same time, so they expire together.
$stash->set( $total_key, 0, $storage_period );
@@ -1759,7 +1953,6 @@ class AbuseFilter {
$stash->set( self::filterMatchesKey(), 0, $storage_period );
}
- // Increment total
$stash->incr( $total_key );
// Increment overflow counter, if our condition limit overflowed
@@ -1795,7 +1988,7 @@ class AbuseFilter {
$stash = ObjectCache::getMainStashInstance();
foreach ( $filters as $filter ) {
- // determine emergency disable values for this action
+ // Determine emergency disable values for this action
$emergencyDisableThreshold =
self::getEmergencyValue( $wgAbuseFilterEmergencyDisableThreshold, $group );
$filterEmergencyDisableCount =
@@ -1848,7 +2041,7 @@ class AbuseFilter {
* @return mixed
*/
public static function getEmergencyValue( array $emergencyValue, $group ) {
- return isset( $emergencyValue[$group] ) ? $emergencyValue[$group] : $emergencyValue['default'];
+ return $emergencyValue[$group] ?? $emergencyValue['default'];
}
/**
@@ -1881,6 +2074,19 @@ class AbuseFilter {
$username = wfMessage( 'abusefilter-blocker' )->inContentLanguage()->text();
$user = User::newSystemUser( $username, [ 'steal' => true ] );
+ if ( !$user ) {
+ // User name is invalid. Don't throw because this is a system message, easy
+ // to change and make wrong either by mistake or intentionally to break the site.
+ wfWarn(
+ 'The AbuseFilter user\'s name is invalid. Please change it in ' .
+ 'MediaWiki:abusefilter-blocker'
+ );
+ // Use the default name to avoid breaking other stuff. This should have no harm,
+ // aside from blocks temporarily attributed to another user.
+ $defaultName = wfMessage( 'abusefilter-blocker' )->inLanguage( 'en' )->text();
+ $user = User::newSystemUser( $defaultName, [ 'steal' => true ] );
+ }
+
// Promote user to 'sysop' so it doesn't look
// like an unprivileged account is blocking users
if ( !in_array( 'sysop', $user->getGroups() ) ) {
@@ -1891,87 +2097,474 @@ class AbuseFilter {
}
/**
- * @param string $rules
- * @param string $textName
- * @param bool $addResultDiv
+ * Extract values for syntax highlight
+ *
* @param bool $canEdit
+ * @return array
+ */
+ public static function getAceConfig( $canEdit ) {
+ $values = self::getBuilderValues();
+ $deprecatedVars = self::getDeprecatedVariables();
+
+ $builderVariables = implode( '|', array_keys( $values['vars'] ) );
+ $builderFunctions = implode( '|', array_keys( AbuseFilterParser::$mFunctions ) );
+ // AbuseFilterTokenizer::$keywords also includes constants (true, false and null),
+ // but Ace redefines these constants afterwards so this will not be an issue
+ $builderKeywords = implode( '|', AbuseFilterTokenizer::$keywords );
+ // Extract operators from tokenizer like we do in AbuseFilterParserTest
+ $operators = implode( '|', array_map( function ( $op ) {
+ return preg_quote( $op, '/' );
+ }, AbuseFilterTokenizer::$operators ) );
+ $deprecatedVariables = implode( '|', array_keys( $deprecatedVars ) );
+ $disabledVariables = implode( '|', array_keys( self::$disabledVars ) );
+
+ return [
+ 'variables' => $builderVariables,
+ 'functions' => $builderFunctions,
+ 'keywords' => $builderKeywords,
+ 'operators' => $operators,
+ 'deprecated' => $deprecatedVariables,
+ 'disabled' => $disabledVariables,
+ 'aceReadOnly' => !$canEdit
+ ];
+ }
+
+ /**
+ * Build input and button for loading a filter
+ *
* @return string
*/
- static function buildEditBox( $rules, $textName = 'wpFilterRules', $addResultDiv = true,
- $canEdit = true ) {
- global $wgOut;
-
- $textareaAttrib = [ 'dir' => 'ltr' ]; # Rules are in English
- if ( !$canEdit ) {
- $textareaAttrib['readonly'] = 'readonly';
- }
-
- global $wgUser;
- $noTestAttrib = [];
- if ( !$wgUser->isAllowed( 'abusefilter-modify' ) ) {
- $noTestAttrib['disabled'] = 'disabled';
- $addResultDiv = false;
- }
-
- $rules = rtrim( $rules ) . "\n";
- $rules = Xml::textarea( $textName, $rules, 40, 15, $textareaAttrib );
-
- if ( $canEdit ) {
- $dropDown = self::getBuilderValues();
- // Generate builder drop-down
- $builder = '';
-
- $builder .= Xml::option( wfMessage( 'abusefilter-edit-builder-select' )->text() );
-
- foreach ( $dropDown as $group => $values ) {
- // Give grep a chance to find the usages:
- // abusefilter-edit-builder-group-op-arithmetic, abusefilter-edit-builder-group-op-comparison,
- // abusefilter-edit-builder-group-op-bool, abusefilter-edit-builder-group-misc,
- // abusefilter-edit-builder-group-funcs, abusefilter-edit-builder-group-vars
- $builder .=
- Xml::openElement(
- 'optgroup',
- [ 'label' => wfMessage( "abusefilter-edit-builder-group-$group" )->text() ]
- ) . "\n";
-
- foreach ( $values as $content => $name ) {
- $builder .=
- Xml::option(
- wfMessage( "abusefilter-edit-builder-$group-$name" )->text(),
- $content
- ) . "\n";
+ public static function buildFilterLoader() {
+ $loadText =
+ new OOUI\TextInputWidget(
+ [
+ 'type' => 'number',
+ 'name' => 'wpInsertFilter',
+ 'id' => 'mw-abusefilter-load-filter'
+ ]
+ );
+ $loadButton =
+ new OOUI\ButtonWidget(
+ [
+ 'label' => wfMessage( 'abusefilter-test-load' )->text(),
+ 'id' => 'mw-abusefilter-load'
+ ]
+ );
+ $loadGroup =
+ new OOUI\ActionFieldLayout(
+ $loadText,
+ $loadButton,
+ [
+ 'label' => wfMessage( 'abusefilter-test-load-filter' )->text()
+ ]
+ );
+ // CSS class for reducing default input field width
+ $loadDiv =
+ Xml::tags(
+ 'div',
+ [ 'class' => 'mw-abusefilter-load-filter-id' ],
+ $loadGroup
+ );
+ return $loadDiv;
+ }
+
+ /**
+ * Check whether a filter is allowed to use a tag
+ *
+ * @param string $tag Tag name
+ * @return Status
+ */
+ public static function isAllowedTag( $tag ) {
+ $tagNameStatus = ChangeTags::isTagNameValid( $tag );
+
+ if ( !$tagNameStatus->isGood() ) {
+ return $tagNameStatus;
+ }
+
+ $finalStatus = Status::newGood();
+
+ $canAddStatus =
+ ChangeTags::canAddTagsAccompanyingChange(
+ [ $tag ]
+ );
+
+ if ( $canAddStatus->isGood() ) {
+ return $finalStatus;
+ }
+
+ if ( $tag === 'abusefilter-condition-limit' ) {
+ $finalStatus->fatal( 'abusefilter-tag-reserved' );
+ 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;
+ }
+
+ /**
+ * Validate throttle parameters
+ *
+ * @param array $params Throttle parameters
+ * @return null|string Null on success, a string with the error message on failure
+ */
+ public static function checkThrottleParameters( $params ) {
+ $throttleRate = explode( ',', $params[1] );
+ $throttleCount = $throttleRate[0];
+ $throttlePeriod = $throttleRate[1];
+ $throttleGroups = array_slice( $params, 2 );
+ $validGroups = [
+ 'ip',
+ 'user',
+ 'range',
+ 'creationdate',
+ 'editcount',
+ 'site',
+ 'page'
+ ];
+
+ $error = null;
+ if ( preg_match( '/^[1-9][0-9]*$/', $throttleCount ) === 0 ) {
+ $error = 'abusefilter-edit-invalid-throttlecount';
+ } elseif ( preg_match( '/^[1-9][0-9]*$/', $throttlePeriod ) === 0 ) {
+ $error = 'abusefilter-edit-invalid-throttleperiod';
+ } elseif ( !$throttleGroups ) {
+ $error = 'abusefilter-edit-empty-throttlegroups';
+ } else {
+ $valid = true;
+ // Groups should be unique in three ways: no direct duplicates like 'user' and 'user',
+ // no duplicated subgroups, not even shuffled ('ip,user' and 'user,ip') and no duplicates
+ // within subgroups ('user,ip,user')
+ $uniqueGroups = [];
+ $uniqueSubGroups = true;
+ // Every group should be valid, and subgroups should have valid groups inside
+ foreach ( $throttleGroups as $group ) {
+ if ( strpos( $group, ',' ) !== false ) {
+ $subGroups = explode( ',', $group );
+ if ( $subGroups !== array_unique( $subGroups ) ) {
+ $uniqueSubGroups = false;
+ break;
+ }
+ foreach ( $subGroups as $subGroup ) {
+ if ( !in_array( $subGroup, $validGroups ) ) {
+ $valid = false;
+ break 2;
+ }
+ }
+ sort( $subGroups );
+ $uniqueGroups[] = implode( ',', $subGroups );
+ } else {
+ if ( !in_array( $group, $validGroups ) ) {
+ $valid = false;
+ break;
+ }
+ $uniqueGroups[] = $group;
+ }
+ }
+
+ if ( !$valid ) {
+ $error = 'abusefilter-edit-invalid-throttlegroups';
+ } elseif ( !$uniqueSubGroups || $uniqueGroups !== array_unique( $uniqueGroups ) ) {
+ $error = 'abusefilter-edit-duplicated-throttlegroups';
+ }
+ }
+
+ return $error;
+ }
+
+ /**
+ * Checks whether user input for the filter editing form is valid and if so saves the filter
+ *
+ * @param AbuseFilterViewEdit $page
+ * @param int|string $filter
+ * @param WebRequest $request
+ * @param stdClass $newRow
+ * @param array $actions
+ * @return Status
+ */
+ public static function saveFilter( $page, $filter, $request, $newRow, $actions ) {
+ $validationStatus = Status::newGood();
+
+ // Check the syntax
+ $syntaxerr = self::checkSyntax( $request->getVal( 'wpFilterRules' ) );
+ if ( $syntaxerr !== true ) {
+ $validationStatus->error( 'abusefilter-edit-badsyntax', $syntaxerr[0] );
+ return $validationStatus;
+ }
+ // Check for missing required fields (title and pattern)
+ $missing = [];
+ if ( !$request->getVal( 'wpFilterRules' ) ||
+ trim( $request->getVal( 'wpFilterRules' ) ) === '' ) {
+ $missing[] = wfMessage( 'abusefilter-edit-field-conditions' )->escaped();
+ }
+ if ( !$request->getVal( 'wpFilterDescription' ) ) {
+ $missing[] = wfMessage( 'abusefilter-edit-field-description' )->escaped();
+ }
+ if ( count( $missing ) !== 0 ) {
+ $missing = $page->getLanguage()->commaList( $missing );
+ $validationStatus->error( 'abusefilter-edit-missingfields', $missing );
+ return $validationStatus;
+ }
+
+ // Don't allow setting as deleted an active filter
+ if ( $request->getCheck( 'wpFilterEnabled' ) == true &&
+ $request->getCheck( 'wpFilterDeleted' ) == true ) {
+ $validationStatus->error( 'abusefilter-edit-deleting-enabled' );
+ return $validationStatus;
+ }
+
+ // If we've activated the 'tag' option, check the arguments for validity.
+ if ( !empty( $actions['tag'] ) ) {
+ foreach ( $actions['tag']['parameters'] as $tag ) {
+ $status = self::isAllowedTag( $tag );
+
+ if ( !$status->isGood() ) {
+ $err = $status->getErrors();
+ $msg = $err[0]['message'];
+ $validationStatus->error( $msg );
+ return $validationStatus;
}
+ }
+ }
- $builder .= Xml::closeElement( 'optgroup' ) . "\n";
+ // If 'throttle' is selected, check its parameters
+ if ( !empty( $actions['throttle'] ) ) {
+ $throttleCheck = self::checkThrottleParameters( $actions['throttle']['parameters'] );
+ if ( $throttleCheck !== null ) {
+ $validationStatus->error( $throttleCheck );
+ return $validationStatus;
}
+ }
+
+ $differences = self::compareVersions(
+ [ $newRow, $actions ],
+ [ $newRow->mOriginalRow, $newRow->mOriginalActions ]
+ );
- $rules .=
- Xml::tags(
- 'select',
- [ 'id' => 'wpFilterBuilder', ],
- $builder
- ) . ' ';
+ // Don't allow adding a new global rule, or updating a
+ // rule that is currently global, without permissions.
+ if ( !$page->canEditFilter( $newRow ) || !$page->canEditFilter( $newRow->mOriginalRow ) ) {
+ $validationStatus->fatal( 'abusefilter-edit-notallowed-global' );
+ return $validationStatus;
+ }
- // Add syntax checking
- $rules .= Xml::element( 'input',
- [
- 'type' => 'button',
- 'value' => wfMessage( 'abusefilter-edit-check' )->text(),
- 'id' => 'mw-abusefilter-syntaxcheck'
- ] + $noTestAttrib );
+ // Don't allow custom messages on global rules
+ if ( $newRow->af_global == 1 && (
+ $request->getVal( 'wpFilterWarnMessage' ) !== 'abusefilter-warning' ||
+ $request->getVal( 'wpFilterDisallowMessage' ) !== 'abusefilter-disallowed'
+ ) ) {
+ $validationStatus->fatal( 'abusefilter-edit-notallowed-global-custom-msg' );
+ return $validationStatus;
+ }
+
+ $origActions = $newRow->mOriginalActions;
+ $wasGlobal = (bool)$newRow->mOriginalRow->af_global;
+
+ unset( $newRow->mOriginalRow );
+ unset( $newRow->mOriginalActions );
+
+ // Check for non-changes
+ if ( !count( $differences ) ) {
+ $validationStatus->setResult( true, false );
+ return $validationStatus;
+ }
+
+ // Check for restricted actions
+ $restrictions = $page->getConfig()->get( 'AbuseFilterRestrictions' );
+ if ( count( array_intersect_key(
+ array_filter( $restrictions ),
+ array_merge(
+ array_filter( $actions ),
+ array_filter( $origActions )
+ )
+ ) )
+ && !$page->getUser()->isAllowed( 'abusefilter-modify-restricted' )
+ ) {
+ $validationStatus->error( 'abusefilter-edit-restricted' );
+ return $validationStatus;
+ }
+
+ // Everything went fine, so let's save the filter
+ list( $new_id, $history_id ) =
+ self::doSaveFilter( $newRow, $differences, $filter, $actions, $wasGlobal, $page );
+ $validationStatus->setResult( true, [ $new_id, $history_id ] );
+ return $validationStatus;
+ }
+
+ /**
+ * Saves new filter's info to DB
+ *
+ * @param stdClass $newRow
+ * @param int|string $filter
+ * @param array $differences
+ * @param array $actions
+ * @param bool $wasGlobal
+ * @param AbuseFilterViewEdit $page
+ * @return int[] first element is new ID, second is history ID
+ */
+ private static function doSaveFilter(
+ $newRow,
+ $differences,
+ $filter,
+ $actions,
+ $wasGlobal,
+ $page
+ ) {
+ $user = $page->getUser();
+ $dbw = wfGetDB( DB_MASTER );
+
+ // Convert from object to array
+ $newRow = get_object_vars( $newRow );
+
+ // Set last modifier.
+ $newRow['af_timestamp'] = $dbw->timestamp();
+ $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 = $filter;
+ $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;
+
+ // 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
+ $availableActions = $page->getConfig()->get( 'AbuseFilterActions' );
+ $actionsRows = [];
+ foreach ( array_filter( $availableActions ) as $action => $_ ) {
+ // Check if it's set
+ $enabled = isset( $actions[$action] ) && (bool)$actions[$action];
+
+ if ( $enabled ) {
+ $parameters = $actions[$action]['parameters'];
+ if ( $action === 'throttle' && $parameters[0] === 'new' ) {
+ // FIXME: Do we really need to keep the filter ID inside throttle parameters?
+ // We'd save space, keep things simpler and avoid this hack. Note: if removing
+ // it, a maintenance script will be necessary to clean up the table.
+ $parameters[0] = $new_id;
+ }
+
+ $thisRow = [
+ 'afa_filter' => $new_id,
+ 'afa_consequence' => $action,
+ 'afa_parameters' => implode( "\n", $parameters )
+ ];
+ $actionsRows[] = $thisRow;
+ }
+ }
+
+ // Create a history row
+ $afh_row = [];
+
+ foreach ( self::$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__ );
- if ( $addResultDiv ) {
- $rules .= Xml::element( 'div',
- [ 'id' => 'mw-abusefilter-syntaxresult', 'style' => 'display: none;' ],
- '&#160;' );
+ $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 = self::getGlobalRulesKey( $group );
+ ObjectCache::getMainWANInstance()->touchCheckKey( $globalRulesKey );
}
- // Add script
- $wgOut->addModules( 'ext.abuseFilter.edit' );
- self::$editboxName = $textName;
+ // Logging
+ $subtype = $filter === 'new' ? 'create' : 'modify';
+ $logEntry = new ManualLogEntry( 'abusefilter', $subtype );
+ $logEntry->setPerformer( $user );
+ $logEntry->setTarget( $page->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();
+ }
- return $rules;
+ self::resetFilterProfile( $new_id );
+ return [ $new_id, $history_id ];
}
/**
@@ -1983,7 +2576,7 @@ class AbuseFilter {
*
* @return array
*/
- static function compareVersions( $version_1, $version_2 ) {
+ public static function compareVersions( $version_1, $version_2 ) {
$compareFields = [
'af_public_comments',
'af_pattern',
@@ -2010,9 +2603,11 @@ class AbuseFilter {
if ( !isset( $actions1[$action] ) && !isset( $actions2[$action] ) ) {
// They're both unset
} elseif ( isset( $actions1[$action] ) && isset( $actions2[$action] ) ) {
- // They're both set.
+ // They're both set. Double check needed, e.g. per T180194
if ( array_diff( $actions1[$action]['parameters'],
- $actions2[$action]['parameters'] ) ) {
+ $actions2[$action]['parameters'] ) ||
+ array_diff( $actions2[$action]['parameters'],
+ $actions1[$action]['parameters'] ) ) {
// Different parameters
$differences[] = 'actions';
}
@@ -2029,28 +2624,28 @@ class AbuseFilter {
* @param stdClass $row
* @return array
*/
- static function translateFromHistory( $row ) {
- # Translate into an abuse_filter row with some black magic.
- # This is ever so slightly evil!
+ public static function translateFromHistory( $row ) {
+ // Manually translate into an abuse_filter row.
$af_row = new stdClass;
foreach ( self::$history_mappings as $af_col => $afh_col ) {
$af_row->$af_col = $row->$afh_col;
}
- # Process flags
-
+ // Process flags
$af_row->af_deleted = 0;
$af_row->af_hidden = 0;
$af_row->af_enabled = 0;
- $flags = explode( ',', $row->afh_flags );
- foreach ( $flags as $flag ) {
- $col_name = "af_$flag";
- $af_row->$col_name = 1;
+ if ( $row->afh_flags !== '' ) {
+ $flags = explode( ',', $row->afh_flags );
+ foreach ( $flags as $flag ) {
+ $col_name = "af_$flag";
+ $af_row->$col_name = 1;
+ }
}
- # Process actions
+ // Process actions
$actions_raw = unserialize( $row->afh_actions );
$actions_output = [];
if ( is_array( $actions_raw ) ) {
@@ -2069,13 +2664,15 @@ class AbuseFilter {
* @param string $action
* @return string
*/
- static function getActionDisplay( $action ) {
+ public static function getActionDisplay( $action ) {
// Give grep a chance to find the usages:
// abusefilter-action-tag, abusefilter-action-throttle, abusefilter-action-warn,
// abusefilter-action-blockautopromote, abusefilter-action-block, abusefilter-action-degroup,
// abusefilter-action-rangeblock, abusefilter-action-disallow
- $display = wfMessage( "abusefilter-action-$action" )->text();
- $display = wfMessage( "abusefilter-action-$action", $display )->isDisabled() ? $action : $display;
+ $display = wfMessage( "abusefilter-action-$action" )->escaped();
+ $display = wfMessage( "abusefilter-action-$action", $display )->isDisabled()
+ ? htmlspecialchars( $action )
+ : $display;
return $display;
}
@@ -2145,18 +2742,11 @@ class AbuseFilter {
$vars->addHolders(
self::generateUserVars( $user ),
- self::generateTitleVars( $title, 'ARTICLE' )
+ self::generateTitleVars( $title, 'PAGE' )
);
$vars->setVar( 'ACTION', 'delete' );
- if ( class_exists( CommentStore::class ) ) {
- $vars->setVar( 'SUMMARY', CommentStore::newKey( 'rc_comment' )
- // $row comes from RecentChange::selectFields()
- ->getCommentLegacy( wfGetDB( DB_REPLICA ), $row )->text
- );
- } else {
- $vars->setVar( 'SUMMARY', $row->rc_comment );
- }
+ $vars->setVar( 'SUMMARY', CommentStore::getStore()->getComment( 'rc_comment', $row )->text );
return $vars;
}
@@ -2178,18 +2768,11 @@ class AbuseFilter {
$vars->addHolders(
self::generateUserVars( $user ),
- self::generateTitleVars( $title, 'ARTICLE' )
+ self::generateTitleVars( $title, 'PAGE' )
);
$vars->setVar( 'ACTION', 'edit' );
- if ( class_exists( CommentStore::class ) ) {
- $vars->setVar( 'SUMMARY', CommentStore::newKey( 'rc_comment' )
- // $row comes from RecentChange::selectFields()
- ->getCommentLegacy( wfGetDB( DB_REPLICA ), $row )->text
- );
- } else {
- $vars->setVar( 'SUMMARY', $row->rc_comment );
- }
+ $vars->setVar( 'SUMMARY', CommentStore::getStore()->getComment( 'rc_comment', $row )->text );
$vars->setLazyLoadVar( 'new_wikitext', 'revision-text-by-id',
[ 'revid' => $row->rc_this_oldid ] );
@@ -2229,21 +2812,14 @@ class AbuseFilter {
self::generateTitleVars( $newTitle, 'MOVED_TO' )
);
- if ( class_exists( CommentStore::class ) ) {
- $vars->setVar( 'SUMMARY', CommentStore::newKey( 'rc_comment' )
- // $row comes from RecentChange::selectFields()
- ->getCommentLegacy( wfGetDB( DB_REPLICA ), $row )->text
- );
- } else {
- $vars->setVar( 'SUMMARY', $row->rc_comment );
- }
+ $vars->setVar( 'SUMMARY', CommentStore::getStore()->getComment( 'rc_comment', $row )->text );
$vars->setVar( 'ACTION', 'move' );
return $vars;
}
/**
- * @param Title $title
+ * @param Title|null $title
* @param Page|null $page
* @return AbuseFilterVariableHolder
*/
@@ -2255,13 +2831,13 @@ class AbuseFilter {
$page = WikiPage::factory( $title );
}
- $vars->setLazyLoadVar( 'edit_diff', 'diff',
+ $vars->setLazyLoadVar( 'edit_diff', 'diff-array',
[ 'oldtext-var' => 'old_wikitext', 'newtext-var' => 'new_wikitext' ] );
- $vars->setLazyLoadVar( 'edit_diff_pst', 'diff',
+ $vars->setLazyLoadVar( 'edit_diff_pst', 'diff-array',
[ 'oldtext-var' => 'old_wikitext', 'newtext-var' => 'new_pst' ] );
$vars->setLazyLoadVar( 'new_size', 'length', [ 'length-var' => 'new_wikitext' ] );
$vars->setLazyLoadVar( 'old_size', 'length', [ 'length-var' => 'old_wikitext' ] );
- $vars->setLazyLoadVar( 'edit_delta', 'subtract',
+ $vars->setLazyLoadVar( 'edit_delta', 'subtract-int',
[ 'val1-var' => 'new_size', 'val2-var' => 'old_size' ] );
// Some more specific/useful details about the changes.
@@ -2279,8 +2855,6 @@ class AbuseFilter {
[ 'oldlink-var' => 'old_links', 'newlink-var' => 'all_links' ] );
$vars->setLazyLoadVar( 'new_text', 'strip-html',
[ 'html-var' => 'new_html' ] );
- $vars->setLazyLoadVar( 'old_text', 'strip-html',
- [ 'html-var' => 'old_html' ] );
if ( $title instanceof Title ) {
$vars->setLazyLoadVar( 'all_links', 'links-from-wikitext',
@@ -2311,12 +2885,6 @@ class AbuseFilter {
'wikitext-var' => 'new_wikitext',
'article' => $page
] );
- $vars->setLazyLoadVar( 'old_html', 'parse-wikitext-nonedit',
- [
- 'namespace' => $title->getNamespace(),
- 'title' => $title->getText(),
- 'wikitext-var' => 'old_wikitext'
- ] );
}
return $vars;
@@ -2358,13 +2926,21 @@ class AbuseFilter {
}
// Now, build the body of the table.
+ $deprecatedVars = self::getDeprecatedVariables();
foreach ( $vars as $key => $value ) {
$key = strtolower( $key );
+ if ( array_key_exists( $key, $deprecatedVars ) ) {
+ $key = $deprecatedVars[$key];
+ }
if ( !empty( $variableMessageMappings[$key] ) ) {
$mapping = $variableMessageMappings[$key];
$keyDisplay = $context->msg( "abusefilter-edit-builder-vars-$mapping" )->parse() .
- ' ' . Xml::element( 'code', null, $context->msg( 'parentheses', $key )->text() );
+ ' ' . Xml::element( 'code', null, $context->msg( 'parentheses' )->rawParams( $key )->text() );
+ } elseif ( !empty( self::$disabledVars[$key] ) ) {
+ $mapping = self::$disabledVars[$key];
+ $keyDisplay = $context->msg( "abusefilter-edit-builder-vars-$mapping" )->parse() .
+ ' ' . Xml::element( 'code', null, $context->msg( 'parentheses' )->rawParams( $key )->text() );
} else {
$keyDisplay = Xml::element( 'code', null, $key );
}
@@ -2393,15 +2969,70 @@ class AbuseFilter {
* @param string[] $parameters
* @return string
*/
- static function formatAction( $action, $parameters ) {
+ public static function formatAction( $action, $parameters ) {
/** @var $wgLang Language */
global $wgLang;
- if ( count( $parameters ) == 0 ) {
+ if ( count( $parameters ) === 0 ||
+ ( $action === 'block' && count( $parameters ) !== 3 ) ) {
$displayAction = self::getActionDisplay( $action );
} else {
- $displayAction = self::getActionDisplay( $action ) .
+ if ( $action === 'block' ) {
+ // Needs to be treated separately since the message is more complex
+ $messages = [
+ wfMessage( 'abusefilter-block-anon' )->escaped() .
+ wfMessage( 'colon-separator' )->escaped() .
+ $wgLang->translateBlockExpiry( $parameters[1] ),
+ wfMessage( 'abusefilter-block-user' )->escaped() .
+ wfMessage( 'colon-separator' )->escaped() .
+ $wgLang->translateBlockExpiry( $parameters[2] )
+ ];
+ if ( $parameters[0] === 'blocktalk' ) {
+ $messages[] = wfMessage( 'abusefilter-block-talk' )->escaped();
+ }
+ $displayAction = $wgLang->commaList( $messages );
+ } elseif ( $action === 'throttle' ) {
+ array_shift( $parameters );
+ list( $actions, $time ) = explode( ',', array_shift( $parameters ) );
+
+ if ( $parameters === [ '' ] ) {
+ // Having empty groups won't happen for new filters due to validation upon saving,
+ // but old entries may have it. We'd better not show a broken message. Also,
+ // the array has an empty string inside because we haven't been passing an empty array
+ // as the default when retrieving wpFilterThrottleGroups with getArray (when it was
+ // a CheckboxMultiselect).
+ $groups = '';
+ } else {
+ // Join comma-separated groups in a commaList with a final "and", and convert to messages.
+ // Messages used here: abusefilter-throttle-ip, abusefilter-throttle-user,
+ // abusefilter-throttle-site, abusefilter-throttle-creationdate, abusefilter-throttle-editcount
+ // abusefilter-throttle-range, abusefilter-throttle-page
+ foreach ( $parameters as &$val ) {
+ if ( strpos( $val, ',' ) !== false ) {
+ $subGroups = explode( ',', $val );
+ foreach ( $subGroups as &$group ) {
+ $msg = wfMessage( "abusefilter-throttle-$group" );
+ // We previously accepted literally everything in this field, so old entries
+ // may have weird stuff.
+ $group = $msg->exists() ? $msg->text() : $group;
+ }
+ unset( $group );
+ $val = $wgLang->listToText( $subGroups );
+ } else {
+ $msg = wfMessage( "abusefilter-throttle-$val" );
+ $val = $msg->exists() ? $msg->text() : $val;
+ }
+ }
+ unset( $val );
+ $groups = $wgLang->semicolonList( $parameters );
+ }
+ $displayAction = self::getActionDisplay( $action ) .
wfMessage( 'colon-separator' )->escaped() .
- $wgLang->semicolonList( $parameters );
+ wfMessage( 'abusefilter-throttle-details' )->params( $actions, $time, $groups )->escaped();
+ } else {
+ $displayAction = self::getActionDisplay( $action ) .
+ wfMessage( 'colon-separator' )->escaped() .
+ $wgLang->semicolonList( array_map( 'htmlspecialchars', $parameters ) );
+ }
}
return $displayAction;
@@ -2411,13 +3042,13 @@ class AbuseFilter {
* @param string $value
* @return string
*/
- static function formatFlags( $value ) {
+ public static function formatFlags( $value ) {
/** @var $wgLang Language */
global $wgLang;
$flags = array_filter( explode( ',', $value ) );
$flags_display = [];
foreach ( $flags as $flag ) {
- $flags_display[] = wfMessage( "abusefilter-history-$flag" )->text();
+ $flags_display[] = wfMessage( "abusefilter-history-$flag" )->escaped();
}
return $wgLang->commaList( $flags_display );
@@ -2427,7 +3058,7 @@ class AbuseFilter {
* @param string $filterID
* @return string
*/
- static function getGlobalFilterDescription( $filterID ) {
+ public static function getGlobalFilterDescription( $filterID ) {
global $wgAbuseFilterCentralDB;
if ( !$wgAbuseFilterCentralDB ) {
@@ -2457,7 +3088,7 @@ class AbuseFilter {
* @param string $group The filter's group (as defined in $wgAbuseFilterValidGroups)
* @return string A name for that filter group, or the input.
*/
- static function nameGroup( $group ) {
+ public static function nameGroup( $group ) {
// Give grep a chance to find the usages: abusefilter-group-default
$msg = "abusefilter-group-$group";
@@ -2474,7 +3105,7 @@ class AbuseFilter {
* Note also that if the revision for any reason is not an Revision
* the function returns with an empty string.
*
- * @param Revision $revision a valid revision
+ * @param Revision|null $revision a valid revision
* @param int $audience one of:
* Revision::FOR_PUBLIC to be displayed to all users
* Revision::FOR_THIS_USER to be displayed to the given user
@@ -2482,7 +3113,7 @@ class AbuseFilter {
* @return string|null the content of the revision as some kind of string,
* or an empty string if it can not be found
*/
- static function revisionToString( $revision, $audience = Revision::FOR_THIS_USER ) {
+ public static function revisionToString( $revision, $audience = Revision::FOR_THIS_USER ) {
if ( !$revision instanceof Revision ) {
return '';
}
@@ -2509,7 +3140,7 @@ class AbuseFilter {
*
* @return string a suitable string representation of the content.
*/
- static function contentToString( Content $content ) {
+ public static function contentToString( Content $content ) {
$text = null;
if ( Hooks::run( 'AbuseFilter-contentToString', [ $content, &$text ] ) ) {
@@ -2518,21 +3149,15 @@ class AbuseFilter {
: $content->getTextForSearchIndex();
}
- if ( is_string( $text ) ) {
- // T22310
- // XXX: Is this really needed? Should we rather apply PST?
- $text = str_replace( "\r\n", "\n", $text );
- } else {
- $text = '';
- }
-
+ // T22310
+ $text = TextContent::normalizeLineEndings( (string)$text );
return $text;
}
- /*
+ /**
* Get the history ID of the first change to a given filter
*
- * @param int $filterId Filter id
+ * @param int $filterID Filter id
* @return int
*/
public static function getFirstFilterChange( $filterID ) {
diff --git a/AbuseFilter/includes/AbuseFilterChangesList.php b/AbuseFilter/includes/AbuseFilterChangesList.php
new file mode 100644
index 00000000..94855f34
--- /dev/null
+++ b/AbuseFilter/includes/AbuseFilterChangesList.php
@@ -0,0 +1,123 @@
+<?php
+
+class AbuseFilterChangesList extends OldChangesList {
+
+ /**
+ * @var string
+ */
+ private $testFilter;
+
+ /**
+ * @param Skin $skin
+ * @param string $testFilter
+ */
+ public function __construct( Skin $skin, $testFilter ) {
+ parent::__construct( $skin );
+ $this->testFilter = $testFilter;
+ }
+
+ /**
+ * @param string &$s
+ * @param RecentChange &$rc
+ * @param string[] &$classes
+ * @suppress PhanUndeclaredProperty for $rc->filterResult, which isn't a big deal
+ */
+ public function insertExtra( &$s, &$rc, &$classes ) {
+ if ( (int)$rc->getAttribute( 'rc_deleted' ) !== 0 ) {
+ $s .= ' ' . $this->msg( 'abusefilter-log-hidden-implicit' )->parse();
+ if ( !$this->userCan( $rc, Revision::SUPPRESSED_ALL ) ) {
+ return;
+ }
+ }
+
+ $examineParams = [];
+ if ( $this->testFilter ) {
+ $examineParams['testfilter'] = $this->testFilter;
+ }
+
+ $title = SpecialPage::getTitleFor( 'AbuseFilter', 'examine/' . $rc->mAttribs['rc_id'] );
+ $examineLink = $this->linkRenderer->makeLink(
+ $title,
+ new HtmlArmor( $this->msg( 'abusefilter-changeslist-examine' )->parse() ),
+ [],
+ $examineParams
+ );
+
+ $s .= ' ' . $this->msg( 'parentheses' )->rawParams( $examineLink )->escaped();
+
+ // Add CSS classes for match and not match
+ if ( isset( $rc->filterResult ) ) {
+ $class = $rc->filterResult ?
+ 'mw-abusefilter-changeslist-match' :
+ 'mw-abusefilter-changeslist-nomatch';
+
+ $classes[] = $class;
+ }
+ }
+
+ /**
+ * Insert links to user page, user talk page and eventually a blocking link.
+ * Like the parent, but don't hide details if user can see them.
+ *
+ * @param string &$s HTML to update
+ * @param RecentChange &$rc
+ */
+ public function insertUserRelatedLinks( &$s, &$rc ) {
+ $links = $this->getLanguage()->getDirMark() . Linker::userLink( $rc->mAttribs['rc_user'],
+ $rc->mAttribs['rc_user_text'] ) .
+ Linker::userToolLinks( $rc->mAttribs['rc_user'], $rc->mAttribs['rc_user_text'] );
+
+ if ( $this->isDeleted( $rc, Revision::DELETED_USER ) ) {
+ if ( $this->userCan( $rc, Revision::DELETED_USER ) ) {
+ $s .= ' <span class="history-deleted">' . $links . '</span>';
+ } else {
+ $s .= ' <span class="history-deleted">' .
+ $this->msg( 'rev-deleted-user' )->escaped() . '</span>';
+ }
+ } else {
+ $s .= $links;
+ }
+ }
+
+ /**
+ * Insert a formatted comment. Like the parent, but don't hide details if user can see them.
+ * @param RecentChange $rc
+ * @return string
+ */
+ public function insertComment( $rc ) {
+ if ( $this->isDeleted( $rc, Revision::DELETED_COMMENT ) ) {
+ if ( $this->userCan( $rc, Revision::DELETED_COMMENT ) ) {
+ return ' <span class="history-deleted">' .
+ Linker::commentBlock( $rc->mAttribs['rc_comment'], $rc->getTitle() ) . '</span>';
+ } else {
+ return ' <span class="history-deleted">' .
+ $this->msg( 'rev-deleted-comment' )->escaped() . '</span>';
+ }
+ } else {
+ return Linker::commentBlock( $rc->mAttribs['rc_comment'], $rc->getTitle() );
+ }
+ }
+
+ /**
+ * Insert a formatted action. The same as parent, but with a different audience in LogFormatter
+ *
+ * @param RecentChange $rc
+ * @return string
+ */
+ public function insertLogEntry( $rc ) {
+ $formatter = LogFormatter::newFromRow( $rc->mAttribs );
+ $formatter->setContext( $this->getContext() );
+ $formatter->setAudience( LogFormatter::FOR_THIS_USER );
+ $formatter->setShowUserToolLinks( true );
+ $mark = $this->getLanguage()->getDirMark();
+ return $formatter->getActionText() . " $mark" . $formatter->getComment();
+ }
+
+ /**
+ * @param string &$s
+ * @param RecentChange &$rc
+ */
+ public function insertRollback( &$s, &$rc ) {
+ // Kill rollback links.
+ }
+}
diff --git a/AbuseFilter/includes/AbuseFilter.hooks.php b/AbuseFilter/includes/AbuseFilterHooks.php
index 9ad8e076..20c07e4a 100644
--- a/AbuseFilter/includes/AbuseFilter.hooks.php
+++ b/AbuseFilter/includes/AbuseFilterHooks.php
@@ -1,23 +1,24 @@
<?php
-use MediaWiki\Auth\AuthManager;
+use MediaWiki\Linker\LinkRenderer;
use MediaWiki\MediaWikiServices;
+use Wikimedia\Rdbms\Database;
class AbuseFilterHooks {
const FETCH_ALL_TAGS_KEY = 'abusefilter-fetch-all-tags';
+ /** @var AbuseFilterVariableHolder|bool */
public static $successful_action_vars = false;
- /** @var WikiPage|Article|bool */
- public static $last_edit_page = false; // make sure edit filter & edit save hooks match
+ /** @var WikiPage|Article|bool|null Make sure edit filter & edit save hooks match */
+ public static $last_edit_page = false;
// So far, all of the error message out-params for these hooks accept HTML.
- // Hooray!
/**
* Called right after configuration has been loaded.
*/
public static function onRegistration() {
global $wgAbuseFilterAvailableActions, $wgAbuseFilterRestrictedActions,
- $wgAuthManagerAutoConfig;
+ $wgAuthManagerAutoConfig, $wgActionFilteredLogs;
if ( isset( $wgAbuseFilterAvailableActions ) || isset( $wgAbuseFilterRestrictedActions ) ) {
wfWarn( '$wgAbuseFilterAvailableActions and $wgAbuseFilterRestrictedActions have been '
@@ -28,8 +29,15 @@ class AbuseFilterHooks {
$wgAuthManagerAutoConfig['preauth'][AbuseFilterPreAuthenticationProvider::class] = [
'class' => AbuseFilterPreAuthenticationProvider::class,
- 'sort' => 5, // run after normal preauth providers to keep the log cleaner
+ // Run after normal preauth providers to keep the log cleaner
+ 'sort' => 5,
];
+
+ $wgActionFilteredLogs['suppress'] = array_merge(
+ $wgActionFilteredLogs['suppress'],
+ // Message: log-action-filter-suppress-abuselog
+ [ 'abuselog' => [ 'hide-afl', 'unhide-afl' ] ]
+ );
}
/**
@@ -41,7 +49,6 @@ class AbuseFilterHooks {
* @param string $summary Edit summary for page
* @param User $user the user performing the edit
* @param bool $minoredit whether this is a minor edit according to the user.
- * @return bool Always true
*/
public static function onEditFilterMergedContent( IContextSource $context, Content $content,
Status $status, $summary, User $user, $minoredit
@@ -54,8 +61,6 @@ class AbuseFilterHooks {
// Produce a useful error message for API edits
$status->apiHookResult = self::getApiResult( $filterStatus );
}
-
- return true;
}
/**
@@ -112,10 +117,10 @@ class AbuseFilterHooks {
// Load vars for filters to check
$vars = self::newVariableHolderForEdit(
- $user, $title, $page, $summary, $content, $oldcontent, $text
+ $user, $title, $page, $summary, $content, $text, $oldcontent
);
- $filter_result = AbuseFilter::filterAction( $vars, $title );
+ $filter_result = AbuseFilter::filterAction( $vars, $title, 'default', $user );
if ( !$filter_result->isOK() ) {
$status->merge( $filter_result );
@@ -134,19 +139,19 @@ class AbuseFilterHooks {
* @param WikiPage|null $page
* @param string $summary
* @param Content $newcontent
- * @param Content|null $oldcontent
* @param string $text
+ * @param Content|null $oldcontent
* @return AbuseFilterVariableHolder
* @throws MWException
*/
private static function newVariableHolderForEdit(
User $user, Title $title, $page, $summary, Content $newcontent,
- $oldcontent = null, $text
+ $text, $oldcontent = null
) {
$vars = new AbuseFilterVariableHolder();
$vars->addHolders(
AbuseFilter::generateUserVars( $user ),
- AbuseFilter::generateTitleVars( $title, 'ARTICLE' )
+ AbuseFilter::generateTitleVars( $title, 'PAGE' )
);
$vars->setVar( 'action', 'edit' );
$vars->setVar( 'summary', $summary );
@@ -189,7 +194,7 @@ class AbuseFilterHooks {
// The value is a nested structure keyed by filter id, which doesn't make sense when we only
// return the result from one filter. Flatten it to a plain array of actions.
$actionsTaken = array_values( array_unique(
- call_user_func_array( 'array_merge', array_values( $status->getValue() ) )
+ array_merge( ...array_values( $status->getValue() ) )
) );
$code = ( $actionsTaken === [ 'warn' ] ) ? 'abusefilter-warning' : 'abusefilter-disallowed';
@@ -212,7 +217,7 @@ class AbuseFilterHooks {
}
/**
- * @param Article|WikiPage $article
+ * @param WikiPage $wikiPage
* @param User $user
* @param string $content Content
* @param string $summary
@@ -223,29 +228,28 @@ class AbuseFilterHooks {
* @param Revision $revision
* @param Status $status
* @param int $baseRevId
- * @return bool
*/
public static function onPageContentSaveComplete(
- &$article, &$user, $content, $summary, $minoredit, $watchthis, $sectionanchor,
- &$flags, $revision, &$status, $baseRevId
+ WikiPage $wikiPage, $user, $content, $summary, $minoredit, $watchthis, $sectionanchor,
+ $flags, $revision, $status, $baseRevId
) {
if ( !self::$successful_action_vars || !$revision ) {
self::$successful_action_vars = false;
-
- return true;
+ return;
}
- /** @var AbuseFilterVariableHolder $vars */
+ /** @var AbuseFilterVariableHolder|bool $vars */
$vars = self::$successful_action_vars;
- if ( $vars->getVar( 'article_prefixedtext' )->toString() !==
- $article->getTitle()->getPrefixedText()
+ if ( $vars->getVar( 'page_prefixedtitle' )->toString() !==
+ $wikiPage->getTitle()->getPrefixedText()
) {
- return true;
+ return;
}
- if ( !self::identicalPageObjects( $article, self::$last_edit_page ) ) {
- return true; // this isn't the edit $successful_action_vars was set for
+ if ( !self::identicalPageObjects( $wikiPage, self::$last_edit_page ) ) {
+ // This isn't the edit $successful_action_vars was set for
+ return;
}
self::$last_edit_page = false;
@@ -254,7 +258,7 @@ class AbuseFilterHooks {
$log_ids = $vars->getVar( 'local_log_ids' )->toNative();
$dbw = wfGetDB( DB_MASTER );
- if ( count( $log_ids ) ) {
+ if ( $log_ids !== null && count( $log_ids ) ) {
$dbw->update( 'abuse_filter_log',
[ 'afl_rev_id' => $revision->getId() ],
[ 'afl_id' => $log_ids ],
@@ -266,7 +270,7 @@ class AbuseFilterHooks {
if ( $vars->getVar( 'global_log_ids' ) ) {
$log_ids = $vars->getVar( 'global_log_ids' )->toNative();
- if ( count( $log_ids ) ) {
+ if ( $log_ids !== null && count( $log_ids ) ) {
global $wgAbuseFilterCentralDB;
$fdb = wfGetDB( DB_MASTER, [], $wgAbuseFilterCentralDB );
@@ -277,8 +281,6 @@ class AbuseFilterHooks {
);
}
}
-
- return true;
}
/**
@@ -296,8 +298,7 @@ class AbuseFilterHooks {
/**
* @param User $user
- * @param array $promote
- * @return bool
+ * @param array &$promote
*/
public static function onGetAutoPromoteGroups( $user, &$promote ) {
if ( $promote ) {
@@ -314,8 +315,6 @@ class AbuseFilterHooks {
$promote = [];
}
}
-
- return true;
}
/**
@@ -338,7 +337,7 @@ class AbuseFilterHooks {
$vars->setVar( 'SUMMARY', $reason );
$vars->setVar( 'ACTION', 'move' );
- $result = AbuseFilter::filterAction( $vars, $oldTitle );
+ $result = AbuseFilter::filterAction( $vars, $oldTitle, 'default', $user );
$status->merge( $result );
return $result->isOK();
@@ -348,22 +347,22 @@ class AbuseFilterHooks {
* @param WikiPage $article
* @param User $user
* @param string $reason
- * @param string $error
+ * @param string &$error
* @param Status $status
* @return bool
*/
- public static function onArticleDelete( &$article, &$user, &$reason, &$error, &$status ) {
+ public static function onArticleDelete( $article, $user, $reason, &$error, $status ) {
$vars = new AbuseFilterVariableHolder;
$vars->addHolders(
AbuseFilter::generateUserVars( $user ),
- AbuseFilter::generateTitleVars( $article->getTitle(), 'ARTICLE' )
+ AbuseFilter::generateTitleVars( $article->getTitle(), 'PAGE' )
);
$vars->setVar( 'SUMMARY', $reason );
$vars->setVar( 'ACTION', 'delete' );
- $filter_result = AbuseFilter::filterAction( $vars, $article->getTitle() );
+ $filter_result = AbuseFilter::filterAction( $vars, $article->getTitle(), 'default', $user );
$status->merge( $filter_result );
$error = $filter_result->isOK() ? '' : $filter_result->getHTML();
@@ -372,65 +371,7 @@ class AbuseFilterHooks {
}
/**
- * @param User $user
- * @param string $message
- * @param bool $autocreate Indicates whether the account is created automatically.
- * @return bool
- * @deprecated AbuseFilterPreAuthenticationProvider will take over this functionality
- */
- private static function checkNewAccount( $user, &$message, $autocreate ) {
- if ( $user->getName() == wfMessage( 'abusefilter-blocker' )->inContentLanguage()->text() ) {
- $message = wfMessage( 'abusefilter-accountreserved' )->text();
-
- return false;
- }
-
- $vars = new AbuseFilterVariableHolder;
-
- // Add variables only for a registered user, so IP addresses of
- // new users won't be exposed
- global $wgUser;
- if ( !$autocreate && $wgUser->getId() ) {
- $vars->addHolders( AbuseFilter::generateUserVars( $wgUser ) );
- }
-
- $vars->setVar( 'ACTION', $autocreate ? 'autocreateaccount' : 'createaccount' );
- $vars->setVar( 'ACCOUNTNAME', $user->getName() );
-
- $filter_result = AbuseFilter::filterAction(
- $vars, SpecialPage::getTitleFor( 'Userlogin' ) );
-
- $message = $filter_result->isOK() ? '' : $filter_result->getWikiText();
-
- return $filter_result->isOK();
- }
-
- /**
- * @param User $user
- * @param string $message
- * @return bool
- * @deprecated AbuseFilterPreAuthenticationProvider will take over this functionality
- */
- public static function onAbortNewAccount( $user, &$message ) {
- return self::checkNewAccount( $user, $message, false );
- }
-
- /**
- * @param User $user
- * @param string $message
- * @return bool
- * @deprecated AbuseFilterPreAuthenticationProvider will take over this functionality
- */
- public static function onAbortAutoAccount( $user, &$message ) {
- // FIXME: ERROR MESSAGE IS SHOWN IN A WEIRD WAY, BEACUSE $message
- // HERE MEANS NAME OF THE MESSAGE, NOT THE TEXT OF THE MESSAGE AS
- // IN AbortNewAccount HOOK WHICH WE CANNOT PROVIDE!
- return self::checkNewAccount( $user, $message, true );
- }
-
- /**
* @param RecentChange $recentChange
- * @return bool
*/
public static function onRecentChangeSave( $recentChange ) {
$title = Title::makeTitle(
@@ -440,14 +381,12 @@ class AbuseFilterHooks {
$action = $recentChange->mAttribs['rc_log_type'] ?
$recentChange->mAttribs['rc_log_type'] : 'edit';
$actionID = implode( '-', [
- $title->getPrefixedText(), $recentChange->mAttribs['rc_user_text'], $action
+ $title->getPrefixedText(), $recentChange->getAttribute( 'rc_user_text' ), $action
] );
if ( isset( AbuseFilter::$tagsToSet[$actionID] ) ) {
$recentChange->addTags( AbuseFilter::$tagsToSet[$actionID] );
}
-
- return true;
}
/**
@@ -471,11 +410,11 @@ class AbuseFilterHooks {
/**
* @param array $tags
* @param bool $enabled
- * @return bool
*/
private static function fetchAllTags( array &$tags, $enabled ) {
$services = MediaWikiServices::getInstance();
$cache = $services->getMainWANObjectCache();
+ $fname = __METHOD__;
$tags = $cache->getWithSetCallback(
// Key to store the cached value under
@@ -485,14 +424,14 @@ class AbuseFilterHooks {
$cache::TTL_MINUTE,
// Function that derives the new key value
- function ( $oldValue, &$ttl, array &$setOpts ) use ( $enabled, $tags ) {
+ function ( $oldValue, &$ttl, array &$setOpts ) use ( $enabled, $tags, $fname ) {
global $wgAbuseFilterCentralDB, $wgAbuseFilterIsCentral;
$dbr = wfGetDB( DB_REPLICA );
// Account for any snapshot/replica DB lag
$setOpts += Database::getCacheSetOptions( $dbr );
- # This is a pretty awful hack.
+ // This is a pretty awful hack.
$where = [ 'afa_consequence' => 'tag', 'af_deleted' => false ];
if ( $enabled ) {
@@ -502,7 +441,7 @@ class AbuseFilterHooks {
[ 'abuse_filter_action', 'abuse_filter' ],
'afa_parameters',
$where,
- __METHOD__,
+ $fname,
[],
[ 'abuse_filter' => [ 'INNER JOIN', 'afa_filter=af_id' ] ]
);
@@ -520,7 +459,7 @@ class AbuseFilterHooks {
[ 'abuse_filter_action', 'abuse_filter' ],
'afa_parameters',
$where,
- __METHOD__,
+ $fname,
[],
[ 'abuse_filter' => [ 'INNER JOIN', 'afa_filter=af_id' ] ]
);
@@ -536,31 +475,28 @@ class AbuseFilterHooks {
}
);
- return true;
+ $tags[] = 'abusefilter-condition-limit';
}
/**
- * @param string[] $tags
- * @return bool
+ * @param string[] &$tags
*/
public static function onListDefinedTags( array &$tags ) {
- return self::fetchAllTags( $tags, false );
+ self::fetchAllTags( $tags, false );
}
/**
- * @param string[] $tags
- * @return bool
+ * @param string[] &$tags
*/
public static function onChangeTagsListActive( array &$tags ) {
- return self::fetchAllTags( $tags, true );
+ self::fetchAllTags( $tags, true );
}
/**
- * @param DatabaseUpdater|null $updater
+ * @param DatabaseUpdater $updater
* @throws MWException
- * @return bool
*/
- public static function onLoadExtensionSchemaUpdates( $updater = null ) {
+ public static function onLoadExtensionSchemaUpdates( DatabaseUpdater $updater ) {
$dir = dirname( __DIR__ );
if ( $updater->getDB()->getType() == 'mysql' || $updater->getDB()->getType() == 'sqlite' ) {
@@ -649,22 +585,74 @@ class AbuseFilterHooks {
$updater->addExtensionUpdate( [
'addPgField', 'abuse_filter', 'af_global', 'SMALLINT NOT NULL DEFAULT 0' ] );
$updater->addExtensionUpdate( [
+ 'addPgField', 'abuse_filter', 'af_group', "TEXT NOT NULL DEFAULT 'default'" ] );
+ $updater->addExtensionUpdate( [
+ 'addPgExtIndex', 'abuse_filter', 'abuse_filter_group_enabled_id',
+ "(af_group, af_enabled, af_id)"
+ ] );
+ $updater->addExtensionUpdate( [
+ 'addPgField', 'abuse_filter_history', 'afh_group', "TEXT" ] );
+ $updater->addExtensionUpdate( [
'addPgField', 'abuse_filter_log', 'afl_wiki', 'TEXT' ] );
$updater->addExtensionUpdate( [
'addPgField', 'abuse_filter_log', 'afl_deleted', 'SMALLINT' ] );
$updater->addExtensionUpdate( [
+ 'setDefault', 'abuse_filter_log', 'afl_deleted', '0' ] );
+ $updater->addExtensionUpdate( [
+ 'changeNullableField', 'abuse_filter_log', 'afl_deleted', 'NOT NULL', true ] );
+ $updater->addExtensionUpdate( [
+ 'addPgField', 'abuse_filter_log', 'afl_patrolled_by', 'INTEGER' ] );
+ $updater->addExtensionUpdate( [
+ 'addPgField', 'abuse_filter_log', 'afl_rev_id', 'INTEGER' ] );
+ $updater->addExtensionUpdate( [
+ 'addPgField', 'abuse_filter_log', 'afl_log_id', 'INTEGER' ] );
+ $updater->addExtensionUpdate( [
'changeField', 'abuse_filter_log', 'afl_filter', 'TEXT', '' ] );
$updater->addExtensionUpdate( [
- 'addPgExtIndex', 'abuse_filter_log', 'abuse_filter_log_ip', "(afl_ip)" ] );
+ 'changeField', 'abuse_filter_log', 'afl_namespace', "INTEGER", '' ] );
+ $updater->addExtensionUpdate( [
+ 'dropPgIndex', 'abuse_filter_log', 'abuse_filter_log_filter' ] );
+ $updater->addExtensionUpdate( [
+ 'dropPgIndex', 'abuse_filter_log', 'abuse_filter_log_ip' ] );
+ $updater->addExtensionUpdate( [
+ 'dropPgIndex', 'abuse_filter_log', 'abuse_filter_log_title' ] );
+ $updater->addExtensionUpdate( [
+ 'dropPgIndex', 'abuse_filter_log', 'abuse_filter_log_user' ] );
+ $updater->addExtensionUpdate( [
+ 'dropPgIndex', 'abuse_filter_log', 'abuse_filter_log_user_text' ] );
+ $updater->addExtensionUpdate( [
+ 'dropPgIndex', 'abuse_filter_log', 'abuse_filter_log_wiki' ] );
+ $updater->addExtensionUpdate( [
+ 'addPgExtIndex', 'abuse_filter_log', 'abuse_filter_log_filter_timestamp',
+ '(afl_filter,afl_timestamp)'
+ ] );
+ $updater->addExtensionUpdate( [
+ 'addPgExtIndex', 'abuse_filter_log', 'abuse_filter_log_user_timestamp',
+ '(afl_user,afl_user_text,afl_timestamp)'
+ ] );
+ $updater->addExtensionUpdate( [
+ 'addPgExtIndex', 'abuse_filter_log', 'abuse_filter_log_page_timestamp',
+ '(afl_namespace,afl_title,afl_timestamp)'
+ ] );
+ $updater->addExtensionUpdate( [
+ 'addPgExtIndex', 'abuse_filter_log', 'abuse_filter_log_ip_timestamp',
+ '(afl_ip, afl_timestamp)'
+ ] );
$updater->addExtensionUpdate( [
- 'addPgExtIndex', 'abuse_filter_log', 'abuse_filter_log_wiki', "(afl_wiki)" ] );
+ 'addPgExtIndex', 'abuse_filter_log', 'abuse_filter_log_rev_id',
+ '(afl_rev_id)'
+ ] );
+ $updater->addExtensionUpdate( [
+ 'addPgExtIndex', 'abuse_filter_log', 'abuse_filter_log_log_id',
+ '(afl_log_id)'
+ ] );
$updater->addExtensionUpdate( [
- 'changeField', 'abuse_filter_log', 'afl_namespace', "INTEGER" ] );
+ 'addPgExtIndex', 'abuse_filter_log', 'abuse_filter_log_wiki_timestamp',
+ '(afl_wiki,afl_timestamp)'
+ ] );
}
$updater->addExtensionUpdate( [ [ __CLASS__, 'createAbuseFilterUser' ] ] );
-
- return true;
}
/**
@@ -678,26 +666,48 @@ class AbuseFilterHooks {
if ( $user && !$updater->updateRowExists( 'create abusefilter-blocker-user' ) ) {
$user = User::newSystemUser( $username, [ 'steal' => true ] );
$updater->insertUpdateRow( 'create abusefilter-blocker-user' );
- # Promote user so it doesn't look too crazy.
+ // Promote user so it doesn't look too crazy.
$user->addGroup( 'sysop' );
}
}
/**
- * @param $id
+ * @param int $id
* @param Title $nt
- * @param array $tools
+ * @param array &$tools
* @param SpecialPage $sp for context
*/
- public static function onContributionsToolLinks( $id, $nt, &$tools, SpecialPage $sp ) {
- if ( $sp->getUser()->isAllowed( 'abusefilter-log' ) ) {
+ public static function onContributionsToolLinks( $id, $nt, array &$tools, SpecialPage $sp ) {
+ $username = $nt->getText();
+ if ( $sp->getUser()->isAllowed( 'abusefilter-log' ) && !IP::isValidRange( $username ) ) {
$linkRenderer = $sp->getLinkRenderer();
$tools['abuselog'] = $linkRenderer->makeLink(
SpecialPage::getTitleFor( 'AbuseLog' ),
$sp->msg( 'abusefilter-log-linkoncontribs' )->text(),
[ 'title' => $sp->msg( 'abusefilter-log-linkoncontribs-text',
- $nt->getText() )->text() ],
- [ 'wpSearchUser' => $nt->getText() ]
+ $username )->text() ],
+ [ 'wpSearchUser' => $username ]
+ );
+ }
+ }
+
+ /**
+ * @param IContextSource $context
+ * @param LinkRenderer $linkRenderer
+ * @param string[] &$links
+ */
+ public static function onHistoryPageToolLinks(
+ IContextSource $context,
+ LinkRenderer $linkRenderer,
+ array &$links
+ ) {
+ $user = $context->getUser();
+ if ( $user->isAllowed( 'abusefilter-log' ) ) {
+ $links[] = $linkRenderer->makeLink(
+ SpecialPage::getTitleFor( 'AbuseLog' ),
+ $context->msg( 'abusefilter-log-linkonhistory' )->text(),
+ [ 'title' => $context->msg( 'abusefilter-log-linkonhistory-text' )->text() ],
+ [ 'wpSearchTitle' => $context->getTitle()->getPrefixedText() ]
);
}
}
@@ -756,7 +766,7 @@ class AbuseFilterHooks {
$vars = new AbuseFilterVariableHolder;
$vars->addHolders(
AbuseFilter::generateUserVars( $user ),
- AbuseFilter::generateTitleVars( $title, 'ARTICLE' )
+ AbuseFilter::generateTitleVars( $title, 'PAGE' )
);
$vars->setVar( 'ACTION', $action );
@@ -768,7 +778,11 @@ class AbuseFilterHooks {
$vars->setVar( 'file_size', $upload->getFileSize() );
$vars->setVar( 'file_mime', $props['mime'] );
- $vars->setVar( 'file_mediatype', MimeMagic::singleton()->getMediaType( null, $props['mime'] ) );
+ $vars->setVar(
+ 'file_mediatype',
+ MediaWiki\MediaWikiServices::getInstance()->getMimeAnalyzer()
+ ->getMediaType( null, $props['mime'] )
+ );
$vars->setVar( 'file_width', $props['width'] );
$vars->setVar( 'file_height', $props['height'] );
$vars->setVar( 'file_bits_per_channel', $props['bits'] );
@@ -806,7 +820,7 @@ class AbuseFilterHooks {
$vars->addHolders( AbuseFilter::getEditVars( $title, $page ) );
}
- $filter_result = AbuseFilter::filterAction( $vars, $title );
+ $filter_result = AbuseFilter::filterAction( $vars, $title, 'default', $user );
if ( !$filter_result->isOK() ) {
$messageAndParams = $filter_result->getErrorsArray()[0];
@@ -824,8 +838,7 @@ class AbuseFilterHooks {
/**
* Adds global variables to the Javascript as needed
*
- * @param array $vars
- * @return bool
+ * @param array &$vars
*/
public static function onMakeGlobalVariablesScript( array &$vars ) {
if ( isset( AbuseFilter::$editboxName ) && AbuseFilter::$editboxName !== null ) {
@@ -838,22 +851,17 @@ class AbuseFilterHooks {
'id' => AbuseFilterViewExamine::$examineId,
];
}
-
- return true;
}
/**
* Tables that Extension:UserMerge needs to update
*
- * @param array $updateFields
- * @return bool
+ * @param array &$updateFields
*/
public static function onUserMergeAccountFields( array &$updateFields ) {
$updateFields[] = [ 'abuse_filter', 'af_user', 'af_user_text' ];
$updateFields[] = [ 'abuse_filter_log', 'afl_user', 'afl_user_text' ];
$updateFields[] = [ 'abuse_filter_history', 'afh_user', 'afh_user_text' ];
-
- return true;
}
/**
@@ -863,7 +871,7 @@ class AbuseFilterHooks {
* @param Content $content
* @param ParserOutput $output
* @param string $summary
- * @param User $user
+ * @param User|null $user
*/
public static function onParserOutputStashForEdit(
WikiPage $page, Content $content, ParserOutput $output, $summary = '', $user = null
@@ -880,9 +888,9 @@ class AbuseFilterHooks {
// Cache any resulting filter matches.
// Do this outside the synchronous stash lock to avoid any chance of slowdown.
DeferredUpdates::addCallableUpdate(
- function () use ( $user, $page, $summary, $content, $oldcontent, $text ) {
+ function () use ( $user, $page, $summary, $content, $text, $oldcontent ) {
$vars = self::newVariableHolderForEdit(
- $user, $page->getTitle(), $page, $summary, $content, $oldcontent, $text
+ $user, $page->getTitle(), $page, $summary, $content, $text, $oldcontent
);
AbuseFilter::filterAction( $vars, $page->getTitle(), 'default', $user, 'stash' );
},
diff --git a/AbuseFilter/includes/AbuseFilterModifyLogFormatter.php b/AbuseFilter/includes/AbuseFilterModifyLogFormatter.php
index 8a148f23..769c27d3 100644
--- a/AbuseFilter/includes/AbuseFilterModifyLogFormatter.php
+++ b/AbuseFilter/includes/AbuseFilterModifyLogFormatter.php
@@ -2,8 +2,15 @@
class AbuseFilterModifyLogFormatter extends LogFormatter {
+ /**
+ * @return string
+ */
protected function getMessageKey() {
- return 'abusefilter-logentry-modify';
+ $subtype = $this->entry->getSubtype();
+ // Messages that can be used here:
+ // * abusefilter-logentry-create
+ // * abusefilter-logentry-modify
+ return "abusefilter-logentry-$subtype";
}
/**
diff --git a/AbuseFilter/includes/AbuseFilterPreAuthenticationProvider.php b/AbuseFilter/includes/AbuseFilterPreAuthenticationProvider.php
index 9058c6ee..ed72c5a4 100644
--- a/AbuseFilter/includes/AbuseFilterPreAuthenticationProvider.php
+++ b/AbuseFilter/includes/AbuseFilterPreAuthenticationProvider.php
@@ -1,12 +1,25 @@
<?php
use MediaWiki\Auth\AbstractPreAuthenticationProvider;
+use MediaWiki\Auth\AuthenticationRequest;
class AbuseFilterPreAuthenticationProvider extends AbstractPreAuthenticationProvider {
+ /**
+ * @param User $user
+ * @param User $creator
+ * @param AuthenticationRequest[] $reqs
+ * @return StatusValue
+ */
public function testForAccountCreation( $user, $creator, array $reqs ) {
return $this->testUser( $user, $creator, false );
}
+ /**
+ * @param User $user
+ * @param bool|string $autocreate
+ * @param array $options
+ * @return StatusValue
+ */
public function testUserForCreation( $user, $autocreate, array $options = [] ) {
// if this is not an autocreation, testForAccountCreation already handled it
if ( $autocreate ) {
diff --git a/AbuseFilter/includes/AbuseFilterSuppressLogFormatter.php b/AbuseFilter/includes/AbuseFilterSuppressLogFormatter.php
new file mode 100644
index 00000000..b547b02b
--- /dev/null
+++ b/AbuseFilter/includes/AbuseFilterSuppressLogFormatter.php
@@ -0,0 +1,16 @@
+<?php
+
+class AbuseFilterSuppressLogFormatter extends LogFormatter {
+
+ /**
+ * @return string
+ */
+ protected function getMessageKey() {
+ if ( $this->entry->getSubtype() === 'unhide-afl' ) {
+ return 'abusefilter-log-entry-unsuppress';
+ } else {
+ return 'abusefilter-log-entry-suppress';
+ }
+ }
+
+}
diff --git a/AbuseFilter/includes/AbuseFilterVariableHolder.php b/AbuseFilter/includes/AbuseFilterVariableHolder.php
index 8a1a1ee4..498c7b4e 100644
--- a/AbuseFilter/includes/AbuseFilterVariableHolder.php
+++ b/AbuseFilter/includes/AbuseFilterVariableHolder.php
@@ -1,9 +1,17 @@
<?php
class AbuseFilterVariableHolder {
+ /** @var (AFPData|AFComputedVariable)[] */
public $mVars = [];
- public static $varBlacklist = [ 'context' ];
+ /** @var string[] Variables used to store meta-data, we'd better be safe. See T191715 */
+ public static $varBlacklist = [ 'context', 'global_log_ids', 'local_log_ids' ];
+
+ /** @var int 2 is the default and means that new variables names (from T173889) should be used.
+ * 1 means that the old ones should be used, e.g. if this object is constructed from an
+ * afl_var_dump which still bears old variables.
+ */
+ public $mVarsVersion = 2;
public function __construct() {
// Backwards-compatibility (unused now)
@@ -11,10 +19,10 @@ class AbuseFilterVariableHolder {
}
/**
- * @param $variable
- * @param $datum
+ * @param string $variable
+ * @param mixed $datum
*/
- function setVar( $variable, $datum ) {
+ public function setVar( $variable, $datum ) {
$variable = strtolower( $variable );
if ( !( $datum instanceof AFPData || $datum instanceof AFComputedVariable ) ) {
$datum = AFPData::newFromPHPVar( $datum );
@@ -24,11 +32,11 @@ class AbuseFilterVariableHolder {
}
/**
- * @param $variable
- * @param $method
- * @param $parameters
+ * @param string $variable
+ * @param string $method
+ * @param array $parameters
*/
- function setLazyLoadVar( $variable, $method, $parameters ) {
+ public function setLazyLoadVar( $variable, $method, $parameters ) {
$placeholder = new AFComputedVariable( $method, $parameters );
$this->setVar( $variable, $placeholder );
}
@@ -36,13 +44,19 @@ class AbuseFilterVariableHolder {
/**
* Get a variable from the current object
*
- * @param $variable string
+ * @param string $variable
* @return AFPData
*/
- function getVar( $variable ) {
+ public function getVar( $variable ) {
$variable = strtolower( $variable );
+ if ( $this->mVarsVersion === 1 && in_array( $variable, AbuseFilter::getDeprecatedVariables() ) ) {
+ // Variables are stored with old names, but the parser has given us
+ // a new name. Translate it back.
+ $variable = array_search( $variable, AbuseFilter::getDeprecatedVariables() );
+ }
if ( isset( $this->mVars[$variable] ) ) {
if ( $this->mVars[$variable] instanceof AFComputedVariable ) {
+ /** @suppress PhanUndeclaredMethod False positive */
$value = $this->mVars[$variable]->compute( $this );
$this->setVar( $variable, $value );
return $value;
@@ -58,21 +72,12 @@ class AbuseFilterVariableHolder {
*/
public static function merge() {
$newHolder = new AbuseFilterVariableHolder;
- call_user_func_array( [ $newHolder, "addHolders" ], func_get_args() );
+ $newHolder->addHolders( ...func_get_args() );
return $newHolder;
}
/**
- * @param $addHolder
- * @throws MWException
- * @deprecated use addHolders() instead
- */
- public function addHolder( $addHolder ) {
- $this->addHolders( $addHolder );
- }
-
- /**
* Merge any number of holders given as arguments into this holder.
*
* @throws MWException
@@ -88,7 +93,7 @@ class AbuseFilterVariableHolder {
}
}
- function __wakeup() {
+ public function __wakeup() {
// Reset the context.
$this->setVar( 'context', 'stored' );
}
@@ -98,7 +103,7 @@ class AbuseFilterVariableHolder {
*
* @return string[]
*/
- function exportAllVars() {
+ public function exportAllVars() {
$exported = [];
foreach ( array_keys( $this->mVars ) as $varName ) {
if ( !in_array( $varName, self::$varBlacklist ) ) {
@@ -114,7 +119,7 @@ class AbuseFilterVariableHolder {
*
* @return string[]
*/
- function exportNonLazyVars() {
+ public function exportNonLazyVars() {
$exported = [];
foreach ( $this->mVars as $varName => $data ) {
if (
@@ -134,8 +139,8 @@ class AbuseFilterVariableHolder {
* either set $compute to an array with the name of the variable or set
* $compute to true to compute all not yet set variables.
*
- * @param $compute array|bool Variables we should copute if not yet set
- * @param $includeUserVars bool Include user set variables
+ * @param array|bool $compute Variables we should copute if not yet set
+ * @param bool $includeUserVars Include user set variables
* @return array
*/
public function dumpAllVars( $compute = [], $includeUserVars = false ) {
@@ -150,15 +155,18 @@ class AbuseFilterVariableHolder {
$coreVariables = AbuseFilter::getBuilderValues();
$coreVariables = array_keys( $coreVariables['vars'] );
+ $deprecatedVariables = array_keys( AbuseFilter::getDeprecatedVariables() );
+ $coreVariables = array_merge( $coreVariables, $deprecatedVariables );
// Title vars can have several prefixes
- $prefixes = [ 'ARTICLE', 'MOVED_FROM', 'MOVED_TO' ];
+ $prefixes = [ 'MOVED_FROM', 'MOVED_TO', 'PAGE' ];
$titleVars = [
- '_ARTICLEID',
+ '_ID',
'_NAMESPACE',
- '_TEXT',
- '_PREFIXEDTEXT',
- '_recent_contributors'
+ '_TITLE',
+ '_PREFIXEDTITLE',
+ '_recent_contributors',
+ '_age',
];
foreach ( $wgRestrictionTypes as $action ) {
$titleVars[] = "_restrictions_$action";
@@ -190,21 +198,25 @@ class AbuseFilterVariableHolder {
}
/**
- * @param $var
+ * @param string $var
* @return bool
*/
- function varIsSet( $var ) {
+ public function varIsSet( $var ) {
return array_key_exists( $var, $this->mVars );
}
/**
* Compute all vars which need DB access. Useful for vars which are going to be saved
* cross-wiki or used for offline analysis.
+ *
+ * @suppress PhanUndeclaredProperty for $value->mMethod (phan thinks $value is always AFPData)
+ * @suppress PhanUndeclaredMethod for $value->compute (phan thinks $value is always AFPData)
*/
- function computeDBVars() {
+ public function computeDBVars() {
static $dbTypes = [
'links-from-wikitext-or-database',
'load-recent-authors',
+ 'page-age',
'get-page-restrictions',
'simple-user-accessor',
'user-age',
diff --git a/AbuseFilter/includes/AbuseLogHitFormatter.php b/AbuseFilter/includes/AbuseLogHitFormatter.php
index 4daf0e57..a2fbb284 100644
--- a/AbuseFilter/includes/AbuseLogHitFormatter.php
+++ b/AbuseFilter/includes/AbuseLogHitFormatter.php
@@ -1,4 +1,5 @@
<?php
+
use MediaWiki\MediaWikiServices;
/**
@@ -17,9 +18,11 @@ class AbuseLogHitFormatter extends LogFormatter {
$params = parent::getMessageParameters();
$filter_title = SpecialPage::getTitleFor( 'AbuseFilter', $entry['filter'] );
- $filter_caption = $this->msg( 'abusefilter-log-detailedentry-local' )->params( $entry['filter'] );
+ $filter_caption = $this->msg( 'abusefilter-log-detailedentry-local' )
+ ->params( $entry['filter'] )
+ ->text();
$log_title = SpecialPage::getTitleFor( 'AbuseLog', $entry['log'] );
- $log_caption = $this->msg( 'abusefilter-log-detailslink' );
+ $log_caption = $this->msg( 'abusefilter-log-detailslink' )->text();
$params[4] = $entry['action'];
diff --git a/AbuseFilter/includes/TableDiffFormatterFullContext.php b/AbuseFilter/includes/TableDiffFormatterFullContext.php
new file mode 100644
index 00000000..e962e9cf
--- /dev/null
+++ b/AbuseFilter/includes/TableDiffFormatterFullContext.php
@@ -0,0 +1,36 @@
+<?php
+
+/**
+ * Like TableDiffFormatter, but will always render the full context
+ * (even for empty diffs).
+ *
+ * @private
+ */
+class TableDiffFormatterFullContext extends TableDiffFormatter {
+ /**
+ * Format a diff.
+ *
+ * @param Diff $diff
+ * @return string The formatted output.
+ */
+ public function format( $diff ) {
+ $xlen = $ylen = 0;
+
+ // Calculate the length of the left and the right side
+ foreach ( $diff->edits as $edit ) {
+ if ( $edit->orig ) {
+ $xlen += count( $edit->orig );
+ }
+ if ( $edit->closing ) {
+ $ylen += count( $edit->closing );
+ }
+ }
+
+ // Just render the diff with no preprocessing
+ $this->startDiff();
+ $this->block( 1, $xlen, 1, $ylen, $diff->edits );
+ $end = $this->endDiff();
+
+ return $end;
+ }
+}
diff --git a/AbuseFilter/includes/Views/AbuseFilterView.php b/AbuseFilter/includes/Views/AbuseFilterView.php
index b8d5fd57..9b019335 100644
--- a/AbuseFilter/includes/Views/AbuseFilterView.php
+++ b/AbuseFilter/includes/Views/AbuseFilterView.php
@@ -3,7 +3,7 @@
use Wikimedia\Rdbms\IDatabase;
abstract class AbuseFilterView extends ContextSource {
- public $mFilter, $mHistoryID, $mSubmit;
+ public $mFilter, $mHistoryID, $mSubmit, $mPage, $mParams;
/**
* @var \MediaWiki\Linker\LinkRenderer
@@ -11,10 +11,10 @@ abstract class AbuseFilterView extends ContextSource {
protected $linkRenderer;
/**
- * @param $page SpecialAbuseFilter
- * @param $params array
+ * @param SpecialAbuseFilter $page
+ * @param array $params
*/
- function __construct( $page, $params ) {
+ public function __construct( $page, $params ) {
$this->mPage = $page;
$this->mParams = $params;
$this->setContext( $this->mPage->getContext() );
@@ -25,11 +25,14 @@ abstract class AbuseFilterView extends ContextSource {
* @param string $subpage
* @return Title
*/
- function getTitle( $subpage = '' ) {
+ public function getTitle( $subpage = '' ) {
return $this->mPage->getPageTitle( $subpage );
}
- abstract function show();
+ /**
+ * Function to show the page
+ */
+ abstract public function show();
/**
* @return bool
@@ -63,11 +66,193 @@ abstract class AbuseFilterView extends ContextSource {
}
/**
+ * @param string $rules
+ * @param string $textName
+ * @param bool $addResultDiv
+ * @param bool $externalForm
+ * @param bool $needsModifyRights
+ * @param-taint $rules none
+ * @return string
+ */
+ public function buildEditBox(
+ $rules,
+ $textName = 'wpFilterRules',
+ $addResultDiv = true,
+ $externalForm = false,
+ $needsModifyRights = true
+ ) {
+ $this->getOutput()->enableOOUI();
+
+ // Rules are in English
+ $editorAttrib = [ 'dir' => 'ltr' ];
+
+ $noTestAttrib = [];
+ $isUserAllowed = $needsModifyRights ?
+ $this->getUser()->isAllowed( 'abusefilter-modify' ) :
+ $this->canViewPrivate();
+ if ( !$isUserAllowed ) {
+ $noTestAttrib['disabled'] = 'disabled';
+ $addResultDiv = false;
+ }
+
+ $rules = rtrim( $rules ) . "\n";
+ $canEdit = $needsModifyRights ? $this->canEdit() : $this->canViewPrivate();
+ $switchEditor = null;
+
+ if ( ExtensionRegistry::getInstance()->isLoaded( 'CodeEditor' ) ) {
+ $editorAttrib['name'] = 'wpAceFilterEditor';
+ $editorAttrib['id'] = 'wpAceFilterEditor';
+ $editorAttrib['class'] = 'mw-abusefilter-editor';
+
+ $switchEditor =
+ new OOUI\ButtonWidget(
+ [
+ 'label' => $this->msg( 'abusefilter-edit-switch-editor' )->text(),
+ 'id' => 'mw-abusefilter-switcheditor'
+ ] + $noTestAttrib
+ );
+
+ $rulesContainer = Xml::element( 'div', $editorAttrib, $rules );
+
+ // Dummy textarea for submitting form and to use in case JS is disabled
+ $textareaAttribs = [];
+ if ( !$canEdit ) {
+ $textareaAttribs['readonly'] = 'readonly';
+ }
+ if ( $externalForm ) {
+ $textareaAttribs['form'] = 'wpFilterForm';
+ }
+ $rulesContainer .= Xml::textarea( $textName, $rules, 40, 15, $textareaAttribs );
+
+ $editorConfig = AbuseFilter::getAceConfig( $canEdit );
+
+ // Add Ace configuration variable
+ $this->getOutput()->addJsConfigVars( 'aceConfig', $editorConfig );
+ } else {
+ if ( !$canEdit ) {
+ $editorAttrib['readonly'] = 'readonly';
+ }
+ if ( $externalForm ) {
+ $editorAttrib['form'] = 'wpFilterForm';
+ }
+ $rulesContainer = Xml::textarea( $textName, $rules, 40, 15, $editorAttrib );
+ }
+
+ if ( $canEdit ) {
+ // Generate builder drop-down
+ $rawDropDown = AbuseFilter::getBuilderValues();
+
+ // The array needs to be rearranged to be understood by OOUI. It comes with the format
+ // [ group-msg-key => [ text-to-add => text-msg-key ] ] and we need it as
+ // [ group-msg => [ text-msg => text-to-add ] ]
+ // Also, the 'other' element must be the first one.
+ $dropDownOptions = [ $this->msg( 'abusefilter-edit-builder-select' )->text() => 'other' ];
+ foreach ( $rawDropDown as $group => $values ) {
+ // Give grep a chance to find the usages:
+ // abusefilter-edit-builder-group-op-arithmetic, abusefilter-edit-builder-group-op-comparison,
+ // abusefilter-edit-builder-group-op-bool, abusefilter-edit-builder-group-misc,
+ // abusefilter-edit-builder-group-funcs, abusefilter-edit-builder-group-vars
+ $localisedGroup = $this->msg( "abusefilter-edit-builder-group-$group" )->text();
+ $dropDownOptions[ $localisedGroup ] = array_flip( $values );
+ $newKeys = array_map(
+ function ( $key ) use ( $group ) {
+ return $this->msg( "abusefilter-edit-builder-$group-$key" )->text();
+ },
+ array_keys( $dropDownOptions[ $localisedGroup ] )
+ );
+ $dropDownOptions[ $localisedGroup ] = array_combine(
+ $newKeys, $dropDownOptions[ $localisedGroup ] );
+ }
+
+ $dropDownList = Xml::listDropDownOptionsOoui( $dropDownOptions );
+ $dropDown = new OOUI\DropdownInputWidget( [
+ 'name' => 'wpFilterBuilder',
+ 'inputId' => 'wpFilterBuilder',
+ 'options' => $dropDownList
+ ] );
+
+ $formElements = [ new OOUI\FieldLayout( $dropDown ) ];
+
+ // Button for syntax check
+ $syntaxCheck =
+ new OOUI\ButtonWidget(
+ [
+ 'label' => $this->msg( 'abusefilter-edit-check' )->text(),
+ 'id' => 'mw-abusefilter-syntaxcheck'
+ ] + $noTestAttrib
+ );
+
+ // Button for switching editor (if Ace is used)
+ if ( $switchEditor !== null ) {
+ $formElements[] = new OOUI\FieldLayout(
+ new OOUI\Widget( [
+ 'content' => new OOUI\HorizontalLayout( [
+ 'items' => [ $switchEditor, $syntaxCheck ]
+ ] )
+ ] )
+ );
+ } else {
+ $formElements[] = new OOUI\FieldLayout( $syntaxCheck );
+ }
+
+ $fieldSet = new OOUI\FieldsetLayout( [
+ 'items' => $formElements,
+ 'classes' => [ 'mw-abusefilter-edit-buttons', 'mw-abusefilter-javascript-tools' ]
+ ] );
+
+ $rulesContainer .= $fieldSet;
+ }
+
+ if ( $addResultDiv ) {
+ $rulesContainer .= Xml::element( 'div',
+ [ 'id' => 'mw-abusefilter-syntaxresult', 'style' => 'display: none;' ],
+ '&#160;' );
+ }
+
+ // Add script
+ $this->getOutput()->addModules( 'ext.abuseFilter.edit' );
+ AbuseFilter::$editboxName = $textName;
+
+ return $rulesContainer;
+ }
+
+ /**
* @param IDatabase $db
+ * @param string|bool $action 'edit', 'move', 'createaccount', 'delete' or false for all
* @return string
*/
- public function buildTestConditions( IDatabase $db ) {
+ public function buildTestConditions( IDatabase $db, $action = false ) {
// If one of these is true, we're abusefilter compatible.
+ switch ( $action ) {
+ case 'edit':
+ return $db->makeList( [
+ // Actually, this is only one condition, but this way we get it as string
+ 'rc_source' => [
+ RecentChange::SRC_EDIT,
+ RecentChange::SRC_NEW,
+ ]
+ ], LIST_AND );
+ case 'move':
+ return $db->makeList( [
+ 'rc_source' => RecentChange::SRC_LOG,
+ 'rc_log_type' => 'move',
+ 'rc_log_action' => 'move'
+ ], LIST_AND );
+ case 'createaccount':
+ return $db->makeList( [
+ 'rc_source' => RecentChange::SRC_LOG,
+ 'rc_log_type' => 'newusers',
+ 'rc_log_action' => [ 'create', 'autocreate' ]
+ ], LIST_AND );
+ case 'delete':
+ return $db->makeList( [
+ 'rc_source' => RecentChange::SRC_LOG,
+ 'rc_log_type' => 'delete',
+ 'rc_log_action' => 'delete'
+ ], LIST_AND );
+ // @ToDo: case 'upload'
+ }
+
return $db->makeList( [
'rc_source' => [
RecentChange::SRC_EDIT,
@@ -82,7 +267,7 @@ abstract class AbuseFilterView extends ContextSource {
], LIST_AND ),
$db->makeList( [
'rc_log_type' => 'newusers',
- 'rc_log_action' => 'create'
+ 'rc_log_action' => [ 'create', 'autocreate' ]
], LIST_AND ),
$db->makeList( [
'rc_log_type' => 'delete',
@@ -95,10 +280,21 @@ abstract class AbuseFilterView extends ContextSource {
}
/**
- * @static
+ * @param string|int $id
+ * @param string|null $text
+ * @return string HTML
+ */
+ public function getLinkToLatestDiff( $id, $text = null ) {
+ return $this->linkRenderer->makeKnownLink(
+ $this->getTitle( "history/$id/diff/prev/cur" ),
+ $text
+ );
+ }
+
+ /**
* @return bool
*/
- static function canViewPrivate() {
+ public static function canViewPrivate() {
global $wgUser;
static $canView = null;
@@ -110,102 +306,3 @@ abstract class AbuseFilterView extends ContextSource {
}
}
-
-class AbuseFilterChangesList extends OldChangesList {
- /**
- * @param $s
- * @param $rc
- * @param $classes array
- */
- public function insertExtra( &$s, &$rc, &$classes ) {
- if ( (int)$rc->getAttribute( 'rc_deleted' ) !== 0 ) {
- $s .= ' ' . $this->msg( 'abusefilter-log-hidden-implicit' )->parse();
- if ( !$this->userCan( $rc, Revision::SUPPRESSED_ALL ) ) {
- return;
- }
- }
-
- $examineParams = empty( $rc->examineParams ) ? [] : $rc->examineParams;
-
- $title = SpecialPage::getTitleFor( 'AbuseFilter', 'examine/' . $rc->mAttribs['rc_id'] );
- $examineLink = $this->linkRenderer->makeLink(
- $title,
- new HtmlArmor( $this->msg( 'abusefilter-changeslist-examine' )->parse() ),
- [],
- $examineParams
- );
-
- $s .= ' '.$this->msg( 'parentheses' )->rawParams( $examineLink )->escaped();
-
- # If we have a match..
- if ( isset( $rc->filterResult ) ) {
- $class = $rc->filterResult ?
- 'mw-abusefilter-changeslist-match' :
- 'mw-abusefilter-changeslist-nomatch';
-
- $classes[] = $class;
- }
- }
-
- /**
- * Insert links to user page, user talk page and eventually a blocking link.
- * Like the parent, but don't hide details if user can see them.
- *
- * @param string &$s HTML to update
- * @param RecentChange &$rc
- */
- public function insertUserRelatedLinks( &$s, &$rc ) {
- $links = $this->getLanguage()->getDirMark() . Linker::userLink( $rc->mAttribs['rc_user'],
- $rc->mAttribs['rc_user_text'] ) .
- Linker::userToolLinks( $rc->mAttribs['rc_user'], $rc->mAttribs['rc_user_text'] );
-
- if ( $this->isDeleted( $rc, Revision::DELETED_USER ) ) {
- if ( $this->userCan( $rc, Revision::DELETED_USER ) ) {
- $s .= ' <span class="history-deleted">' . $links . '</span>';
- } else {
- $s .= ' <span class="history-deleted">' .
- $this->msg( 'rev-deleted-user' )->escaped() . '</span>';
- }
- } else {
- $s .= $links;
- }
- }
-
- /**
- * Insert a formatted comment. Like the parent, but don't hide details if user can see them.
- * @param RecentChange $rc
- * @return string
- */
- public function insertComment( $rc ) {
- if ( $this->isDeleted( $rc, Revision::DELETED_COMMENT ) ) {
- if ( $this->userCan( $rc, Revision::DELETED_COMMENT ) ) {
- return ' <span class="history-deleted">' .
- Linker::commentBlock( $rc->mAttribs['rc_comment'], $rc->getTitle() ) . '</span>';
- } else {
- return ' <span class="history-deleted">' .
- $this->msg( 'rev-deleted-comment' )->escaped() . '</span>';
- }
- } else {
- return Linker::commentBlock( $rc->mAttribs['rc_comment'], $rc->getTitle() );
- }
- }
-
- /**
- * Insert a formatted action. The same as parent, but with a different audience in LogFormatter
- *
- * @param RecentChange $rc
- * @return string
- */
- public function insertLogEntry( $rc ) {
- $formatter = LogFormatter::newFromRow( $rc->mAttribs );
- $formatter->setContext( $this->getContext() );
- $formatter->setAudience( LogFormatter::FOR_THIS_USER );
- $formatter->setShowUserToolLinks( true );
- $mark = $this->getLanguage()->getDirMark();
- return $formatter->getActionText() . " $mark" . $formatter->getComment();
- }
-
- // Kill rollback links.
- public function insertRollback( &$s, &$rc ) {
- }
-}
diff --git a/AbuseFilter/includes/Views/AbuseFilterViewDiff.php b/AbuseFilter/includes/Views/AbuseFilterViewDiff.php
index 6c29664e..8128ad65 100644
--- a/AbuseFilter/includes/Views/AbuseFilterViewDiff.php
+++ b/AbuseFilter/includes/Views/AbuseFilterViewDiff.php
@@ -1,100 +1,86 @@
<?php
-/**
- * Like TableDiffFormatter, but will always render the full context
- * (even for empty diffs).
- *
- * @private
- */
-class TableDiffFormatterFullContext extends TableDiffFormatter {
- /**
- * Format a diff.
- *
- * @param Diff $diff
- * @return string The formatted output.
- */
- function format( $diff ) {
- $xlen = $ylen = 0;
-
- // Calculate the length of the left and the right side
- foreach ( $diff->edits as $edit ) {
- if ( $edit->orig ) {
- $xlen += count( $edit->orig );
- }
- if ( $edit->closing ) {
- $ylen += count( $edit->closing );
- }
- }
-
- // Just render the diff with no preprocessing
- $this->startDiff();
- $this->block( 1, $xlen, 1, $ylen, $diff->edits );
- $end = $this->endDiff();
-
- return $end;
- }
-}
-
class AbuseFilterViewDiff extends AbuseFilterView {
public $mOldVersion = null;
public $mNewVersion = null;
public $mNextHistoryId = null;
public $mFilter = null;
- function show() {
+ /**
+ * Shows the page
+ */
+ public function show() {
$show = $this->loadData();
$out = $this->getOutput();
+ $out->enableOOUI();
+ $out->addModuleStyles( [ 'oojs-ui.styles.icons-movement' ] );
$links = [];
if ( $this->mFilter ) {
- $links['abusefilter-history-backedit'] = $this->getTitle( $this->mFilter );
- $links['abusefilter-diff-backhistory'] = $this->getTitle( 'history/' . $this->mFilter );
+ $links['abusefilter-history-backedit'] =
+ $this->getTitle( $this->mFilter )->getFullURL();
+ $links['abusefilter-diff-backhistory'] =
+ $this->getTitle( 'history/' . $this->mFilter )->getFullURL();
}
- foreach ( $links as $msg => $title ) {
- $links[$msg] = $this->linkRenderer->makeLink( $title, $this->msg( $msg )->text() );
+ foreach ( $links as $msg => $href ) {
+ $links[$msg] =
+ new OOUI\ButtonWidget( [
+ 'label' => $this->msg( $msg )->text(),
+ 'href' => $href
+ ] );
}
- $backlinks = $this->getLanguage()->pipeList( $links );
- $out->addHTML( Xml::tags( 'p', null, $backlinks ) );
+ $backlinks =
+ new OOUI\HorizontalLayout( [
+ 'items' => $links
+ ] );
+ $out->addHTML( $backlinks );
if ( $show ) {
$out->addHTML( $this->formatDiff() );
-
// Next and previous change links
- $links = [];
+ $buttons = [];
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()
- );
+ $href = $this->getTitle(
+ 'history/' . $this->mFilter . '/diff/prev/' . $this->mOldVersion['meta']['history_id']
+ )->getFullURL();
+ $buttons[] = new OOUI\ButtonWidget( [
+ 'label' => $this->msg( 'abusefilter-diff-prev' )->text(),
+ 'href' => $href,
+ 'icon' => 'previous'
+ ] );
}
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' )
- );
+ $href = $this->getTitle(
+ 'history/' . $this->mFilter . '/diff/prev/' . $this->mNextHistoryId
+ )->getFullURL();
+ $buttons[] = new OOUI\ButtonWidget( [
+ 'label' => $this->msg( 'abusefilter-diff-next' )->text(),
+ 'href' => $href,
+ 'icon' => 'next'
+ ] );
}
- if ( count( $links ) > 0 ) {
- $backlinks = $this->getLanguage()->pipeList( $links );
- $out->addHTML( Xml::tags( 'p', null, $backlinks ) );
+ if ( count( $buttons ) > 0 ) {
+ $buttons = new OOUI\HorizontalLayout( [
+ 'items' => $buttons,
+ 'classes' => [ 'mw-abusefilter-history-buttons' ]
+ ] );
+ $out->addHTML( $buttons );
}
}
}
- function loadData() {
+ /**
+ * @return bool
+ */
+ public function loadData() {
$oldSpec = $this->mParams[3];
$newSpec = $this->mParams[4];
$this->mFilter = $this->mParams[1];
@@ -127,7 +113,7 @@ class AbuseFilterViewDiff extends AbuseFilterView {
* @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 ) {
+ public function getNextHistoryId( $historyId ) {
$dbr = wfGetDB( DB_REPLICA );
$row = $dbr->selectRow(
'abuse_filter_history',
@@ -145,7 +131,12 @@ class AbuseFilterViewDiff extends AbuseFilterView {
return null;
}
- function loadSpec( $spec, $otherSpec ) {
+ /**
+ * @param string $spec
+ * @param string $otherSpec
+ * @return array|null
+ */
+ public function loadSpec( $spec, $otherSpec ) {
static $dependentSpecs = [ 'prev', 'next' ];
static $cache = [];
@@ -154,18 +145,31 @@ class AbuseFilterViewDiff extends AbuseFilterView {
}
$dbr = wfGetDB( DB_REPLICA );
+ // All but afh_filter, afh_deleted and afh_changed_fields
+ $selectFields = [
+ 'afh_id',
+ 'afh_user',
+ 'afh_user_text',
+ 'afh_timestamp',
+ 'afh_pattern',
+ 'afh_comments',
+ 'afh_flags',
+ 'afh_public_comments',
+ 'afh_actions',
+ 'afh_group',
+ ];
$row = null;
if ( is_numeric( $spec ) ) {
$row = $dbr->selectRow(
'abuse_filter_history',
- '*',
+ $selectFields,
[ 'afh_id' => $spec, 'afh_filter' => $this->mFilter ],
__METHOD__
);
} elseif ( $spec == 'cur' ) {
$row = $dbr->selectRow(
'abuse_filter_history',
- '*',
+ $selectFields,
[ 'afh_filter' => $this->mFilter ],
__METHOD__,
[ 'ORDER BY' => 'afh_timestamp desc' ]
@@ -176,7 +180,7 @@ class AbuseFilterViewDiff extends AbuseFilterView {
$row = $dbr->selectRow(
'abuse_filter_history',
- '*',
+ $selectFields,
[
'afh_filter' => $this->mFilter,
'afh_id<' . $dbr->addQuotes( $other['meta']['history_id'] ),
@@ -196,7 +200,7 @@ class AbuseFilterViewDiff extends AbuseFilterView {
$row = $dbr->selectRow(
'abuse_filter_history',
- '*',
+ $selectFields,
[
'afh_filter' => $this->mFilter,
'afh_id>' . $dbr->addQuotes( $other['meta']['history_id'] ),
@@ -222,7 +226,11 @@ class AbuseFilterViewDiff extends AbuseFilterView {
return $data;
}
- function loadFromHistoryRow( $row ) {
+ /**
+ * @param stdClass $row
+ * @return array
+ */
+ public function loadFromHistoryRow( $row ) {
return [
'meta' => [
'history_id' => $row->afh_id,
@@ -246,7 +254,7 @@ class AbuseFilterViewDiff extends AbuseFilterView {
* @param int $history_id
* @return string
*/
- function formatVersionLink( $timestamp, $history_id ) {
+ public function formatVersionLink( $timestamp, $history_id ) {
$filter = $this->mFilter;
$text = $this->getLanguage()->timeanddate( $timestamp, true );
$title = $this->getTitle( "history/$filter/item/$history_id" );
@@ -259,7 +267,7 @@ class AbuseFilterViewDiff extends AbuseFilterView {
/**
* @return string
*/
- function formatDiff() {
+ public function formatDiff() {
$oldVersion = $this->mOldVersion;
$newVersion = $this->mNewVersion;
@@ -300,17 +308,17 @@ class AbuseFilterViewDiff extends AbuseFilterView {
$headings = Xml::tags( 'tr', null, $headings );
+ $body = '';
// Basic info
+ $infoHeader = $this->getHeaderRow( 'abusefilter-diff-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 ||
+ count( $this->getConfig()->get( 'AbuseFilterValidGroups' ) ) > 1 ||
$oldVersion['info']['group'] != $newVersion['info']['group']
) {
$info .= $this->getDiffRow(
@@ -331,29 +339,43 @@ class AbuseFilterViewDiff extends AbuseFilterView {
$newVersion['info']['notes']
);
+ if ( $info !== '' ) {
+ $body .= $infoHeader . $info;
+ }
+
// Pattern
- $info .= $this->getHeaderRow( 'abusefilter-diff-pattern' );
- $info .= $this->getDiffRow(
+ $patternHeader = $this->getHeaderRow( 'abusefilter-diff-pattern' );
+ $pattern = '';
+ $pattern .= $this->getDiffRow(
'abusefilter-edit-rules',
$oldVersion['pattern'],
- $newVersion['pattern'],
- 'text'
+ $newVersion['pattern']
);
+ if ( $pattern !== '' ) {
+ $body .= $patternHeader . $pattern;
+ }
+
// Actions
+ $actionsHeader = $this->getHeaderRow( 'abusefilter-edit-consequences' );
+ $actions = '';
+
$oldActions = $this->stringifyActions( $oldVersion['actions'] );
$newActions = $this->stringifyActions( $newVersion['actions'] );
- $info .= $this->getHeaderRow( 'abusefilter-edit-consequences' );
- $info .= $this->getDiffRow(
+ $actions .= $this->getDiffRow(
'abusefilter-edit-consequences',
$oldActions,
$newActions
);
+ if ( $actions !== '' ) {
+ $body .= $actionsHeader . $actions;
+ }
+
$html = "<table class='wikitable'>
<thead>$headings</thead>
- <tbody>$info</tbody>
+ <tbody>$body</tbody>
</table>";
$html = Xml::tags( 'h2', null, $this->msg( 'abusefilter-diff-title' )->parse() ) . $html;
@@ -365,7 +387,7 @@ class AbuseFilterViewDiff extends AbuseFilterView {
* @param array $actions
* @return array
*/
- function stringifyActions( $actions ) {
+ public function stringifyActions( $actions ) {
$lines = [];
ksort( $actions );
@@ -384,7 +406,7 @@ class AbuseFilterViewDiff extends AbuseFilterView {
* @param string $msg
* @return string
*/
- function getHeaderRow( $msg ) {
+ public 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 );
@@ -398,7 +420,7 @@ class AbuseFilterViewDiff extends AbuseFilterView {
* @param array|string $new
* @return string
*/
- function getDiffRow( $msg, $old, $new ) {
+ public function getDiffRow( $msg, $old, $new ) {
if ( !is_array( $old ) ) {
$old = explode( "\n", preg_replace( "/\\\r\\\n?/", "\n", $old ) );
}
@@ -406,6 +428,10 @@ class AbuseFilterViewDiff extends AbuseFilterView {
$new = explode( "\n", preg_replace( "/\\\r\\\n?/", "\n", $new ) );
}
+ if ( $old === $new ) {
+ return '';
+ }
+
$diffEngine = new DifferenceEngine( $this->getContext() );
$diffEngine->showDiffStyle();
diff --git a/AbuseFilter/includes/Views/AbuseFilterViewEdit.php b/AbuseFilter/includes/Views/AbuseFilterViewEdit.php
index 952fcce7..e4c5a93e 100644
--- a/AbuseFilter/includes/Views/AbuseFilterViewEdit.php
+++ b/AbuseFilter/includes/Views/AbuseFilterViewEdit.php
@@ -1,87 +1,25 @@
<?php
class AbuseFilterViewEdit extends AbuseFilterView {
+ public static $mLoadedRow = null, $mLoadedActions = null;
/**
* @param SpecialAbuseFilter $page
* @param array $params
*/
- function __construct( $page, $params ) {
+ public function __construct( $page, $params ) {
parent::__construct( $page, $params );
$this->mFilter = $page->mFilter;
$this->mHistoryID = $page->mHistoryID;
}
- /// @todo When older versions of MediaWiki are no longer
- /// supported, remove this method and call ChangeTags::isTagNameValid directly
- /// Because it's planned for removal, this is private.
/**
- * Check whether the characters in the tag name are valid.
- *
- * @param string $tag Tag name
- * @return Status
+ * Shows the page
*/
- private static function isTagNameValid( $tag ) {
- if ( is_callable( 'ChangeTags::isTagNameValid' ) ) {
- $status = ChangeTags::isTagNameValid( $tag );
- } else {
- // BC
- if ( strpos( $tag, ',' ) !== false || strpos( $tag, '|' ) !== false ||
- strpos( $tag, '/' ) !== false ||
- !Title::makeTitleSafe( NS_MEDIAWIKI, "tag-{$tag}-description" )
- ) {
- $status = Status::newFatal( 'abusefilter-edit-bad-tags' );
- } else {
- $status = Status::newGood();
- }
- }
-
- return $status;
- }
-
- /**
- * Check whether a filter is allowed to use a tag
- *
- * @param string $tag Tag name
- * @return Status
- */
- protected function isAllowedTag( $tag ) {
- $tagNameStatus = self::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() {
+ public function show() {
$user = $this->getUser();
$out = $this->getOutput();
$request = $this->getRequest();
+ $config = $this->getConfig();
$out->setPageTitle( $this->msg( 'abusefilter-edit' ) );
$out->addHelpLink( 'Extension:AbuseFilter/Rules format' );
@@ -104,11 +42,17 @@ class AbuseFilterViewEdit extends AbuseFilterView {
}
}
- // Add default warning messages
- $this->exposeWarningMessages();
+ // Add the default warning and disallow messages in a JS variable
+ $this->exposeMessages();
if ( $filter == 'new' && !$this->canEdit() ) {
- $out->addWikiMsg( 'abusefilter-edit-notallowed' );
+ $out->addHTML(
+ Xml::tags(
+ 'p',
+ null,
+ Html::errorBox( $this->msg( 'abusefilter-edit-notallowed' )->parse() )
+ )
+ );
return;
}
@@ -117,246 +61,55 @@ class AbuseFilterViewEdit extends AbuseFilterView {
$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;
- }
-
- $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;
- }
+ $status = AbuseFilter::saveFilter( $this, $filter, $request, $newRow, $actions );
+ if ( !$status->isGood() ) {
+ $err = $status->getErrors();
+ $msg = $err[0]['message'];
+ $params = $err[0]['params'];
+ if ( $status->isOK() ) {
+ $out->addHTML(
+ $this->buildFilterEditor(
+ $this->msg( $msg, $params )->parseAsBlock(),
+ $filter,
+ $history_id
+ )
+ );
+ } else {
+ $out->addWikiMsg( $msg );
}
- }
-
- $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;
+ if ( $status->getValue() === false ) {
+ // No change
+ $out->redirect( $this->getTitle()->getLocalURL() );
} 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'];
+ list( $new_id, $history_id ) = $status->getValue();
+ $out->redirect(
+ $this->getTitle()->getLocalURL(
+ [
+ 'result' => 'success',
+ 'changedfilter' => $new_id,
+ 'changeid' => $history_id,
+ ]
+ )
+ );
}
-
- $globalRulesKey = AbuseFilter::getGlobalRulesKey( $group );
- ObjectCache::getMainWANInstance()->touchCheckKey( $globalRulesKey );
}
-
- // Logging
- $logEntry = new ManualLogEntry( 'abusefilter', 'modify' );
- $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' );
+ // Lost rights meanwhile
+ $out->addHTML(
+ Xml::tags(
+ 'p',
+ null,
+ Html::errorBox( $this->msg( 'abusefilter-edit-notallowed' )->parse() )
+ )
+ );
+ } elseif ( $request->wasPosted() ) {
+ // Warn the user to re-attempt save
+ $out->addHTML(
+ Html::warningBox( $this->msg( 'abusefilter-edit-token-not-match' )->escaped() )
+ );
}
if ( $history_id ) {
@@ -377,19 +130,21 @@ class AbuseFilterViewEdit extends AbuseFilterView {
* 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 $error string An error message to show above the filter box.
- * @param $filter int The filter ID
- * @param $history_id int The history ID of the filter, if applicable. Otherwise null
+ * @param string|null $error An error message to show above the filter box.
+ * @param int $filter The filter ID
+ * @param int|null $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 ) {
+ public function buildFilterEditor( $error, $filter, $history_id = null ) {
if ( $filter === null ) {
return false;
}
// Build the edit form
$out = $this->getOutput();
+ $out->enableOOUI();
+ $out->addJsConfigVars( 'isFilterEditor', true );
$lang = $this->getLanguage();
$user = $this->getUser();
@@ -397,9 +152,19 @@ class AbuseFilterViewEdit extends AbuseFilterView {
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() ) );
+ $out->addHTML(
+ Xml::tags(
+ 'p',
+ null,
+ Html::errorBox( $this->msg( 'abusefilter-edit-badfilter' )->parse() )
+ )
+ );
+ $href = $this->getTitle()->getFullURL();
+ $btn = new OOUI\ButtonWidget( [
+ 'label' => $this->msg( 'abusefilter-return' )->text(),
+ 'href' => $href
+ ] );
+ $out->addHTML( $btn );
return false;
}
@@ -412,60 +177,57 @@ class AbuseFilterViewEdit extends AbuseFilterView {
if ( ( ( isset( $row->af_hidden ) && $row->af_hidden ) ||
AbuseFilter::filterHidden( $filter ) )
&& !$this->canViewPrivate() ) {
- return $this->msg( 'abusefilter-edit-denied' )->text();
+ return $this->msg( 'abusefilter-edit-denied' )->escaped();
}
$output = '';
if ( $error ) {
- $out->addHTML( "<span class=\"error\">$error</span>" );
+ $output .= Html::errorBox( $error );
}
// Read-only attribute
$readOnlyAttrib = [];
- $cbReadOnlyAttrib = []; // For checkboxes
-
- $styleAttrib = [ 'style' => 'width:95%' ];
if ( !$this->canEditFilter( $row ) ) {
- $readOnlyAttrib['readonly'] = 'readonly';
- $cbReadOnlyAttrib['disabled'] = 'disabled';
+ $readOnlyAttrib['disabled'] = 'disabled';
}
$fields = [];
$fields['abusefilter-edit-id'] =
$this->mFilter == 'new' ?
- $this->msg( 'abusefilter-edit-new' )->text() :
+ $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'
+ new OOUI\TextInputWidget( [
+ 'name' => 'wpFilterDescription',
+ 'value' => $row->af_public_comments ?? ''
+ ] + $readOnlyAttrib
);
+ $validGroups = $this->getConfig()->get( 'AbuseFilterValidGroups' );
+ if ( count( $validGroups ) > 1 ) {
+ $groupSelector =
+ new OOUI\DropdownInputWidget( [
+ 'name' => 'wpFilterGroup',
+ 'id' => 'mw-abusefilter-edit-group-input',
+ 'value' => 'default',
+ 'disabled' => !empty( $readOnlyAttrib )
+ ] );
+
+ $options = [];
if ( isset( $row->af_group ) && $row->af_group ) {
- $groupSelector->setDefault( $row->af_group );
+ $groupSelector->setValue( $row->af_group );
}
- foreach ( $wgAbuseFilterValidGroups as $group ) {
- $groupSelector->addOption( AbuseFilter::nameGroup( $group ), $group );
+ foreach ( $validGroups as $group ) {
+ $options += [ AbuseFilter::nameGroup( $group ) => $group ];
}
- if ( !empty( $readOnlyAttrib ) ) {
- $groupSelector->setAttribute( 'disabled', 'disabled' );
- }
+ $options = Xml::listDropDownOptionsOoui( $options );
+ $groupSelector->setOptions( $options );
- $fields['abusefilter-edit-group'] = $groupSelector->getHTML();
+ $fields['abusefilter-edit-group'] = $groupSelector;
}
// Hit count display
@@ -484,14 +246,13 @@ class AbuseFilterViewEdit extends AbuseFilterView {
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 ) {
+ if ( $this->getConfig()->get( 'AbuseFilterProfile' ) ) {
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 )
@@ -504,35 +265,32 @@ class AbuseFilterViewEdit extends AbuseFilterView {
}
}
- $fields['abusefilter-edit-rules'] = AbuseFilter::buildEditBox(
+ $fields['abusefilter-edit-rules'] = $this->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
+ true
);
+ $fields['abusefilter-edit-notes'] =
+ new OOUI\MultilineTextInputWidget( [
+ 'name' => 'wpFilterNotes',
+ 'value' => isset( $row->af_comments ) ? $row->af_comments . "\n" : "\n",
+ 'rows' => 15
+ ] + $readOnlyAttrib
+ );
- // Build checkboxen
+ // Build checkboxes
$checkboxes = [ 'hidden', 'enabled', 'deleted' ];
$flags = '';
- global $wgAbuseFilterIsCentral;
- if ( $wgAbuseFilterIsCentral ) {
+ if ( $this->getConfig()->get( 'AbuseFilterIsCentral' ) ) {
$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 )
+ array_filter( $this->getConfig()->get( 'AbuseFilterRestrictions' ) )
);
if ( $throttledActions ) {
@@ -544,9 +302,11 @@ class AbuseFilterViewEdit extends AbuseFilterView {
);
$flags .= $out->parse(
- $this->msg( 'abusefilter-edit-throttled-warning' )
- ->plaintextParams( $lang->commaList( $throttledActions ) )
- ->text()
+ Html::warningBox(
+ $this->msg( 'abusefilter-edit-throttled-warning' )
+ ->plaintextParams( $lang->commaList( $throttledActions ) )
+ ->text()
+ )
);
}
}
@@ -561,18 +321,39 @@ class AbuseFilterViewEdit extends AbuseFilterView {
$dbField = "af_$checkboxId";
$postVar = 'wpFilter' . ucfirst( $checkboxId );
+ $checkboxAttribs = [
+ 'name' => $postVar,
+ 'id' => $postVar,
+ 'selected' => $row->$dbField ?? false,
+ ] + $readOnlyAttrib;
+ $labelAttribs = [
+ 'label' => $this->msg( $message )->text(),
+ 'align' => 'inline',
+ ];
+
if ( $checkboxId == 'global' && !$this->canEditGlobal() ) {
- $cbReadOnlyAttrib['disabled'] = 'disabled';
+ $checkboxAttribs['disabled'] = 'disabled';
}
- $checkbox = Xml::checkLabel(
- $this->msg( $message )->text(),
- $postVar,
- $postVar,
- isset( $row->$dbField ) ? $row->$dbField : false,
- $cbReadOnlyAttrib
- );
- $checkbox = Xml::tags( 'p', null, $checkbox );
+ // Set readonly on deleted if the filter isn't disabled
+ if ( $checkboxId == 'deleted' && $row->af_enabled == 1 ) {
+ $checkboxAttribs['disabled'] = 'disabled';
+ }
+
+ // Add infusable where needed
+ if ( $checkboxId == 'deleted' || $checkboxId == 'enabled' ) {
+ $checkboxAttribs['infusable'] = true;
+ if ( $checkboxId == 'deleted' ) {
+ $labelAttribs['id'] = $postVar . 'Label';
+ $labelAttribs['infusable'] = true;
+ }
+ }
+
+ $checkbox =
+ new OOUI\FieldLayout(
+ new OOUI\CheckboxInputWidget( $checkboxAttribs ),
+ $labelAttribs
+ );
$flags .= $checkbox;
}
@@ -604,15 +385,24 @@ class AbuseFilterViewEdit extends AbuseFilterView {
$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 ),
+ $this->getLinkToLatestDiff(
+ $filter,
+ $lang->timeanddate( $row->af_timestamp, true )
+ ),
$userLink,
- $lang->date( $row->af_timestamp, true ),
- $lang->time( $row->af_timestamp, true ),
- $userName
+ $this->getLinkToLatestDiff(
+ $filter,
+ $lang->date( $row->af_timestamp, true )
+ ),
+ $this->getLinkToLatestDiff(
+ $filter,
+ $lang->time( $row->af_timestamp, true )
+ )
+ )->params(
+ wfEscapeWikiText( $row->af_user_text )
)->parse();
$history_display = new HtmlArmor( $this->msg( 'abusefilter-edit-viewhistory' )->parse() );
$fields['abusefilter-edit-history'] =
@@ -623,10 +413,13 @@ class AbuseFilterViewEdit extends AbuseFilterView {
$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
- );
+ $tools .=
+ new OOUI\MultilineTextInputWidget( [
+ 'id' => 'mw-abusefilter-export',
+ 'readOnly' => true,
+ 'value' => $exportText,
+ 'rows' => 10
+ ] );
$fields['abusefilter-edit-tools'] = $tools;
@@ -638,10 +431,14 @@ class AbuseFilterViewEdit extends AbuseFilterView {
);
if ( $this->canEditFilter( $row ) ) {
- $form .= Xml::submitButton(
- $this->msg( 'abusefilter-edit-save' )->text(),
- [ 'accesskey' => 's' ]
- );
+ $form .=
+ new OOUI\ButtonInputWidget( [
+ 'type' => 'submit',
+ 'label' => $this->msg( 'abusefilter-edit-save' )->text(),
+ 'useInputTag' => true,
+ 'accesskey' => 's',
+ 'flags' => [ 'progressive', 'primary' ]
+ ] );
$form .= Html::hidden(
'wpEditToken',
$user->getEditToken( [ 'abusefilter', $filter ] )
@@ -663,15 +460,15 @@ class AbuseFilterViewEdit extends AbuseFilterView {
/**
* Builds the "actions" editor for a given filter.
- * @param $row stdClass A row from the abuse_filter table.
- * @param $actions Array of rows from the abuse_filter_action table
+ * @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.
+ * @return string HTML text for an action editor.
*/
- function buildConsequenceEditor( $row, $actions ) {
- global $wgAbuseFilterActions;
-
- $enabledActions = array_filter( $wgAbuseFilterActions );
+ public function buildConsequenceEditor( $row, $actions ) {
+ $enabledActions = array_filter(
+ $this->getConfig()->get( 'AbuseFilterActions' )
+ );
$setActions = [];
foreach ( $enabledActions as $action => $_ ) {
@@ -681,9 +478,9 @@ class AbuseFilterViewEdit extends AbuseFilterView {
$output = '';
foreach ( $enabledActions as $action => $_ ) {
- MediaWiki\suppressWarnings();
+ Wikimedia\suppressWarnings();
$params = $actions[$action]['parameters'];
- MediaWiki\restoreWarnings();
+ Wikimedia\restoreWarnings();
$output .= $this->buildConsequenceSelector(
$action, $setActions[$action], $params, $row );
}
@@ -692,39 +489,45 @@ class AbuseFilterViewEdit extends AbuseFilterView {
}
/**
- * @param $action string The action to build an editor for
- * @param $set bool Whether or not the action is activated
- * @param $parameters array Action parameters
- * @param $row stdClass abuse_filter row object
- * @return string
+ * @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|\OOUI\FieldLayout
*/
- function buildConsequenceSelector( $action, $set, $parameters, $row ) {
- global $wgAbuseFilterActions, $wgMainCacheType;
-
- if ( empty( $wgAbuseFilterActions[$action] ) ) {
+ public function buildConsequenceSelector( $action, $set, $parameters, $row ) {
+ $config = $this->getConfig();
+ $actions = $config->get( 'AbuseFilterActions' );
+ if ( empty( $actions[$action] ) ) {
return '';
}
$readOnlyAttrib = [];
- $cbReadOnlyAttrib = []; // For checkboxes
if ( !$this->canEditFilter( $row ) ) {
- $readOnlyAttrib['readonly'] = 'readonly';
- $cbReadOnlyAttrib['disabled'] = 'disabled';
+ $readOnlyAttrib['disabled'] = 'disabled';
}
switch ( $action ) {
case 'throttle':
// Throttling is only available via object caching
- if ( $wgMainCacheType === CACHE_NONE ) {
+ if ( $config->get( 'MainCacheType' ) === 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 );
+ $throttleSettings =
+ new OOUI\FieldLayout(
+ new OOUI\CheckboxInputWidget( [
+ 'name' => 'wpFilterActionThrottle',
+ 'id' => 'mw-abusefilter-action-checkbox-throttle',
+ 'selected' => $set,
+ 'classes' => [ 'mw-abusefilter-action-checkbox' ]
+ ] + $readOnlyAttrib
+ ),
+ [
+ 'label' => $this->msg( 'abusefilter-edit-action-throttle' )->text(),
+ 'align' => 'inline'
+ ]
+ );
$throttleFields = [];
if ( $set ) {
@@ -733,94 +536,188 @@ class AbuseFilterViewEdit extends AbuseFilterView {
$throttleCount = $throttleRate[0];
$throttlePeriod = $throttleRate[1];
- $throttleGroups = implode( "\n", array_slice( $parameters, 1 ) );
+ $throttleGroups = array_slice( $parameters, 1 );
} else {
$throttleCount = 3;
$throttlePeriod = 60;
- $throttleGroups = "user\n";
+ $throttleGroups = [ 'user' ];
}
$throttleFields['abusefilter-edit-throttle-count'] =
- Xml::input( 'wpFilterThrottleCount', 20, $throttleCount, $readOnlyAttrib );
+ new OOUI\FieldLayout(
+ new OOUI\TextInputWidget( [
+ 'type' => 'number',
+ 'name' => 'wpFilterThrottleCount',
+ 'value' => $throttleCount
+ ] + $readOnlyAttrib
+ ),
+ [
+ 'label' => $this->msg( 'abusefilter-edit-throttle-count' )->text()
+ ]
+ );
$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 );
+ new OOUI\FieldLayout(
+ new OOUI\TextInputWidget( [
+ 'type' => 'number',
+ 'name' => 'wpFilterThrottlePeriod',
+ 'value' => $throttlePeriod
+ ] + $readOnlyAttrib
+ ),
+ [
+ 'label' => $this->msg( 'abusefilter-edit-throttle-period' )->text()
+ ]
+ );
+
+ $throttleConfig = [
+ 'values' => $throttleGroups,
+ 'label' => $this->msg( 'abusefilter-edit-throttle-groups' )->parse(),
+ 'disabled' => $readOnlyAttrib
+ ];
+ $this->getOutput()->addJsConfigVars( 'throttleConfig', $throttleConfig );
+
+ $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'
+ ] + $readOnlyAttrib
+ ),
+ [
+ 'label' => new OOUI\HtmlSnippet(
+ $this->msg( 'abusefilter-edit-throttle-groups' )->parse()
+ ),
+ 'align' => 'top',
+ 'id' => 'mw-abusefilter-hidden-throttle'
+ ]
+ );
+
+ $throttleFields['abusefilter-edit-throttle-groups'] = $hiddenGroups;
+
$throttleSettings .=
Xml::tags(
'div',
[ 'id' => 'mw-abusefilter-throttle-parameters' ],
- Xml::buildForm( $throttleFields )
+ new OOUI\FieldsetLayout( [ 'items' => $throttleFields ] )
);
return $throttleSettings;
+ case 'disallow':
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];
+ $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' ]
+ ] + $readOnlyAttrib
+ ),
+ [
+ // abusefilter-edit-action-warn, abusefilter-edit-action-disallow
+ 'label' => $this->msg( "abusefilter-edit-action-$action" )->text(),
+ 'align' => 'inline'
+ ]
+ );
+ $output .= $checkbox;
+ $defaultWarnMsg = $config->get( 'AbuseFilterDefaultWarningMessage' );
+ $defaultDisallowMsg = $config->get( 'AbuseFilterDefaultDisallowMessage' );
+
+ if ( $set && isset( $parameters[0] ) ) {
+ $msg = $parameters[0];
} elseif (
$row &&
- isset( $row->af_group ) && $row->af_group &&
- isset( $wgAbuseFilterDefaultWarningMessage[$row->af_group] )
+ isset( $row->af_group ) && $row->af_group && (
+ ( $action === 'warn' && isset( $defaultWarnMsg[$row->af_group] ) ) ||
+ ( $action === 'disallow' && isset( $defaultDisallowMsg[$row->af_group] ) )
+ )
) {
- $warnMsg = $wgAbuseFilterDefaultWarningMessage[$row->af_group];
+ $msg = $action === 'warn' ? $defaultWarnMsg[$row->af_group] :
+ $defaultDisallowMsg[$row->af_group];
} else {
- $warnMsg = 'abusefilter-warning';
+ $msg = $action === 'warn' ? 'abusefilter-warning' : 'abusefilter-disallowed';
}
- $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
+ $fields["abusefilter-edit-$action-message"] =
+ $this->getExistingSelector( $msg, !empty( $readOnlyAttrib ), $action );
+ $otherFieldName = $action === 'warn' ? 'wpFilterWarnMessageOther'
+ : 'wpFilterDisallowMessageOther';
+
+ $fields["abusefilter-edit-$action-other-label"] =
+ 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
+ ] + $readOnlyAttrib
+ ),
+ [
+ 'label' => new OOUI\HtmlSnippet(
+ // abusefilter-edit-warn-other-label, abusefilter-edit-disallow-other-label
+ $this->msg( "abusefilter-edit-$action-other-label" )->parse()
+ )
+ ]
);
- $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()
+ $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 ( $this->getUser()->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 = Xml::element(
+ $previewHolder = Xml::tags(
'div',
- [ 'id' => 'mw-abusefilter-warn-preview' ], ''
+ [
+ // mw-abusefilter-warn-preview, mw-abusefilter-disallow-preview
+ 'id' => "mw-abusefilter-$action-preview",
+ 'style' => 'display:none'
+ ],
+ ''
);
- $warnFields['abusefilter-edit-warn-actions'] =
- Xml::tags( 'p', null, $previewButton . $editButton ) . "\n$previewHolder";
+ $fields["abusefilter-edit-$action-actions"] = $buttonGroup;
$output .=
Xml::tags(
'div',
- [ 'id' => 'mw-abusefilter-warn-parameters' ],
- Xml::buildForm( $warnFields )
- );
+ // mw-abusefilter-warn-parameters, mw-abusefilter-disallow-parameters
+ [ 'id' => "mw-abusefilter-$action-parameters" ],
+ new OOUI\FieldsetLayout( [ 'items' => $fields ] )
+ ) . $previewHolder;
+
return $output;
case 'tag':
if ( $set ) {
@@ -830,71 +727,225 @@ class AbuseFilterViewEdit extends AbuseFilterView {
}
$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 );
+ $checkbox =
+ new OOUI\FieldLayout(
+ new OOUI\CheckboxInputWidget( [
+ 'name' => 'wpFilterActionTag',
+ 'id' => 'mw-abusefilter-action-checkbox-tag',
+ 'selected' => $set,
+ 'classes' => [ 'mw-abusefilter-action-checkbox' ]
+ ] + $readOnlyAttrib
+ ),
+ [
+ 'label' => $this->msg( 'abusefilter-edit-action-tag' )->text(),
+ 'align' => 'inline'
+ ]
+ );
+ $output .= $checkbox;
+
+ $tagConfig = [
+ 'values' => $tags,
+ 'label' => $this->msg( 'abusefilter-edit-tag-tag' )->parse(),
+ 'disabled' => $readOnlyAttrib
+ ];
+ $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'
+ ] + $readOnlyAttrib
+ ),
+ [
+ 'label' => new OOUI\HtmlSnippet(
+ $this->msg( 'abusefilter-edit-tag-tag' )->parse()
+ ),
+ 'align' => 'top',
+ 'id' => 'mw-abusefilter-hidden-tag'
+ ]
+ );
$output .=
Xml::tags( 'div',
[ 'id' => 'mw-abusefilter-tag-parameters' ],
- Xml::buildForm( $tagFields )
+ $hiddenTags
+ );
+ return $output;
+ case 'block':
+ 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 ( $config->get( 'AbuseFilterAnonBlockDuration' ) ) {
+ $defaultAnonDuration = $config->get( 'AbuseFilterAnonBlockDuration' );
+ } else {
+ $defaultAnonDuration = $config->get( 'AbuseFilterBlockDuration' );
+ }
+ $defaultUserDuration = $config->get( 'AbuseFilterBlockDuration' );
+ }
+ $suggestedBlocks = SpecialBlock::getSuggestedDurations( null, 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' ]
+ ] + $readOnlyAttrib
+ ),
+ [
+ 'label' => $this->msg( 'abusefilter-edit-action-block' )->text(),
+ 'align' => 'inline'
+ ]
+ );
+ $output .= $checkbox;
+
+ $suggestedBlocks = Xml::listDropDownOptionsOoui( $suggestedBlocks );
+
+ $anonDuration =
+ new OOUI\DropdownInputWidget( [
+ 'name' => 'wpBlockAnonDuration',
+ 'options' => $suggestedBlocks,
+ 'value' => $defaultAnonDuration,
+ 'disabled' => !$this->canEditFilter( $row )
+ ] );
+
+ $userDuration =
+ new OOUI\DropdownInputWidget( [
+ 'name' => 'wpBlockUserDuration',
+ 'options' => $suggestedBlocks,
+ 'value' => $defaultUserDuration,
+ 'disabled' => !$this->canEditFilter( $row )
+ ] );
+
+ $blockOptions = [];
+ if ( $config->get( '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' ]
+ ] + $readOnlyAttrib
+ ),
+ [
+ 'label' => $this->msg( 'abusefilter-edit-action-blocktalk' )->text(),
+ 'align' => 'left'
+ ]
+ );
+
+ $blockOptions['abusefilter-edit-block-options'] = $talkCheckbox;
+ }
+ $blockOptions['abusefilter-edit-block-anon-durations'] =
+ new OOUI\FieldLayout(
+ $anonDuration,
+ [
+ 'label' => $this->msg( 'abusefilter-edit-block-anon-durations' )->text()
+ ]
);
+ $blockOptions['abusefilter-edit-block-user-durations'] =
+ new OOUI\FieldLayout(
+ $userDuration,
+ [
+ 'label' => $this->msg( 'abusefilter-edit-block-user-durations' )->text()
+ ]
+ );
+
+ $output .= Xml::tags(
+ 'div',
+ [ 'id' => 'mw-abusefilter-block-parameters' ],
+ new OOUI\FieldsetLayout( [ 'items' => $blockOptions ] )
+ );
+
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-block
- // abusefilter-edit-action-throttle, abusefilter-edit-action-rangeblock
- // abusefilter-edit-action-tag
+ // abusefilter-edit-action-disallow,
+ // abusefilter-edit-action-blockautopromote,
+ // abusefilter-edit-action-degroup,
+ // abusefilter-edit-action-rangeblock,
$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 );
+ $thisAction =
+ new OOUI\FieldLayout(
+ new OOUI\CheckboxInputWidget( [
+ 'name' => $form_field,
+ 'id' => "mw-abusefilter-action-checkbox-$action",
+ 'selected' => $status,
+ 'classes' => [ 'mw-abusefilter-action-checkbox' ]
+ ] + $readOnlyAttrib
+ ),
+ [
+ 'label' => $this->msg( $message )->text(),
+ 'align' => 'inline'
+ ]
+ );
+ $thisAction = $thisAction;
return $thisAction;
}
}
/**
- * @param $warnMsg string
- * @param $readOnly bool
- * @return string
+ * @param string $warnMsg
+ * @param bool $readOnly
+ * @param string $action
+ * @return \OOUI\FieldLayout
*/
- function getExistingSelector( $warnMsg, $readOnly = false ) {
- $existingSelector = new XmlSelect(
- 'wpFilterWarnMessage',
- 'mw-abusefilter-warn-message-existing',
- $warnMsg == 'abusefilter-warning' ? 'abusefilter-warning' : 'other'
- );
+ 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 MWException( "Unexpected action value $action" );
+ }
- $existingSelector->addOption( 'abusefilter-warning' );
+ $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->setAttribute( 'disabled', 'disabled' );
+ $existingSelector->setDisabled( true );
} else {
// Find other messages.
$dbr = wfGetDB( DB_REPLICA );
+ $pageTitlePrefix = "Abusefilter-$action";
$res = $dbr->select(
'page',
[ 'page_title' ],
[
'page_namespace' => 8,
- 'page_title LIKE ' . $dbr->addQuotes( 'Abusefilter-warning%' )
+ 'page_title LIKE ' . $dbr->addQuotes( $pageTitlePrefix . '%' )
],
__METHOD__
);
@@ -902,27 +953,87 @@ class AbuseFilterViewEdit extends AbuseFilterView {
$lang = $this->getLanguage();
foreach ( $res as $row ) {
if ( $lang->lcfirst( $row->page_title ) == $lang->lcfirst( $warnMsg ) ) {
- $existingSelector->setDefault( $lang->lcfirst( $warnMsg ) );
+ $existingSelector->setValue( $lang->lcfirst( $warnMsg ) );
}
- if ( $row->page_title != 'Abusefilter-warning' ) {
- $existingSelector->addOption( $lang->lcfirst( $row->page_title ) );
+ if ( $row->page_title != "Abusefilter-$action" ) {
+ $options += [ $lang->lcfirst( $row->page_title ) => $lang->lcfirst( $row->page_title ) ];
}
}
}
- $existingSelector->addOption( $this->msg( 'abusefilter-edit-warn-other' )->text(), 'other' );
+ // abusefilter-edit-warn-other, abusefilter-edit-disallow-other
+ $options += [ $this->msg( "abusefilter-edit-$formId-other" )->text() => 'other' ];
+
+ $options = Xml::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 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.
+ $anonDuration = self::getAbsoluteBlockDuration( $wgAbuseFilterAnonBlockDuration );
+ $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;
+ }
- return $existingSelector->getHTML();
+ /**
+ * 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
+ */
+ protected static function getAbsoluteBlockDuration( $duration ) {
+ if ( wfIsInfinity( $duration ) ) {
+ return 'infinity';
+ }
+ return strtotime( $duration, 0 );
}
/**
* Loads filter data from the database by ID.
- * @param $id int The filter's ID number
+ * @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 ) {
+ public function loadFilterData( $id ) {
if ( $id == 'new' ) {
$obj = new stdClass;
$obj->af_pattern = '';
@@ -969,8 +1080,9 @@ class AbuseFilterViewEdit extends AbuseFilterView {
// Load the actions
$actions = [];
- $res = $dbr->select( 'abuse_filter_action',
- '*',
+ $res = $dbr->select(
+ 'abuse_filter_action',
+ [ 'afa_consequence', 'afa_parameters' ],
[ 'afa_filter' => $id ],
__METHOD__
);
@@ -978,7 +1090,7 @@ class AbuseFilterViewEdit extends AbuseFilterView {
foreach ( $res as $actionRow ) {
$thisAction = [];
$thisAction['action'] = $actionRow->afa_consequence;
- $thisAction['parameters'] = explode( "\n", $actionRow->afa_parameters );
+ $thisAction['parameters'] = array_filter( explode( "\n", $actionRow->afa_parameters ) );
$actions[$actionRow->afa_consequence] = $thisAction;
}
@@ -991,21 +1103,21 @@ class AbuseFilterViewEdit extends AbuseFilterView {
* Either from the HTTP request or from the filter/history_id given.
* The HTTP request always takes precedence.
* Includes caching.
- * @param $filter int The filter ID being requested.
- * @param $history_id int If any, the history ID being requested.
- * @return Array with filter data if available, otherwise null.
+ * @param int $filter The filter ID being requested.
+ * @param int|null $history_id If any, the history ID being requested.
+ * @return array|null 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;
+ public function loadRequest( $filter, $history_id = null ) {
+ $row = self::$mLoadedRow;
+ $actions = self::$mLoadedActions;
$request = $this->getRequest();
if ( !is_null( $actions ) && !is_null( $row ) ) {
return [ $row, $actions ];
} elseif ( $request->wasPosted() ) {
- # Nothing, we do it all later
+ // Nothing, we do it all later
} elseif ( $history_id ) {
return $this->loadHistoryItem( $history_id );
} else {
@@ -1051,18 +1163,17 @@ class AbuseFilterViewEdit extends AbuseFilterView {
$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;
+ $row->af_deleted = $request->getCheck( 'wpFilterDeleted' );
+ $row->af_enabled = $request->getCheck( 'wpFilterEnabled' );
+ $row->af_hidden = $request->getCheck( 'wpFilterHidden' );
+ $row->af_global = $request->getCheck( 'wpFilterGlobal' )
+ && $this->getConfig()->get( 'AbuseFilterIsCentral' );
// Actions
- global $wgAbuseFilterActions;
$actions = [];
- foreach ( array_filter( $wgAbuseFilterActions ) as $action => $_ ) {
+ foreach ( array_filter( $this->getConfig()->get( 'AbuseFilterActions' ) ) as $action => $_ ) {
// Check if it's set
- $enabled = $request->getBool( 'wpFilterAction' . ucfirst( $action ) );
+ $enabled = $request->getCheck( 'wpFilterAction' . ucfirst( $action ) );
if ( $enabled ) {
$parameters = [];
@@ -1071,10 +1182,20 @@ class AbuseFilterViewEdit extends AbuseFilterView {
// We need to load the parameters
$throttleCount = $request->getIntOrNull( 'wpFilterThrottleCount' );
$throttlePeriod = $request->getIntOrNull( 'wpFilterThrottlePeriod' );
- $throttleGroups = explode( "\n",
- trim( $request->getText( 'wpFilterThrottleGroups' ) ) );
+ // 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 ) {
+ if ( strpos( $group, ',' ) !== false ) {
+ $subGroups = explode( ',', $group );
+ $throttleGroups[] = implode( ',', array_map( 'trim', $subGroups ) );
+ } elseif ( trim( $group ) !== '' ) {
+ $throttleGroups[] = trim( $group );
+ }
+ }
- $parameters[0] = $this->mFilter; // For now, anyway
+ $parameters[0] = $this->mFilter;
$parameters[1] = "$throttleCount,$throttlePeriod";
$parameters = array_merge( $parameters, $throttleGroups );
} elseif ( $action == 'warn' ) {
@@ -1085,8 +1206,21 @@ class AbuseFilterViewEdit extends AbuseFilterView {
}
$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 == 'disallow' ) {
+ $specMsg = $request->getVal( 'wpFilterDisallowMessage' );
+
+ if ( $specMsg == 'other' ) {
+ $specMsg = $request->getVal( 'wpFilterDisallowMessageOther' );
+ }
+
+ $parameters[0] = $specMsg;
} elseif ( $action == 'tag' ) {
- $parameters = explode( "\n", $request->getText( 'wpFilterTags' ) );
+ $parameters = explode( ',', trim( $request->getText( 'wpFilterTags' ) ) );
}
$thisAction = [ 'action' => $action, 'parameters' => $parameters ];
@@ -1097,20 +1231,21 @@ class AbuseFilterViewEdit extends AbuseFilterView {
$row->af_actions = implode( ',', array_keys( array_filter( $actions ) ) );
+ self::$mLoadedRow = $row;
+ self::$mLoadedActions = $actions;
return [ $row, $actions ];
}
/**
* Loads historical data in a form that the editor can understand.
- * @param $id int History ID
+ * @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 ) {
+ public function loadHistoryItem( $id ) {
$dbr = wfGetDB( DB_REPLICA );
- // Load the row.
$row = $dbr->selectRow( 'abuse_filter_history',
'*',
[ 'afh_id' => $id ],
@@ -1124,11 +1259,17 @@ class AbuseFilterViewEdit extends AbuseFilterView {
return AbuseFilter::translateFromHistory( $row );
}
- protected function exposeWarningMessages() {
- global $wgOut, $wgAbuseFilterDefaultWarningMessage;
- $wgOut->addJsConfigVars(
+ /**
+ * Exports the default warning and disallow messages to a JS variable
+ */
+ protected function exposeMessages() {
+ $this->getOutput()->addJsConfigVars(
'wgAbuseFilterDefaultWarningMessage',
- $wgAbuseFilterDefaultWarningMessage
+ $this->getConfig()->get( 'AbuseFilterDefaultWarningMessage' )
+ );
+ $this->getOutput()->addJsConfigVars(
+ 'wgAbuseFilterDefaultDisallowMessage',
+ $this->getConfig()->get( 'AbuseFilterDefaultDisallowMessage' )
);
}
}
diff --git a/AbuseFilter/includes/Views/AbuseFilterViewExamine.php b/AbuseFilter/includes/Views/AbuseFilterViewExamine.php
index 4d72a7d4..6c3d065c 100644
--- a/AbuseFilter/includes/Views/AbuseFilterViewExamine.php
+++ b/AbuseFilter/includes/Views/AbuseFilterViewExamine.php
@@ -4,10 +4,13 @@ class AbuseFilterViewExamine extends AbuseFilterView {
public static $examineType = null;
public static $examineId = null;
- public $mCounter, $mSearchUser, $mSearchPeriodStart, $mSearchPeriodEnd,
- $mTestFilter;
+ public $mCounter, $mSearchUser, $mSearchPeriodStart, $mSearchPeriodEnd;
+ public $mTestFilter;
- function show() {
+ /**
+ * Shows the page
+ */
+ public function show() {
$out = $this->getOutput();
$out->setPageTitle( $this->msg( 'abusefilter-examine' ) );
$out->addWikiMsg( 'abusefilter-examine-intro' );
@@ -27,25 +30,36 @@ class AbuseFilterViewExamine extends AbuseFilterView {
}
}
- function showSearch() {
+ /**
+ * Shows the search form
+ */
+ public 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',
+ 'ipallowed' => true,
'default' => $this->mSearchUser,
],
'SearchPeriodStart' => [
'label-message' => 'abusefilter-test-period-start',
- 'type' => 'text',
+ 'type' => 'datetime',
'default' => $this->mSearchPeriodStart,
+ 'min' => $min,
+ 'max' => $max,
],
'SearchPeriodEnd' => [
'label-message' => 'abusefilter-test-period-end',
- 'type' => 'text',
+ 'type' => 'datetime',
'default' => $this->mSearchPeriodEnd,
+ 'min' => $min,
+ 'max' => $max,
],
];
- $htmlForm = HTMLForm::factory( 'table', $formDescriptor, $this->getContext() );
+ $htmlForm = HTMLForm::factory( 'ooui', $formDescriptor, $this->getContext() );
$htmlForm->setWrapperLegendMsg( 'abusefilter-examine-legend' )
->addHiddenField( 'submit', 1 )
->setSubmitTextMsg( 'abusefilter-examine-submit' )
@@ -58,8 +72,11 @@ class AbuseFilterViewExamine extends AbuseFilterView {
}
}
- function showResults() {
- $changesList = new AbuseFilterChangesList( $this->getSkin() );
+ /**
+ * Show search results
+ */
+ public function showResults() {
+ $changesList = new AbuseFilterChangesList( $this->getSkin(), $this->mTestFilter );
$output = $changesList->beginRecentChangesList();
$this->mCounter = 1;
@@ -74,14 +91,20 @@ class AbuseFilterViewExamine extends AbuseFilterView {
$this->getOutput()->addHTML( $output );
}
- function showExaminerForRC( $rcid ) {
+ /**
+ * @param int $rcid
+ */
+ public function showExaminerForRC( $rcid ) {
// Get data
$dbr = wfGetDB( DB_REPLICA );
+ $rcQuery = RecentChange::getQueryInfo();
$row = $dbr->selectRow(
- 'recentchanges',
- RecentChange::selectFields(),
+ $rcQuery['tables'],
+ $rcQuery['fields'],
[ 'rc_id' => $rcid ],
- __METHOD__
+ __METHOD__,
+ [],
+ $rcQuery['joins']
);
$out = $this->getOutput();
if ( !$row ) {
@@ -102,10 +125,23 @@ class AbuseFilterViewExamine extends AbuseFilterView {
$this->showExaminer( $vars );
}
- function showExaminerForLogEntry( $logid ) {
+ /**
+ * @param int $logid
+ */
+ public function showExaminerForLogEntry( $logid ) {
// Get data
$dbr = wfGetDB( DB_REPLICA );
- $row = $dbr->selectRow( 'abuse_filter_log', '*', [ 'afl_id' => $logid ], __METHOD__ );
+ $row = $dbr->selectRow(
+ 'abuse_filter_log',
+ [
+ 'afl_filter',
+ 'afl_deleted',
+ 'afl_var_dump',
+ 'afl_rev_id'
+ ],
+ [ 'afl_id' => $logid ],
+ __METHOD__
+ );
$out = $this->getOutput();
if ( !$row ) {
@@ -138,8 +174,12 @@ class AbuseFilterViewExamine extends AbuseFilterView {
$this->showExaminer( $vars );
}
- function showExaminer( $vars ) {
+ /**
+ * @param AbuseFilterVariableHolder|null $vars
+ */
+ public function showExaminer( $vars ) {
$output = $this->getOutput();
+ $output->enableOOUI();
if ( !$vars ) {
$output->addWikiMsg( 'abusefilter-examine-incompatible' );
@@ -155,34 +195,16 @@ class AbuseFilterViewExamine extends AbuseFilterView {
$output->addModules( 'ext.abuseFilter.examine' );
// Add test bit
- if ( $this->getUser()->isAllowed( 'abusefilter-modify' ) ) {
+ if ( $this->canViewPrivate() ) {
$tester = Xml::tags( 'h2', null, $this->msg( 'abusefilter-examine-test' )->parse() );
- $tester .= AbuseFilter::buildEditBox( $this->mTestFilter, 'wpTestFilter', false );
- $tester .=
- "\n" .
- Xml::inputLabel(
- $this->msg( 'abusefilter-test-load-filter' )->text(),
- 'wpInsertFilter',
- 'mw-abusefilter-load-filter',
- 10,
- ''
- ) .
- '&#160;' .
- Xml::element(
- 'input',
- [
- 'type' => 'button',
- 'value' => $this->msg( 'abusefilter-test-load' )->text(),
- 'id' => 'mw-abusefilter-load'
- ]
- );
+ $tester .= $this->buildEditBox( $this->mTestFilter, 'wpTestFilter', false, false, false );
+ $tester .= AbuseFilter::buildFilterLoader();
$html .= Xml::tags( 'div', [ 'id' => 'mw-abusefilter-examine-editor' ], $tester );
$html .= Xml::tags( 'p',
null,
- Xml::element( 'input',
+ new OOUI\ButtonInputWidget(
[
- 'type' => 'button',
- 'value' => $this->msg( 'abusefilter-examine-test-button' )->text(),
+ 'label' => $this->msg( 'abusefilter-examine-test-button' )->text(),
'id' => 'mw-abusefilter-examine-test'
]
) .
@@ -206,83 +228,19 @@ class AbuseFilterViewExamine extends AbuseFilterView {
$output->addHTML( $html );
}
- function loadParameters() {
+ /**
+ * Loads parameters from request
+ */
+ public function loadParameters() {
$request = $this->getRequest();
- $searchUsername = $request->getText( 'wpSearchUser' );
$this->mSearchPeriodStart = $request->getText( 'wpSearchPeriodStart' );
$this->mSearchPeriodEnd = $request->getText( 'wpSearchPeriodEnd' );
$this->mSubmit = $request->getCheck( 'submit' );
$this->mTestFilter = $request->getText( 'testfilter' );
// Normalise username
- $userTitle = Title::newFromText( $searchUsername );
-
- if ( $userTitle && $userTitle->getNamespace() == NS_USER ) {
- $this->mSearchUser = $userTitle->getText(); // Allow User:Blah syntax.
- } elseif ( $userTitle ) {
- // Not sure of the value of prefixedText over text, but no need to munge unnecessarily.
- $this->mSearchUser = $userTitle->getPrefixedText();
- } else {
- $this->mSearchUser = '';
- }
- }
-}
-
-class AbuseFilterExaminePager extends ReverseChronologicalPager {
- /**
- * @param AbuseFilterViewExamine $page
- * @param AbuseFilterChangesList $changesList
- */
- function __construct( $page, $changesList ) {
- parent::__construct();
- $this->mChangesList = $changesList;
- $this->mPage = $page;
- }
-
- /**
- * @fixme this is similar to AbuseFilterViewTestBatch::doTest
- */
- function getQueryInfo() {
- $dbr = wfGetDB( DB_REPLICA );
- $conds = [];
- $conds['rc_user_text'] = $this->mPage->mSearchUser;
-
- $startTS = strtotime( $this->mPage->mSearchPeriodStart );
- if ( $startTS ) {
- $conds[] = 'rc_timestamp>=' . $dbr->addQuotes( $dbr->timestamp( $startTS ) );
- }
- $endTS = strtotime( $this->mPage->mSearchPeriodEnd );
- if ( $endTS ) {
- $conds[] = 'rc_timestamp<=' . $dbr->addQuotes( $dbr->timestamp( $endTS ) );
- }
-
- $conds[] = $this->mPage->buildTestConditions( $dbr );
-
- $info = [
- 'tables' => 'recentchanges',
- 'fields' => RecentChange::selectFields(),
- 'conds' => array_filter( $conds ),
- 'options' => [ 'ORDER BY' => 'rc_timestamp DESC' ],
- ];
-
- return $info;
- }
-
- function formatRow( $row ) {
- $rc = RecentChange::newFromRow( $row );
- $rc->counter = $this->mPage->mCounter++;
- return $this->mChangesList->recentChangesLine( $rc, false );
- }
-
- function getIndexField() {
- return 'rc_id';
- }
-
- function getTitle() {
- return $this->mPage->getTitle( 'examine' );
- }
-
- function getEmptyBody() {
- return $this->msg( 'abusefilter-examine-noresults' )->parseAsBlock();
+ $searchUsername = $request->getText( 'wpSearchUser' );
+ $userTitle = Title::newFromText( $searchUsername, NS_USER );
+ $this->mSearchUser = $userTitle ? $userTitle->getText() : '';
}
}
diff --git a/AbuseFilter/includes/Views/AbuseFilterViewHistory.php b/AbuseFilter/includes/Views/AbuseFilterViewHistory.php
index a6ef187c..66a14a56 100644
--- a/AbuseFilter/includes/Views/AbuseFilterViewHistory.php
+++ b/AbuseFilter/includes/Views/AbuseFilterViewHistory.php
@@ -1,22 +1,30 @@
<?php
class AbuseFilterViewHistory extends AbuseFilterView {
- function __construct( $page, $params ) {
+ /**
+ * @param SpecialAbuseFilter $page
+ * @param array $params
+ */
+ public function __construct( $page, $params ) {
parent::__construct( $page, $params );
$this->mFilter = $page->mFilter;
}
- function show() {
+ /**
+ * Shows the page
+ */
+ public function show() {
$out = $this->getOutput();
- $filter = $this->mFilter;
+ $out->enableOOUI();
+ $filter = $this->getRequest()->getText( 'filter' ) ?: $this->mFilter;
if ( $filter ) {
- $out->setPageTitle( $this->msg( 'abusefilter-history', $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
+ // Check perms. abusefilter-modify is a superset of abusefilter-view-private
if ( $filter && AbuseFilter::filterHidden( $filter )
&& !$this->getUser()->isAllowedAny( 'abusefilter-modify', 'abusefilter-view-private' )
) {
@@ -24,30 +32,36 @@ class AbuseFilterViewHistory extends AbuseFilterView {
return;
}
- # Useful links
+ // Useful links
$links = [];
if ( $filter ) {
- $links['abusefilter-history-backedit'] = $this->getTitle( $filter );
+ $links['abusefilter-history-backedit'] = $this->getTitle( $filter )->getFullURL();
}
foreach ( $links as $msg => $title ) {
- $links[$msg] = $this->linkRenderer->makeLink(
- $title,
- new HtmlArmor( $this->msg( $msg )->parse() )
- );
+ $links[$msg] =
+ new OOUI\ButtonWidget( [
+ 'label' => $this->msg( $msg )->text(),
+ 'href' => $title
+ ] );
}
- $backlinks = $this->getLanguage()->pipeList( $links );
- $out->addHTML( Xml::tags( 'p', null, $backlinks ) );
+ $backlinks =
+ new OOUI\HorizontalLayout( [
+ 'items' => $links
+ ] );
+ $out->addHTML( $backlinks );
- # For user
+ // 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
+ // We don't really need to get a user ID
+ Linker::userLink( 1, $user ),
+ // For GENDER
+ $user
)->text()
);
}
@@ -59,223 +73,30 @@ class AbuseFilterViewHistory extends AbuseFilterView {
'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( 'table', $formDescriptor, $this->getContext() );
+ $htmlForm = HTMLForm::factory( 'ooui', $formDescriptor, $this->getContext() );
$htmlForm->setSubmitTextMsg( 'abusefilter-history-select-submit' )
->setWrapperLegendMsg( 'abusefilter-history-select-legend' )
- ->setAction( $this->getTitle( "history/$filter" )->getLocalURL() )
+ ->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() );
- }
-}
-
-class AbuseFilterHistoryPager extends TablePager {
-
- protected $linkRenderer;
- /**
- * @param $filter
- * @param $page ContextSource
- * @param $user string User name
- * @param \MediaWiki\Linker\LinkRenderer $linkRenderer
- */
- function __construct( $filter, $page, $user, $linkRenderer ) {
- $this->mFilter = $filter;
- $this->mPage = $page;
- $this->mUser = $user;
- $this->mDefaultDirection = true;
- $this->linkRenderer = $linkRenderer;
- parent::__construct( $this->mPage->getContext() );
- }
-
- function getFieldNames() {
- static $headers = null;
-
- if ( !empty( $headers ) ) {
- return $headers;
- }
-
- $headers = [
- 'afh_timestamp' => 'abusefilter-history-timestamp',
- 'afh_user_text' => 'abusefilter-history-user',
- 'afh_public_comments' => 'abusefilter-history-public',
- 'afh_flags' => 'abusefilter-history-flags',
- 'afh_actions' => 'abusefilter-history-actions',
- 'afh_id' => 'abusefilter-history-diff',
- ];
-
- if ( !$this->mFilter ) {
- // awful hack
- $headers = [ 'afh_filter' => 'abusefilter-history-filterid' ] + $headers;
- unset( $headers['afh_comments'] );
- }
-
- foreach ( $headers as &$msg ) {
- $msg = $this->msg( $msg )->text();
- }
-
- return $headers;
- }
-
- function formatValue( $name, $value ) {
- $lang = $this->getLanguage();
-
- $row = $this->mCurrentRow;
-
- switch ( $name ) {
- case 'afh_filter':
- $formatted = $this->linkRenderer->makeLink(
- SpecialPage::getTitleFor( 'AbuseFilter', intval( $row->afh_filter ) ),
- $lang->formatNum( $row->afh_filter )
- );
- break;
- case 'afh_timestamp':
- $title = SpecialPage::getTitleFor( 'AbuseFilter',
- 'history/' . $row->afh_filter . '/item/' . $row->afh_id );
- $formatted = $this->linkRenderer->makeLink(
- $title,
- $lang->timeanddate( $row->afh_timestamp, true )
- );
- break;
- case 'afh_user_text':
- $formatted =
- Linker::userLink( $row->afh_user, $row->afh_user_text ) . ' ' .
- Linker::userToolLinks( $row->afh_user, $row->afh_user_text );
- break;
- case 'afh_public_comments':
- $formatted = htmlspecialchars( $value, ENT_QUOTES, 'UTF-8', false );
- break;
- case 'afh_flags':
- $formatted = AbuseFilter::formatFlags( $value );
- break;
- case 'afh_actions':
- $actions = unserialize( $value );
-
- $display_actions = '';
- foreach ( $actions as $action => $parameters ) {
- $displayAction = AbuseFilter::formatAction( $action, $parameters );
- $display_actions .= Xml::tags( 'li', null, $displayAction );
- }
- $display_actions = Xml::tags( 'ul', null, $display_actions );
-
- $formatted = $display_actions;
- break;
- case 'afh_id':
- $formatted = '';
- if ( AbuseFilter::getFirstFilterChange( $row->afh_filter ) != $value ) {
- // Set a link to a diff with the previous version if this isn't the first edit to the filter
- $title = $this->mPage->getTitle(
- 'history/' . $row->afh_filter . "/diff/prev/$value" );
- $formatted = $this->linkRenderer->makeLink(
- $title,
- new HtmlArmor( $this->msg( 'abusefilter-history-diff' )->parse() )
- );
- }
- break;
- default:
- $formatted = "Unable to format $name";
- break;
- }
-
- $mappings = array_flip( AbuseFilter::$history_mappings ) +
- [ 'afh_actions' => 'actions', 'afh_id' => 'id' ];
- $changed = explode( ',', $row->afh_changed_fields );
-
- $fieldChanged = false;
- if ( $name == 'afh_flags' ) {
- // This is a bit freaky, but it works.
- // Basically, returns true if any of those filters are in the $changed array.
- $filters = [ 'af_enabled', 'af_hidden', 'af_deleted', 'af_global' ];
- if ( count( array_diff( $filters, $changed ) ) < count( $filters ) ) {
- $fieldChanged = true;
- }
- } elseif ( in_array( $mappings[$name], $changed ) ) {
- $fieldChanged = true;
- }
-
- if ( $fieldChanged ) {
- $formatted = Xml::tags( 'div',
- [ 'class' => 'mw-abusefilter-history-changed' ],
- $formatted
- );
- }
-
- return $formatted;
- }
-
- function getQueryInfo() {
- $info = [
- 'tables' => [ 'abuse_filter_history', 'abuse_filter' ],
- 'fields' => [
- 'afh_filter',
- 'afh_timestamp',
- 'afh_user_text',
- 'afh_public_comments',
- 'afh_flags',
- 'afh_comments',
- 'afh_actions',
- 'afh_id',
- 'afh_user',
- 'afh_changed_fields',
- 'afh_pattern',
- 'afh_id',
- 'af_hidden'
- ],
- 'conds' => [],
- 'join_conds' => [
- 'abuse_filter' =>
- [
- 'LEFT JOIN',
- 'afh_filter=af_id',
- ],
- ],
- ];
-
- if ( $this->mUser ) {
- $info['conds']['afh_user_text'] = $this->mUser;
- }
-
- if ( $this->mFilter ) {
- $info['conds']['afh_filter'] = $this->mFilter;
- }
-
- if ( !$this->getUser()->isAllowedAny(
- 'abusefilter-modify', 'abusefilter-view-private' )
- ) {
- // Hide data the user can't see.
- $info['conds']['af_hidden'] = 0;
- }
-
- return $info;
- }
-
- function getIndexField() {
- return 'afh_timestamp';
- }
-
- function getDefaultSort() {
- return 'afh_timestamp';
- }
-
- function isFieldSortable( $name ) {
- $sortable_fields = [ 'afh_timestamp', 'afh_user_text' ];
- return in_array( $name, $sortable_fields );
- }
-
- /**
- * Title used for self-links.
- *
- * @return Title
- */
- function getTitle() {
- return $this->mPage->getTitle( 'history/' . $this->mFilter );
+ $out->addHTML(
+ $pager->getNavigationBar() .
+ $pager->getBody() .
+ $pager->getNavigationBar()
+ );
}
}
diff --git a/AbuseFilter/includes/Views/AbuseFilterViewImport.php b/AbuseFilter/includes/Views/AbuseFilterViewImport.php
index 6bd4c269..01e2fc85 100644
--- a/AbuseFilter/includes/Views/AbuseFilterViewImport.php
+++ b/AbuseFilter/includes/Views/AbuseFilterViewImport.php
@@ -1,7 +1,10 @@
<?php
class AbuseFilterViewImport extends AbuseFilterView {
- function show() {
+ /**
+ * Shows the page
+ */
+ public function show() {
$out = $this->getOutput();
if ( !$this->getUser()->isAllowed( 'abusefilter-modify' ) ) {
$out->addWikiMsg( 'abusefilter-edit-notallowed' );
diff --git a/AbuseFilter/includes/Views/AbuseFilterViewList.php b/AbuseFilter/includes/Views/AbuseFilterViewList.php
index daecf8d7..ad92ad6c 100644
--- a/AbuseFilter/includes/Views/AbuseFilterViewList.php
+++ b/AbuseFilter/includes/Views/AbuseFilterViewList.php
@@ -4,48 +4,76 @@
* The default view used in Special:AbuseFilter
*/
class AbuseFilterViewList extends AbuseFilterView {
- function show() {
- global $wgAbuseFilterCentralDB, $wgAbuseFilterIsCentral;
-
+ /**
+ * Shows the page
+ */
+ public function show() {
$out = $this->getOutput();
$request = $this->getRequest();
+ $config = $this->getConfig();
- // Status info...
+ // Show filter performance statistics
$this->showStatus();
$out->addWikiMsg( 'abusefilter-intro' );
// New filter button
if ( $this->canEdit() ) {
- $title = $this->getTitle( 'new' );
- $link = $this->linkRenderer->makeLink( $title, $this->msg( 'abusefilter-new' )->text() );
- $links = Xml::tags( 'p', null, $link ) . "\n";
- $out->addHTML( $links );
+ $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' );
+ $furtherOptions = $request->getArray( 'furtheroptions', [] );
+ // Backward compatibility with old links
+ if ( $request->getBool( 'hidedisabled' ) ) {
+ $furtherOptions[] = 'hidedisabled';
+ }
+ if ( $request->getBool( 'hideprivate' ) ) {
+ $furtherOptions[] = 'hideprivate';
+ }
$defaultscope = 'all';
- if ( isset( $wgAbuseFilterCentralDB ) && !$wgAbuseFilterIsCentral ) {
+ if ( $config->get( 'AbuseFilterCentralDB' ) !== null
+ && !$config->get( 'AbuseFilterIsCentral' ) ) {
// Show on remote wikis as default only local filters
$defaultscope = 'local';
}
$scope = $request->getVal( 'rulescope', $defaultscope );
+ $searchEnabled = $this->canViewPrivate() && !(
+ $config->get( 'AbuseFilterCentralDB' ) !== null &&
+ !$config->get( 'AbuseFilterIsCentral' ) &&
+ $scope == 'global' );
+
+ if ( $searchEnabled ) {
+ $querypattern = $request->getVal( 'querypattern' );
+ $searchmode = $request->getVal( 'searchoption', 'LIKE' );
+ } else {
+ $querypattern = '';
+ $searchmode = '';
+ }
+
if ( $deleted == 'show' ) {
- # Nothing
+ // Nothing
} elseif ( $deleted == 'only' ) {
$conds['af_deleted'] = 1;
- } else { # hide, or anything else.
+ } else {
+ // hide, or anything else.
$conds['af_deleted'] = 0;
$deleted = 'hide';
}
- if ( $hidedisabled ) {
+ if ( in_array( 'hidedisabled', $furtherOptions ) ) {
$conds['af_deleted'] = 0;
$conds['af_enabled'] = 1;
}
+ if ( in_array( 'hideprivate', $furtherOptions ) ) {
+ $conds['af_hidden'] = 0;
+ }
if ( $scope == 'local' ) {
$conds['af_global'] = 0;
@@ -53,123 +81,206 @@ class AbuseFilterViewList extends AbuseFilterView {
$conds['af_global'] = 1;
}
- $this->showList( $conds, compact( 'deleted', 'hidedisabled', 'scope' ) );
- }
-
- function showList( $conds = [ 'af_deleted' => 0 ], $optarray = [] ) {
- global $wgAbuseFilterCentralDB, $wgAbuseFilterIsCentral;
+ $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->addHTML(
+ Xml::tags(
+ 'p',
+ null,
+ Html::errorBox( $this->msg( 'abusefilter-list-regexerror' )->parse() )
+ )
+ );
+ $this->showList(
+ [ 'af_deleted' => 0 ],
+ compact(
+ 'deleted',
+ 'furtherOptions',
+ '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()
+ );
+ }
+ }
- $output = '';
- $output .= Xml::element( 'h2', null,
- $this->msg( 'abusefilter-list' )->parse() );
+ $this->showList(
+ $conds,
+ compact(
+ 'deleted',
+ 'furtherOptions',
+ 'querypattern',
+ 'searchmode',
+ 'scope',
+ 'searchEnabled'
+ )
+ );
+ }
- $pager = new AbuseFilterPager( $this, $conds, $this->linkRenderer );
+ /**
+ * @param array $conds
+ * @param array $optarray
+ */
+ public function showList( $conds = [ 'af_deleted' => 0 ], $optarray = [] ) {
+ $config = $this->getConfig();
+ $this->getOutput()->addHTML(
+ Xml::tags( 'h2', null, $this->msg( 'abusefilter-list' )->parse() )
+ );
$deleted = $optarray['deleted'];
- $hidedisabled = $optarray['hidedisabled'];
+ $furtherOptions = $optarray['furtherOptions'];
$scope = $optarray['scope'];
-
- # Options form
- $fields = [];
- $fields['abusefilter-list-options-deleted'] =
- Xml::radioLabel(
- $this->msg( 'abusefilter-list-options-deleted-show' )->text(),
- 'deletedfilters',
- 'show',
- 'mw-abusefilter-deletedfilters-show',
- $deleted == 'show'
- ) .
- Xml::radioLabel(
- $this->msg( 'abusefilter-list-options-deleted-hide' )->text(),
- 'deletedfilters',
- 'hide',
- 'mw-abusefilter-deletedfilters-hide',
- $deleted == 'hide'
- ) .
- Xml::radioLabel(
- $this->msg( 'abusefilter-list-options-deleted-only' )->text(),
- 'deletedfilters',
- 'only',
- 'mw-abusefilter-deletedfilters-only',
- $deleted == 'only'
+ $searchEnabled = $optarray['searchEnabled'];
+ $querypattern = $optarray['querypattern'];
+ $searchmode = $optarray['searchmode'];
+
+ if (
+ $config->get( 'AbuseFilterCentralDB' ) !== null
+ && !$config->get( 'AbuseFilterIsCentral' )
+ && $scope == 'global'
+ ) {
+ $pager = new GlobalAbuseFilterPager(
+ $this,
+ $conds,
+ $this->linkRenderer
+ );
+ } else {
+ $pager = new AbuseFilterPager(
+ $this,
+ $conds,
+ $this->linkRenderer,
+ [ $querypattern, $searchmode ]
);
+ }
- if ( isset( $wgAbuseFilterCentralDB ) ) {
- $fields['abusefilter-list-options-scope'] =
- Xml::radioLabel(
- $this->msg( 'abusefilter-list-options-scope-local' )->text(),
- 'rulescope',
- 'local',
- 'mw-abusefilter-rulescope-local',
- $scope == 'local'
- ) .
- Xml::radioLabel(
- $this->msg( 'abusefilter-list-options-scope-global' )->text(),
- 'rulescope',
- 'global',
- 'mw-abusefilter-rulescope-global',
- $scope == 'global'
- );
+ // 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 ( $wgAbuseFilterIsCentral ) {
+ if ( $config->get( 'AbuseFilterCentralDB' ) !== null ) {
+ $optionsMsg = [
+ 'abusefilter-list-options-scope-local' => 'local',
+ 'abusefilter-list-options-scope-global' => 'global',
+ ];
+ if ( $config->get( 'AbuseFilterIsCentral' ) ) {
// For central wiki: add third scope option
- $fields['abusefilter-list-options-scope'] .=
- Xml::radioLabel(
- $this->msg( 'abusefilter-list-options-scope-all' )->text(),
- 'rulescope',
- 'all',
- 'mw-abusefilter-rulescope-all',
- $scope == 'all'
- );
+ $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,
+ ];
}
- $fields['abusefilter-list-options-disabled'] =
- Xml::checkLabel(
- $this->msg( 'abusefilter-list-options-hidedisabled' )->text(),
- 'hidedisabled',
- 'mw-abusefilter-disabledfilters-hide',
- $hidedisabled
- );
- $fields['abusefilter-list-limit'] = $pager->getLimitSelect();
-
- $options = Xml::buildForm( $fields, 'abusefilter-list-options-submit' );
- $options .= Html::hidden( 'title', $this->getTitle()->getPrefixedDBkey() );
- $options = Xml::tags( 'form',
- [
- 'method' => 'get',
- 'action' => $this->getTitle()->getFullURL()
+ $formDescriptor['furtheroptions'] = [
+ 'name' => 'furtheroptions',
+ 'type' => 'multiselect',
+ 'label-message' => 'abusefilter-list-options-further-options',
+ 'flatlist' => true,
+ 'options' => [
+ $this->msg( 'abusefilter-list-options-hideprivate' )->parse() => 'hideprivate',
+ $this->msg( 'abusefilter-list-options-hidedisabled' )->parse() => 'hidedisabled',
],
- $options
- );
- $options = Xml::fieldset( $this->msg( 'abusefilter-list-options' )->text(), $options );
-
- $output .= $options;
+ 'default' => $furtherOptions
+ ];
- if ( isset( $wgAbuseFilterCentralDB ) && !$wgAbuseFilterIsCentral && $scope == 'global' ) {
- $globalPager = new GlobalAbuseFilterPager( $this, $conds, $this->linkRenderer );
- $output .=
- $globalPager->getNavigationBar() .
- $globalPager->getBody() .
- $globalPager->getNavigationBar();
- } else {
- $output .=
- $pager->getNavigationBar() .
- $pager->getBody() .
- $pager->getNavigationBar();
+ // 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
+ ];
}
- $this->getOutput()->addHTML( $output );
- }
+ $formDescriptor['limit'] = [
+ 'name' => 'limit',
+ 'type' => 'select',
+ 'label-message' => 'abusefilter-list-limit',
+ 'options' => $pager->getLimitSelectList(),
+ 'default' => $pager->getLimit(),
+ ];
- function showStatus() {
- global $wgAbuseFilterConditionLimit, $wgAbuseFilterValidGroups;
+ 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 );
+
+ $this->getOutput()->addHTML(
+ $pager->getNavigationBar() .
+ $pager->getBody() .
+ $pager->getNavigationBar()
+ );
+ }
+ /**
+ * Show stats
+ */
+ public function showStatus() {
$stash = ObjectCache::getMainStashInstance();
$overflow_count = (int)$stash->get( AbuseFilter::filterLimitReachedKey() );
$match_count = (int)$stash->get( AbuseFilter::filterMatchesKey() );
$total_count = 0;
- foreach ( $wgAbuseFilterValidGroups as $group ) {
+ foreach ( $this->getConfig()->get( 'AbuseFilterValidGroups' ) as $group ) {
$total_count += (int)$stash->get( AbuseFilter::filterUsedKey( $group ) );
}
@@ -182,7 +293,7 @@ class AbuseFilterViewList extends AbuseFilterView {
$total_count,
$overflow_count,
$overflow_percent,
- $wgAbuseFilterConditionLimit,
+ $this->getConfig()->get( 'AbuseFilterConditionLimit' ),
$match_count,
$match_percent
)->parse();
@@ -192,255 +303,3 @@ class AbuseFilterViewList extends AbuseFilterView {
}
}
}
-
-/**
- * Class to build paginated filter list
- */
-// Probably no need to autoload this class, as it will only be called from the class above.
-class AbuseFilterPager extends TablePager {
-
- /**
- * @var \MediaWiki\Linker\LinkRenderer
- */
- protected $linkRenderer;
-
- function __construct( $page, $conds, $linkRenderer ) {
- $this->mPage = $page;
- $this->mConds = $conds;
- $this->linkRenderer = $linkRenderer;
- parent::__construct( $this->mPage->getContext() );
- }
-
- function getQueryInfo() {
- return [
- 'tables' => [ 'abuse_filter' ],
- 'fields' => [
- 'af_id',
- 'af_enabled',
- 'af_deleted',
- 'af_global',
- 'af_public_comments',
- 'af_hidden',
- 'af_hit_count',
- 'af_timestamp',
- 'af_user_text',
- 'af_user',
- 'af_actions',
- 'af_group',
- ],
- 'conds' => $this->mConds,
- ];
- }
-
- function getFieldNames() {
- static $headers = null;
-
- if ( !empty( $headers ) ) {
- return $headers;
- }
-
- $headers = [
- 'af_id' => 'abusefilter-list-id',
- 'af_public_comments' => 'abusefilter-list-public',
- 'af_actions' => 'abusefilter-list-consequences',
- 'af_enabled' => 'abusefilter-list-status',
- 'af_timestamp' => 'abusefilter-list-lastmodified',
- 'af_hidden' => 'abusefilter-list-visibility',
- ];
-
- if ( $this->mPage->getUser()->isAllowed( 'abusefilter-log-detail' ) ) {
- $headers['af_hit_count'] = 'abusefilter-list-hitcount';
- }
-
- global $wgAbuseFilterValidGroups;
- if ( count( $wgAbuseFilterValidGroups ) > 1 ) {
- $headers['af_group'] = 'abusefilter-list-group';
- }
-
- foreach ( $headers as &$msg ) {
- $msg = $this->msg( $msg )->text();
- }
-
- return $headers;
- }
-
- function formatValue( $name, $value ) {
- $lang = $this->getLanguage();
- $row = $this->mCurrentRow;
-
- switch ( $name ) {
- case 'af_id':
- return $this->linkRenderer->makeLink(
- SpecialPage::getTitleFor( 'AbuseFilter', intval( $value ) ),
- $lang->formatNum( intval( $value ) )
- );
- case 'af_public_comments':
- return $this->linkRenderer->makeLink(
- SpecialPage::getTitleFor( 'AbuseFilter', intval( $row->af_id ) ),
- $value
- );
- case 'af_actions':
- $actions = explode( ',', $value );
- $displayActions = [];
- foreach ( $actions as $action ) {
- $displayActions[] = AbuseFilter::getActionDisplay( $action );
- }
- return htmlspecialchars( $lang->commaList( $displayActions ) );
- case 'af_enabled':
- $statuses = [];
- if ( $row->af_deleted ) {
- $statuses[] = $this->msg( 'abusefilter-deleted' )->parse();
- } elseif ( $row->af_enabled ) {
- $statuses[] = $this->msg( 'abusefilter-enabled' )->parse();
- } else {
- $statuses[] = $this->msg( 'abusefilter-disabled' )->parse();
- }
-
- global $wgAbuseFilterIsCentral;
- if ( $row->af_global && $wgAbuseFilterIsCentral ) {
- $statuses[] = $this->msg( 'abusefilter-status-global' )->parse();
- }
-
- return $lang->commaList( $statuses );
- case 'af_hidden':
- $msg = $value ? 'abusefilter-hidden' : 'abusefilter-unhidden';
- return $this->msg( $msg )->parse();
- case 'af_hit_count':
- if ( SpecialAbuseLog::canSeeDetails( $row->af_id, $row->af_hidden ) ) {
- $count_display = $this->msg( 'abusefilter-hitcount' )
- ->numParams( $value )->parse();
- $link = $this->linkRenderer->makeKnownLink(
- SpecialPage::getTitleFor( 'AbuseLog' ),
- $count_display,
- [],
- [ 'wpSearchFilter' => $row->af_id ]
- );
- } else {
- $link = "";
- }
- return $link;
- case 'af_timestamp':
- $userLink =
- Linker::userLink(
- $row->af_user,
- $row->af_user_text
- ) .
- Linker::userToolLinks(
- $row->af_user,
- $row->af_user_text
- );
- $user = $row->af_user_text;
- return $this->msg( 'abusefilter-edit-lastmod-text' )
- ->rawParams( $lang->timeanddate( $value, true ),
- $userLink,
- $lang->date( $value, true ),
- $lang->time( $value, true ),
- $user
- )->parse();
- case 'af_group':
- return AbuseFilter::nameGroup( $value );
- break;
- default:
- throw new MWException( "Unknown row type $name!" );
- }
- }
-
- function getDefaultSort() {
- return 'af_id';
- }
-
- function getRowClass( $row ) {
- if ( $row->af_enabled ) {
- return 'mw-abusefilter-list-enabled';
- } elseif ( $row->af_deleted ) {
- return 'mw-abusefilter-list-deleted';
- } else {
- return 'mw-abusefilter-list-disabled';
- }
- }
-
- function isFieldSortable( $name ) {
- $sortable_fields = [
- 'af_id',
- 'af_enabled',
- 'af_throttled',
- 'af_user_text',
- 'af_timestamp',
- 'af_hidden',
- 'af_group',
- ];
- if ( $this->mPage->getUser()->isAllowed( 'abusefilter-log-detail' ) ) {
- $sortable_fields[] = 'af_hit_count';
- }
- return in_array( $name, $sortable_fields );
- }
-}
-
-/**
- * Class to build paginated filter list for wikis using global abuse filters
- */
-class GlobalAbuseFilterPager extends AbuseFilterPager {
- function __construct( $page, $conds, $linkRenderer ) {
- parent::__construct( $page, $conds, $linkRenderer );
- global $wgAbuseFilterCentralDB;
- $this->mDb = wfGetDB( DB_REPLICA, [], $wgAbuseFilterCentralDB );
- }
-
- function formatValue( $name, $value ) {
- $lang = $this->getLanguage();
- $row = $this->mCurrentRow;
-
- switch ( $name ) {
- case 'af_id':
- return $lang->formatNum( intval( $value ) );
- case 'af_public_comments':
- return $this->getOutput()->parseInline( $value );
- case 'af_actions':
- $actions = explode( ',', $value );
- $displayActions = [];
- foreach ( $actions as $action ) {
- $displayActions[] = AbuseFilter::getActionDisplay( $action );
- }
- return htmlspecialchars( $lang->commaList( $displayActions ) );
- case 'af_enabled':
- $statuses = [];
- if ( $row->af_deleted ) {
- $statuses[] = $this->msg( 'abusefilter-deleted' )->parse();
- } elseif ( $row->af_enabled ) {
- $statuses[] = $this->msg( 'abusefilter-enabled' )->parse();
- } else {
- $statuses[] = $this->msg( 'abusefilter-disabled' )->parse();
- }
- if ( $row->af_global ) {
- $statuses[] = $this->msg( 'abusefilter-status-global' )->parse();
- }
-
- return $lang->commaList( $statuses );
- case 'af_hidden':
- $msg = $value ? 'abusefilter-hidden' : 'abusefilter-unhidden';
- return $this->msg( $msg, 'parseinline' )->parse();
- case 'af_hit_count':
- // If the rule is hidden, don't show it, even to priviledged local admins
- if ( $row->af_hidden ) {
- return '';
- }
- return $this->msg( 'abusefilter-hitcount' )->numParams( $value )->parse();
- case 'af_timestamp':
- $user = $row->af_user_text;
- return $this->msg(
- 'abusefilter-edit-lastmod-text',
- $lang->timeanddate( $value, true ),
- $user,
- $lang->date( $value, true ),
- $lang->time( $value, true ),
- $user
- )->parse();
- case 'af_group':
- // If this is global, local name probably doesn't exist, but try
- return AbuseFilter::nameGroup( $value );
- break;
- default:
- throw new MWException( "Unknown row type $name!" );
- }
- }
-}
diff --git a/AbuseFilter/includes/Views/AbuseFilterViewRevert.php b/AbuseFilter/includes/Views/AbuseFilterViewRevert.php
index 1e8c77b0..38a0d073 100644
--- a/AbuseFilter/includes/Views/AbuseFilterViewRevert.php
+++ b/AbuseFilter/includes/Views/AbuseFilterViewRevert.php
@@ -1,10 +1,14 @@
<?php
class AbuseFilterViewRevert extends AbuseFilterView {
- public $origPeriodStart, $origPeriodEnd, $mPeriodStart, $mPeriodEnd,
- $mReason;
+ public $origPeriodStart, $origPeriodEnd, $mPeriodStart, $mPeriodEnd;
+ public $mReason;
- function show() {
+ /**
+ * Shows the page
+ */
+ public function show() {
+ $lang = $this->getLanguage();
$filter = $this->mPage->mFilter;
$user = $this->getUser();
@@ -20,32 +24,50 @@ class AbuseFilterViewRevert extends AbuseFilterView {
return;
}
- $out->addWikiMsg( 'abusefilter-revert-intro', $filter );
- $out->setPageTitle( $this->msg( 'abusefilter-revert-title', $filter ) );
-
- // First, the search form.
- $searchFields = [];
- $searchFields['abusefilter-revert-filter'] =
- Xml::element( 'strong', null, $filter );
- $searchFields['abusefilter-revert-periodstart'] =
- Xml::input( 'wpPeriodStart', 45, $this->origPeriodStart );
- $searchFields['abusefilter-revert-periodend'] =
- Xml::input( 'wpPeriodEnd', 45, $this->origPeriodEnd );
- $searchForm = Xml::buildForm( $searchFields, 'abusefilter-revert-search' );
- $searchForm .= "\n" . Html::hidden( 'submit', 1 );
- $searchForm =
- Xml::tags(
- 'form',
- [
- 'action' => $this->getTitle( "revert/$filter" )->getLocalURL(),
- 'method' => 'post'
- ],
- $searchForm
+ $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 ) )
);
- $searchForm =
- Xml::fieldset( $this->msg( 'abusefilter-revert-search-legend' )->text(), $searchForm );
-
- $out->addHTML( $searchForm );
+ $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.
@@ -53,7 +75,6 @@ class AbuseFilterViewRevert extends AbuseFilterView {
// Look up all of them.
$results = $this->doLookup();
- $lang = $this->getLanguage();
$list = [];
foreach ( $results as $result ) {
@@ -62,12 +83,17 @@ class AbuseFilterViewRevert extends AbuseFilterView {
$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 ),
+ ->params(
+ $lang->timeanddate( $result['timestamp'], true )
+ )->rawParams(
+ Linker::userLink( $result['userid'], $result['user'] )
+ )->params(
+ $result['action']
+ )->rawParams(
+ $this->linkRenderer->makeLink( $result['title'] )
+ )->params(
+ $lang->commaList( $displayActions )
+ )->rawParams(
$this->linkRenderer->makeLink(
SpecialPage::getTitleFor( 'AbuseLog' ),
$this->msg( 'abusefilter-log-detailslink' )->text(),
@@ -81,30 +107,48 @@ class AbuseFilterViewRevert extends AbuseFilterView {
$out->addHTML( Xml::tags( 'ul', null, implode( "\n", $list ) ) );
// Add a button down the bottom.
- $confirmForm =
- Html::hidden( 'editToken', $user->getEditToken( "abusefilter-revert-$filter" ) ) .
- Html::hidden( 'title', $this->getTitle( "revert/$filter" )->getPrefixedDBkey() ) .
- Html::hidden( 'wpPeriodStart', $this->origPeriodStart ) .
- Html::hidden( 'wpPeriodEnd', $this->origPeriodEnd ) .
- Xml::inputLabel(
- $this->msg( 'abusefilter-revert-reasonfield' )->text(),
- 'wpReason', 'wpReason', 45
- ) .
- "\n" .
- Xml::submitButton( $this->msg( 'abusefilter-revert-confirm' )->text() );
- $confirmForm = Xml::tags(
- 'form',
- [
- 'action' => $this->getTitle( "revert/$filter" )->getLocalURL(),
- 'method' => 'post'
- ],
- $confirmForm
- );
- $out->addHTML( $confirmForm );
+ $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() {
+ /**
+ * @return array
+ */
+ public function doLookup() {
$periodStart = $this->mPeriodStart;
$periodEnd = $this->mPeriodEnd;
$filter = $this->mPage->mFilter;
@@ -114,14 +158,26 @@ class AbuseFilterViewRevert extends AbuseFilterView {
$dbr = wfGetDB( DB_REPLICA );
if ( $periodStart ) {
- $conds[] = 'afl_timestamp>' . $dbr->addQuotes( $dbr->timestamp( $periodStart ) );
+ $conds[] = 'afl_timestamp >= ' . $dbr->addQuotes( $dbr->timestamp( $periodStart ) );
}
if ( $periodEnd ) {
- $conds[] = 'afl_timestamp<' . $dbr->addQuotes( $dbr->timestamp( $periodEnd ) );
+ $conds[] = 'afl_timestamp <= ' . $dbr->addQuotes( $dbr->timestamp( $periodEnd ) );
}
- // Database query.
- $res = $dbr->select( 'abuse_filter_log', '*', $conds, __METHOD__ );
+ // All but afl_filter, afl_ip, afl_deleted, afl_patrolled_by, afl_rev_id and afl_log_id
+ $selectFields = [
+ 'afl_id',
+ 'afl_user',
+ 'afl_user_text',
+ 'afl_action',
+ 'afl_actions',
+ 'afl_var_dump',
+ 'afl_timestamp',
+ 'afl_namespace',
+ 'afl_title',
+ 'afl_wiki',
+ ];
+ $res = $dbr->select( 'abuse_filter_log', $selectFields, $conds, __METHOD__ );
$results = [];
foreach ( $res as $row ) {
@@ -150,7 +206,10 @@ class AbuseFilterViewRevert extends AbuseFilterView {
return $results;
}
- function loadParameters() {
+ /**
+ * Loads parameters from request
+ */
+ public function loadParameters() {
$request = $this->getRequest();
$this->origPeriodStart = $request->getText( 'wpPeriodStart' );
@@ -161,7 +220,10 @@ class AbuseFilterViewRevert extends AbuseFilterView {
$this->mReason = $request->getVal( 'wpReason' );
}
- function attemptRevert() {
+ /**
+ * @return bool
+ */
+ public function attemptRevert() {
$filter = $this->mPage->mFilter;
$token = $this->getRequest()->getVal( 'editToken' );
if ( !$this->getUser()->matchEditToken( $token, "abusefilter-revert-$filter" ) ) {
@@ -175,22 +237,25 @@ class AbuseFilterViewRevert extends AbuseFilterView {
$this->revertAction( $action, $result );
}
}
- $this->getOutput()->addWikiMsg(
- 'abusefilter-revert-success',
- $filter,
- $this->getLanguage()->formatNum( $filter )
+ $this->getOutput()->wrapWikiMsg(
+ '<p class="success">$1</p>',
+ [
+ 'abusefilter-revert-success',
+ $filter,
+ $this->getLanguage()->formatNum( $filter )
+ ]
);
return true;
}
/**
- * @param $action string
- * @param $result array
+ * @param string $action
+ * @param array $result
* @return bool
* @throws MWException
*/
- function revertAction( $action, $result ) {
+ public function revertAction( $action, $result ) {
switch ( $action ) {
case 'block':
$block = Block::newFromTarget( $result['user'] );
@@ -216,8 +281,7 @@ class AbuseFilterViewRevert extends AbuseFilterView {
return true;
case 'degroup':
// Pull the user's groups from the vars.
- $oldGroups = $result['vars']['USER_GROUPS'];
- $oldGroups = explode( ',', $oldGroups );
+ $oldGroups = $result['vars']->getVar( 'user_groups' )->toNative();
$oldGroups = array_diff(
$oldGroups,
array_intersect( $oldGroups, User::getImplicitGroups() )
@@ -245,15 +309,21 @@ class AbuseFilterViewRevert extends AbuseFilterView {
$dbw->insert( 'user_groups', $rows, __METHOD__, [ 'IGNORE' ] );
$user->invalidateCache();
- $log = new LogPage( 'rights' );
- $log->addEntry( 'rights', $user->getUserPage(),
+ $logEntry = new ManualLogEntry( 'rights', 'rights' );
+ $logEntry->setTarget( $user->getUserPage() );
+ $logEntry->setPerformer( $this->getUser() );
+ $logEntry->setComment(
$this->msg(
'abusefilter-revert-reason',
$this->mPage->mFilter,
$this->mReason
- )->inContentLanguage()->text(),
- [ implode( ',', $currentGroups ), implode( ',', $newGroups ) ]
+ )->inContentLanguage()->text()
);
+ $logEntry->setParameters( [
+ '4::oldgroups' => $currentGroups,
+ '5::newgroups' => $newGroups
+ ] );
+ $logEntry->publish( $logEntry->insert() );
return true;
}
diff --git a/AbuseFilter/includes/Views/AbuseFilterViewTestBatch.php b/AbuseFilter/includes/Views/AbuseFilterViewTestBatch.php
index abc33fc6..168933ec 100644
--- a/AbuseFilter/includes/Views/AbuseFilterViewTestBatch.php
+++ b/AbuseFilter/includes/Views/AbuseFilterViewTestBatch.php
@@ -4,16 +4,19 @@ class AbuseFilterViewTestBatch extends AbuseFilterView {
// Hard-coded for now.
protected static $mChangeLimit = 100;
- public $mShowNegative, $mTestPeriodStart, $mTestPeriodEnd, $mTestPage,
- $mTestUser;
+ public $mShowNegative, $mTestPeriodStart, $mTestPeriodEnd, $mTestPage;
+ public $mTestUser, $mExcludeBots, $mTestAction;
- function show() {
+ /**
+ * Shows the page
+ */
+ public function show() {
$out = $this->getOutput();
AbuseFilter::disableConditionLimit();
- if ( !$this->getUser()->isAllowed( 'abusefilter-modify' ) ) {
- $out->addWikiMsg( 'abusefilter-mustbeeditor' );
+ if ( !$this->canViewPrivate() ) {
+ $out->addWikiMsg( 'abusefilter-mustviewprivateoredit' );
return;
}
@@ -21,56 +24,95 @@ class AbuseFilterViewTestBatch extends AbuseFilterView {
$out->setPageTitle( $this->msg( 'abusefilter-test' ) );
$out->addWikiMsg( 'abusefilter-test-intro', self::$mChangeLimit );
+ $out->enableOOUI();
$output = '';
- $output .= AbuseFilter::buildEditBox( $this->mFilter, 'wpTestFilter' ) . "\n";
$output .=
- Xml::inputLabel(
- $this->msg( 'abusefilter-test-load-filter' )->text(),
- 'wpInsertFilter',
- 'mw-abusefilter-load-filter',
- 10,
- ''
- ) .
- '&#160;' .
- Xml::element(
- 'input',
- [
- 'type' => 'button',
- 'value' => $this->msg( 'abusefilter-test-load' )->text(),
- 'id' => 'mw-abusefilter-load'
- ]
- );
+ $this->buildEditBox(
+ $this->mFilter,
+ 'wpTestFilter',
+ true,
+ true,
+ false
+ ) . "\n";
+
+ $output .= AbuseFilter::buildFilterLoader();
$output = Xml::tags( 'div', [ 'id' => 'mw-abusefilter-test-editor' ], $output );
- $output .= Xml::tags( 'p', null, Xml::checkLabel(
- $this->msg( 'abusefilter-test-shownegative' )->text(),
- 'wpShowNegative', 'wpShowNegative', $this->mShowNegative )
- );
-
- // Selectory stuff
- $selectFields = [];
- $selectFields['abusefilter-test-user'] = Xml::input( 'wpTestUser', 45, $this->mTestUser );
- $selectFields['abusefilter-test-period-start'] =
- Xml::input( 'wpTestPeriodStart', 45, $this->mTestPeriodStart );
- $selectFields['abusefilter-test-period-end'] =
- Xml::input( 'wpTestPeriodEnd', 45, $this->mTestPeriodEnd );
- $selectFields['abusefilter-test-page'] =
- Xml::input( 'wpTestPage', 45, $this->mTestPage );
-
- $output .= Xml::buildForm( $selectFields, 'abusefilter-test-submit' );
-
- $output .= Html::hidden( 'title', $this->getTitle( 'test' )->getPrefixedDBkey() );
- $output = Xml::tags( 'form',
- [
- 'action' => $this->getTitle( 'test' )->getLocalURL(),
- 'method' => 'post'
- ],
- $output
- );
-
- $output = Xml::fieldset( $this->msg( 'abusefilter-test-legend' )->text(), $output );
-
+ $RCMaxAge = $this->getConfig()->get( 'RCMaxAge' );
+ $min = wfTimestamp( TS_ISO_8601, time() - $RCMaxAge );
+ $max = wfTimestampNow();
+
+ // Search form
+ $formFields = [];
+ $formFields['wpTestAction'] = [
+ 'name' => 'wpTestAction',
+ 'type' => 'select',
+ 'label-message' => 'abusefilter-test-action',
+ 'options' => [
+ $this->msg( 'abusefilter-test-search-type-all' )->text() => 0,
+ $this->msg( 'abusefilter-test-search-type-edit' )->text() => 'edit',
+ $this->msg( 'abusefilter-test-search-type-move' )->text() => 'move',
+ $this->msg( 'abusefilter-test-search-type-delete' )->text() => 'delete',
+ $this->msg( 'abusefilter-test-search-type-createaccount' )->text() => 'createaccount'
+ // @ToDo: add 'upload' once T170249 is resolved
+ ]
+ ];
+ $formFields['wpTestUser'] = [
+ 'name' => 'wpTestUser',
+ 'type' => 'user',
+ 'ipallowed' => true,
+ 'label-message' => 'abusefilter-test-user',
+ 'default' => $this->mTestUser
+ ];
+ $formFields['wpExcludeBots'] = [
+ 'name' => 'wpExcludeBots',
+ 'type' => 'check',
+ 'label-message' => 'abusefilter-test-nobots',
+ 'default' => $this->mExcludeBots
+ ];
+ $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,
+ 'required' => false
+ ];
+ $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()
+ ->getHTML( true );
+
+ $output = Xml::fieldset( $this->msg( 'abusefilter-test-legend' )->text(), $output . $htmlForm );
$out->addHTML( $output );
if ( $this->getRequest()->wasPosted() ) {
@@ -79,9 +121,9 @@ class AbuseFilterViewTestBatch extends AbuseFilterView {
}
/**
- * @fixme this is similar to AbuseFilterExaminePager::getQueryInfo
+ * Loads the revisions and checks the given syntax against them
*/
- function doTest() {
+ public function doTest() {
// Quick syntax check.
$out = $this->getOutput();
$result = AbuseFilter::checkSyntax( $this->mFilter );
@@ -92,7 +134,12 @@ class AbuseFilterViewTestBatch extends AbuseFilterView {
$dbr = wfGetDB( DB_REPLICA );
$conds = [];
- $conds['rc_user_text'] = $this->mTestUser;
+
+ if ( (string)$this->mTestUser !== '' ) {
+ $conds[] = ActorMigration::newMigration()->getWhere(
+ $dbr, 'rc_user', User::newFromName( $this->mTestUser, false )
+ )['conds'];
+ }
if ( $this->mTestPeriodStart ) {
$conds[] = 'rc_timestamp >= ' .
@@ -113,18 +160,28 @@ class AbuseFilterViewTestBatch extends AbuseFilterView {
}
}
- $conds[] = $this->buildTestConditions( $dbr );
+ $action = $this->mTestAction != '0' ? $this->mTestAction : false;
+ $conds[] = $this->buildTestConditions( $dbr, $action );
+
+ $conds = array_filter( $conds );
+
+ // To be added after filtering, otherwise it gets stripped
+ if ( $this->mExcludeBots ) {
+ $conds['rc_bot'] = 0;
+ }
// Get our ChangesList
- $changesList = new AbuseFilterChangesList( $this->getSkin() );
+ $changesList = new AbuseFilterChangesList( $this->getSkin(), $this->mFilter );
$output = $changesList->beginRecentChangesList();
+ $rcQuery = RecentChange::getQueryInfo();
$res = $dbr->select(
- 'recentchanges',
- RecentChange::selectFields(),
- array_filter( $conds ),
+ $rcQuery['tables'],
+ $rcQuery['fields'],
+ $conds,
__METHOD__,
- [ 'LIMIT' => self::$mChangeLimit, 'ORDER BY' => 'rc_timestamp desc' ]
+ [ 'LIMIT' => self::$mChangeLimit, 'ORDER BY' => 'rc_timestamp desc' ],
+ $rcQuery['joins']
);
$counter = 1;
@@ -141,7 +198,7 @@ class AbuseFilterViewTestBatch extends AbuseFilterView {
if ( $result || $this->mShowNegative ) {
// Stash result in RC item
$rc = RecentChange::newFromRow( $row );
- $rc->examineParams['testfilter'] = $this->mFilter;
+ /** @suppress PhanUndeclaredProperty for $rc->filterResult, which isn't a big deal */
$rc->filterResult = $result;
$rc->counter = $counter++;
$output .= $changesList->recentChangesLine( $rc, false );
@@ -153,7 +210,10 @@ class AbuseFilterViewTestBatch extends AbuseFilterView {
$out->addHTML( $output );
}
- function loadParameters() {
+ /**
+ * Loads parameters from request
+ */
+ public function loadParameters() {
$request = $this->getRequest();
$this->mFilter = $request->getText( 'wpTestFilter' );
@@ -162,6 +222,8 @@ class AbuseFilterViewTestBatch extends AbuseFilterView {
$this->mTestPeriodEnd = $request->getText( 'wpTestPeriodEnd' );
$this->mTestPeriodStart = $request->getText( 'wpTestPeriodStart' );
$this->mTestPage = $request->getText( 'wpTestPage' );
+ $this->mExcludeBots = $request->getBool( 'wpExcludeBots' );
+ $this->mTestAction = $request->getText( 'wpTestAction' );
if ( !$this->mFilter
&& count( $this->mParams ) > 1
@@ -176,15 +238,7 @@ class AbuseFilterViewTestBatch extends AbuseFilterView {
}
// Normalise username
- $userTitle = Title::newFromText( $testUsername );
-
- if ( $userTitle && $userTitle->getNamespace() == NS_USER ) {
- $this->mTestUser = $userTitle->getText(); // Allow User:Blah syntax.
- } elseif ( $userTitle ) {
- // Not sure of the value of prefixedText over text, but no need to munge unnecessarily.
- $this->mTestUser = $userTitle->getPrefixedText();
- } else {
- $this->mTestUser = null; // No user specified.
- }
+ $userTitle = Title::newFromText( $testUsername, NS_USER );
+ $this->mTestUser = $userTitle ? $userTitle->getText() : null;
}
}
diff --git a/AbuseFilter/includes/Views/AbuseFilterViewTools.php b/AbuseFilter/includes/Views/AbuseFilterViewTools.php
index c8625617..44ee0eb4 100644
--- a/AbuseFilter/includes/Views/AbuseFilterViewTools.php
+++ b/AbuseFilter/includes/Views/AbuseFilterViewTools.php
@@ -1,12 +1,16 @@
<?php
class AbuseFilterViewTools extends AbuseFilterView {
- function show() {
+ /**
+ * Shows the page
+ */
+ public function show() {
$out = $this->getOutput();
- $user = $this->getUser();
+ $out->enableOOUI();
+ $request = $this->getRequest();
- if ( !$user->isAllowed( 'abusefilter-modify' ) ) {
- $out->addWikiMsg( 'abusefilter-mustbeeditor' );
+ if ( !$this->canViewPrivate() ) {
+ $out->addWikiMsg( 'abusefilter-mustviewprivateoredit' );
return;
}
@@ -15,16 +19,21 @@ class AbuseFilterViewTools extends AbuseFilterView {
// Expression evaluator
$eval = '';
- $eval .= AbuseFilter::buildEditBox( '', 'wpTestExpr' );
-
- $eval .= Xml::tags( 'p', null,
- Xml::element( 'input',
- [
- 'type' => 'button',
- 'id' => 'mw-abusefilter-submitexpr',
- 'value' => $this->msg( 'abusefilter-tools-submitexpr' )->text() ]
- )
+ $eval .= $this->buildEditBox(
+ $request->getText( 'wpTestExpr' ),
+ 'wpTestExpr',
+ true,
+ false,
+ false
);
+
+ $eval .=
+ Xml::tags( 'p', null,
+ new OOUI\ButtonInputWidget( [
+ 'label' => $this->msg( 'abusefilter-tools-submitexpr' )->text(),
+ 'id' => 'mw-abusefilter-submitexpr'
+ ] )
+ );
$eval .= Xml::element( 'p', [ 'id' => 'mw-abusefilter-expr-result' ], ' ' );
$eval = Xml::fieldset( $this->msg( 'abusefilter-tools-expr' )->text(), $eval );
@@ -33,23 +42,21 @@ class AbuseFilterViewTools extends AbuseFilterView {
$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 );
+ $formDescriptor = [
+ 'RestoreAutoconfirmed' => [
+ 'label-message' => 'abusefilter-tools-reautoconfirm-user',
+ 'type' => 'user',
+ 'name' => 'wpReAutoconfirmUser',
+ 'id' => 'reautoconfirm-user',
+ 'infusable' => true
+ ],
+ ];
+ $htmlForm = HTMLForm::factory( 'ooui', $formDescriptor, $this->getContext() );
+ $htmlForm->setWrapperLegendMsg( 'abusefilter-tools-reautoconfirm' )
+ ->setSubmitTextMsg( 'abusefilter-tools-reautoconfirm-submit' )
+ ->setSubmitName( 'wpReautoconfirmSubmit' )
+ ->setSubmitId( 'mw-abusefilter-reautoconfirmsubmit' )
+ ->prepareForm()
+ ->displayForm( false );
}
}
diff --git a/AbuseFilter/includes/api/ApiAbuseFilterCheckMatch.php b/AbuseFilter/includes/api/ApiAbuseFilterCheckMatch.php
index bcea726e..9d4fd8c3 100644
--- a/AbuseFilter/includes/api/ApiAbuseFilterCheckMatch.php
+++ b/AbuseFilter/includes/api/ApiAbuseFilterCheckMatch.php
@@ -1,12 +1,15 @@
<?php
class ApiAbuseFilterCheckMatch extends ApiBase {
+ /**
+ * @see ApiBase::execute
+ */
public function execute() {
$params = $this->extractRequestParams();
$this->requireOnlyOneParameter( $params, 'vars', 'rcid', 'logid' );
// "Anti-DoS"
- if ( !$this->getUser()->isAllowed( 'abusefilter-modify' ) ) {
+ if ( !$this->getUser()->isAllowedAny( 'abusefilter-modify', 'abusefilter-view-private' ) ) {
$this->dieWithError( 'apierror-abusefilter-canttest', 'permissiondenied' );
}
@@ -19,11 +22,14 @@ class ApiAbuseFilterCheckMatch extends ApiBase {
}
} elseif ( $params['rcid'] ) {
$dbr = wfGetDB( DB_REPLICA );
+ $rcQuery = RecentChange::getQueryInfo();
$row = $dbr->selectRow(
- 'recentchanges',
- RecentChange::selectFields(),
+ $rcQuery['tables'],
+ $rcQuery['fields'],
[ 'rc_id' => $params['rcid'] ],
- __METHOD__
+ __METHOD__,
+ [],
+ $rcQuery['joins']
);
if ( !$row ) {
@@ -35,7 +41,7 @@ class ApiAbuseFilterCheckMatch extends ApiBase {
$dbr = wfGetDB( DB_REPLICA );
$row = $dbr->selectRow(
'abuse_filter_log',
- '*',
+ 'afl_var_dump',
[ 'afl_id' => $params['logid'] ],
__METHOD__
);
@@ -63,6 +69,10 @@ class ApiAbuseFilterCheckMatch extends ApiBase {
);
}
+ /**
+ * @see ApiBase::getAllowedParams
+ * @return array
+ */
public function getAllowedParams() {
return [
'filter' => [
@@ -80,6 +90,7 @@ class ApiAbuseFilterCheckMatch extends ApiBase {
/**
* @see ApiBase::getExamplesMessages()
+ * @return array
*/
protected function getExamplesMessages() {
return [
diff --git a/AbuseFilter/includes/api/ApiAbuseFilterCheckSyntax.php b/AbuseFilter/includes/api/ApiAbuseFilterCheckSyntax.php
index b0ac3f73..213fb904 100644
--- a/AbuseFilter/includes/api/ApiAbuseFilterCheckSyntax.php
+++ b/AbuseFilter/includes/api/ApiAbuseFilterCheckSyntax.php
@@ -2,9 +2,12 @@
class ApiAbuseFilterCheckSyntax extends ApiBase {
+ /**
+ * @see ApiBase::execute
+ */
public function execute() {
// "Anti-DoS"
- if ( !$this->getUser()->isAllowed( 'abusefilter-modify' ) ) {
+ if ( !$this->getUser()->isAllowedAny( 'abusefilter-modify', 'abusefilter-view-private' ) ) {
$this->dieWithError( 'apierror-abusefilter-cantcheck', 'permissiondenied' );
}
@@ -26,6 +29,10 @@ class ApiAbuseFilterCheckSyntax extends ApiBase {
$this->getResult()->addValue( null, $this->getModuleName(), $r );
}
+ /**
+ * @see ApiBase::getAllowedParams
+ * @return array
+ */
public function getAllowedParams() {
return [
'filter' => [
@@ -36,6 +43,7 @@ class ApiAbuseFilterCheckSyntax extends ApiBase {
/**
* @see ApiBase::getExamplesMessages()
+ * @return array
*/
protected function getExamplesMessages() {
return [
diff --git a/AbuseFilter/includes/api/ApiAbuseFilterEvalExpression.php b/AbuseFilter/includes/api/ApiAbuseFilterEvalExpression.php
index 74fb0852..18701670 100644
--- a/AbuseFilter/includes/api/ApiAbuseFilterEvalExpression.php
+++ b/AbuseFilter/includes/api/ApiAbuseFilterEvalExpression.php
@@ -1,6 +1,9 @@
<?php
class ApiAbuseFilterEvalExpression extends ApiBase {
+ /**
+ * @see ApiBase::execute()
+ */
public function execute() {
$params = $this->extractRequestParams();
@@ -9,6 +12,10 @@ class ApiAbuseFilterEvalExpression extends ApiBase {
$this->getResult()->addValue( null, $this->getModuleName(), [ 'result' => $result ] );
}
+ /**
+ * @see ApiBase::getAllowedParams()
+ * @return array
+ */
public function getAllowedParams() {
return [
'expression' => [
@@ -19,6 +26,7 @@ class ApiAbuseFilterEvalExpression extends ApiBase {
/**
* @see ApiBase::getExamplesMessages()
+ * @return array
*/
protected function getExamplesMessages() {
return [
diff --git a/AbuseFilter/includes/api/ApiAbuseFilterUnblockAutopromote.php b/AbuseFilter/includes/api/ApiAbuseFilterUnblockAutopromote.php
index 5ceb17f8..195e72d3 100644
--- a/AbuseFilter/includes/api/ApiAbuseFilterUnblockAutopromote.php
+++ b/AbuseFilter/includes/api/ApiAbuseFilterUnblockAutopromote.php
@@ -1,6 +1,9 @@
<?php
class ApiAbuseFilterUnblockAutopromote extends ApiBase {
+ /**
+ * @see ApiBase::execute()
+ */
public function execute() {
$this->checkUserRightsAny( 'abusefilter-modify' );
@@ -10,7 +13,7 @@ class ApiAbuseFilterUnblockAutopromote extends ApiBase {
if ( $user === false ) {
$encParamName = $this->encodeParamName( 'user' );
$this->dieWithError(
- [ 'apierror-baduser', $encParamName, wfEscapeWikiText( $param['user'] ) ],
+ [ 'apierror-baduser', $encParamName, wfEscapeWikiText( $params['user'] ) ],
"baduser_{$encParamName}"
);
}
@@ -27,14 +30,26 @@ class ApiAbuseFilterUnblockAutopromote extends ApiBase {
$this->getResult()->addValue( null, $this->getModuleName(), $res );
}
+ /**
+ * @see ApiBase::mustBePosted()
+ * @return bool
+ */
public function mustBePosted() {
return true;
}
+ /**
+ * @see ApiBase::isWriteMode()
+ * @return bool
+ */
public function isWriteMode() {
return true;
}
+ /**
+ * @see ApiBase::getAllowedParams()
+ * @return array
+ */
public function getAllowedParams() {
return [
'user' => [
@@ -45,12 +60,17 @@ class ApiAbuseFilterUnblockAutopromote extends ApiBase {
];
}
+ /**
+ * @see ApiBase::needsToken()
+ * @return string
+ */
public function needsToken() {
return 'csrf';
}
/**
* @see ApiBase::getExamplesMessages()
+ * @return array
*/
protected function getExamplesMessages() {
return [
diff --git a/AbuseFilter/includes/api/ApiQueryAbuseFilters.php b/AbuseFilter/includes/api/ApiQueryAbuseFilters.php
index 79808125..cafe20d1 100644
--- a/AbuseFilter/includes/api/ApiQueryAbuseFilters.php
+++ b/AbuseFilter/includes/api/ApiQueryAbuseFilters.php
@@ -30,10 +30,17 @@
* @ingroup Extensions
*/
class ApiQueryAbuseFilters extends ApiQueryBase {
+ /**
+ * @param ApiQuery $query
+ * @param string $moduleName
+ */
public function __construct( $query, $moduleName ) {
parent::__construct( $query, $moduleName, 'abf' );
}
+ /**
+ * @see ApiQueryBase::execute()
+ */
public function execute() {
$user = $this->getUser();
$this->checkUserRightsAny( 'abusefilter-view' );
@@ -150,6 +157,10 @@ class ApiQueryAbuseFilters extends ApiQueryBase {
$result->addIndexedTagName( [ 'query', $this->getModuleName() ], 'filter' );
}
+ /**
+ * @see ApiQueryBase::getAllowedParams()
+ * @return array
+ */
public function getAllowedParams() {
return [
'startid' => [
@@ -205,6 +216,7 @@ class ApiQueryAbuseFilters extends ApiQueryBase {
/**
* @see ApiBase::getExamplesMessages()
+ * @return array
*/
protected function getExamplesMessages() {
return [
diff --git a/AbuseFilter/includes/api/ApiQueryAbuseLog.php b/AbuseFilter/includes/api/ApiQueryAbuseLog.php
index b559b0c5..bf085318 100644
--- a/AbuseFilter/includes/api/ApiQueryAbuseLog.php
+++ b/AbuseFilter/includes/api/ApiQueryAbuseLog.php
@@ -30,10 +30,17 @@
* @ingroup Extensions
*/
class ApiQueryAbuseLog extends ApiQueryBase {
+ /**
+ * @param ApiQuery $query
+ * @param string $moduleName
+ */
public function __construct( $query, $moduleName ) {
parent::__construct( $query, $moduleName, 'afl' );
}
+ /**
+ * @see ApiQueryBase::execute()
+ */
public function execute() {
$user = $this->getUser();
$errors = $this->getTitle()->getUserPermissionsErrors(
@@ -49,7 +56,6 @@ class ApiQueryAbuseLog extends ApiQueryBase {
$fld_ids = isset( $prop['ids'] );
$fld_filter = isset( $prop['filter'] );
$fld_user = isset( $prop['user'] );
- $fld_ip = isset( $prop['ip'] );
$fld_title = isset( $prop['title'] );
$fld_action = isset( $prop['action'] );
$fld_details = isset( $prop['details'] );
@@ -57,10 +63,9 @@ class ApiQueryAbuseLog extends ApiQueryBase {
$fld_timestamp = isset( $prop['timestamp'] );
$fld_hidden = isset( $prop['hidden'] );
$fld_revid = isset( $prop['revid'] );
+ $isCentral = $this->getConfig()->get( 'AbuseFilterIsCentral' );
+ $fld_wiki = $isCentral && isset( $prop['wiki'] );
- if ( $fld_ip ) {
- $this->checkUserRightsAny( 'abusefilter-private' );
- }
if ( $fld_details ) {
$this->checkUserRightsAny( 'abusefilter-log-detail' );
}
@@ -90,11 +95,11 @@ class ApiQueryAbuseLog extends ApiQueryBase {
$this->addFields( 'afl_filter' );
$this->addFieldsIf( 'afl_id', $fld_ids );
$this->addFieldsIf( 'afl_user_text', $fld_user );
- $this->addFieldsIf( 'afl_ip', $fld_ip );
$this->addFieldsIf( [ 'afl_namespace', 'afl_title' ], $fld_title );
$this->addFieldsIf( 'afl_action', $fld_action );
$this->addFieldsIf( 'afl_var_dump', $fld_details );
$this->addFieldsIf( 'afl_actions', $fld_result );
+ $this->addFieldsIf( 'afl_wiki', $fld_wiki );
if ( $fld_filter ) {
$this->addTables( 'abuse_filter' );
@@ -105,6 +110,8 @@ class ApiQueryAbuseLog extends ApiQueryBase {
$this->addOption( 'LIMIT', $params['limit'] + 1 );
+ $this->addWhereIf( [ 'afl_id' => $params['logid'] ], isset( $params['logid'] ) );
+
$this->addWhereRange( 'afl_timestamp', $params['dir'], $params['start'], $params['end'] );
$db = $this->getDB();
@@ -135,7 +142,11 @@ class ApiQueryAbuseLog extends ApiQueryBase {
}
$this->addWhereIf( [ 'afl_filter' => $params['filter'] ], isset( $params['filter'] ) );
- $this->addWhereIf( $notDeletedCond, !SpecialAbuseLog::canSeeHidden( $user ) );
+ $this->addWhereIf( $notDeletedCond, !SpecialAbuseLog::canSeeHidden() );
+ if ( isset( $params['wiki'] ) ) {
+ // 'wiki' won't be set if $wgAbuseFilterIsCentral = false
+ $this->addWhereIf( [ 'afl_wiki' => $params['wiki'] ], $isCentral );
+ }
$title = $params['title'];
if ( !is_null( $title ) ) {
@@ -170,10 +181,7 @@ class ApiQueryAbuseLog extends ApiQueryBase {
$entry = [];
if ( $fld_ids ) {
$entry['id'] = intval( $row->afl_id );
- $entry['filter_id'] = '';
- if ( $canSeeDetails ) {
- $entry['filter_id'] = $row->afl_filter;
- }
+ $entry['filter_id'] = $canSeeDetails ? $row->afl_filter : '';
}
if ( $fld_filter ) {
$globalIndex = AbuseFilter::decodeGlobalName( $row->afl_filter );
@@ -186,8 +194,8 @@ class ApiQueryAbuseLog extends ApiQueryBase {
if ( $fld_user ) {
$entry['user'] = $row->afl_user_text;
}
- if ( $fld_ip ) {
- $entry['ip'] = $row->afl_ip;
+ if ( $fld_wiki ) {
+ $entry['wiki'] = $row->afl_wiki;
}
if ( $fld_title ) {
$title = Title::makeTitle( $row->afl_namespace, $row->afl_title );
@@ -200,10 +208,7 @@ class ApiQueryAbuseLog extends ApiQueryBase {
$entry['result'] = $row->afl_actions;
}
if ( $fld_revid && !is_null( $row->afl_rev_id ) ) {
- $entry['revid'] = '';
- if ( $canSeeDetails ) {
- $entry['revid'] = $row->afl_rev_id;
- }
+ $entry['revid'] = $canSeeDetails ? $row->afl_rev_id : '';
}
if ( $fld_timestamp ) {
$ts = new MWTimestamp( $row->afl_timestamp );
@@ -237,8 +242,15 @@ class ApiQueryAbuseLog extends ApiQueryBase {
$result->addIndexedTagName( [ 'query', $this->getModuleName() ], 'item' );
}
+ /**
+ * @see ApiQueryBase::getAllowedParams()
+ * @return array
+ */
public function getAllowedParams() {
- return [
+ $params = [
+ 'logid' => [
+ ApiBase::PARAM_TYPE => 'integer'
+ ],
'start' => [
ApiBase::PARAM_TYPE => 'timestamp'
],
@@ -256,6 +268,7 @@ class ApiQueryAbuseLog extends ApiQueryBase {
'user' => null,
'title' => null,
'filter' => [
+ ApiBase::PARAM_TYPE => 'string',
ApiBase::PARAM_ISMULTI => true
],
'limit' => [
@@ -271,7 +284,6 @@ class ApiQueryAbuseLog extends ApiQueryBase {
'ids',
'filter',
'user',
- 'ip',
'title',
'action',
'details',
@@ -283,10 +295,19 @@ class ApiQueryAbuseLog extends ApiQueryBase {
ApiBase::PARAM_ISMULTI => true
]
];
+ if ( $this->getConfig()->get( 'AbuseFilterIsCentral' ) ) {
+ $params['wiki'] = [
+ ApiBase::PARAM_TYPE => 'string',
+ ];
+ $params['prop'][ApiBase::PARAM_DFLT] .= '|wiki';
+ $params['prop'][ApiBase::PARAM_TYPE][] = 'wiki';
+ }
+ return $params;
}
/**
* @see ApiBase::getExamplesMessages()
+ * @return array
*/
protected function getExamplesMessages() {
return [
diff --git a/AbuseFilter/includes/pagers/AbuseFilterExaminePager.php b/AbuseFilter/includes/pagers/AbuseFilterExaminePager.php
new file mode 100644
index 00000000..02b13755
--- /dev/null
+++ b/AbuseFilter/includes/pagers/AbuseFilterExaminePager.php
@@ -0,0 +1,82 @@
+<?php
+
+class AbuseFilterExaminePager extends ReverseChronologicalPager {
+ public $mChangesList, $mPage;
+
+ /**
+ * @param AbuseFilterViewExamine $page
+ * @param AbuseFilterChangesList $changesList
+ */
+ public function __construct( $page, $changesList ) {
+ parent::__construct();
+ $this->mChangesList = $changesList;
+ $this->mPage = $page;
+ }
+
+ /**
+ * @return array
+ */
+ public function getQueryInfo() {
+ $dbr = wfGetDB( DB_REPLICA );
+ $conds = [];
+
+ if ( (string)$this->mPage->mSearchUser !== '' ) {
+ $conds[] = ActorMigration::newMigration()->getWhere(
+ $dbr, 'rc_user', User::newFromName( $this->mPage->mSearchUser, false )
+ )['conds'];
+ }
+
+ $startTS = strtotime( $this->mPage->mSearchPeriodStart );
+ if ( $startTS ) {
+ $conds[] = 'rc_timestamp>=' . $dbr->addQuotes( $dbr->timestamp( $startTS ) );
+ }
+ $endTS = strtotime( $this->mPage->mSearchPeriodEnd );
+ if ( $endTS ) {
+ $conds[] = 'rc_timestamp<=' . $dbr->addQuotes( $dbr->timestamp( $endTS ) );
+ }
+
+ $conds[] = $this->mPage->buildTestConditions( $dbr );
+
+ $rcQuery = RecentChange::getQueryInfo();
+ $info = [
+ 'tables' => $rcQuery['tables'],
+ 'fields' => $rcQuery['fields'],
+ 'conds' => array_filter( $conds ),
+ 'options' => [ 'ORDER BY' => 'rc_timestamp DESC' ],
+ 'join_conds' => $rcQuery['joins'],
+ ];
+
+ return $info;
+ }
+
+ /**
+ * @param stdClass $row
+ * @return string
+ */
+ public function formatRow( $row ) {
+ $rc = RecentChange::newFromRow( $row );
+ $rc->counter = $this->mPage->mCounter++;
+ return $this->mChangesList->recentChangesLine( $rc, false );
+ }
+
+ /**
+ * @return string
+ */
+ public function getIndexField() {
+ return 'rc_id';
+ }
+
+ /**
+ * @return Title
+ */
+ public function getTitle() {
+ return $this->mPage->getTitle( 'examine' );
+ }
+
+ /**
+ * @return string
+ */
+ public function getEmptyBody() {
+ return $this->msg( 'abusefilter-examine-noresults' )->parseAsBlock();
+ }
+}
diff --git a/AbuseFilter/includes/pagers/AbuseFilterHistoryPager.php b/AbuseFilter/includes/pagers/AbuseFilterHistoryPager.php
new file mode 100644
index 00000000..265c97f1
--- /dev/null
+++ b/AbuseFilter/includes/pagers/AbuseFilterHistoryPager.php
@@ -0,0 +1,229 @@
+<?php
+
+use MediaWiki\Linker\LinkRenderer;
+
+class AbuseFilterHistoryPager extends TablePager {
+ public $mFilter, $mPage, $mUser;
+
+ protected $linkRenderer;
+ /**
+ * @param string $filter
+ * @param AbuseFilterViewHistory $page
+ * @param string $user User name
+ * @param LinkRenderer $linkRenderer
+ */
+ public function __construct( $filter, $page, $user, $linkRenderer ) {
+ $this->mFilter = $filter;
+ $this->mPage = $page;
+ $this->mUser = $user;
+ $this->mDefaultDirection = true;
+ $this->linkRenderer = $linkRenderer;
+ parent::__construct( $this->mPage->getContext() );
+ }
+
+ /**
+ * @see Pager::getFieldNames()
+ * @return array
+ */
+ public function getFieldNames() {
+ static $headers = null;
+
+ if ( !empty( $headers ) ) {
+ return $headers;
+ }
+
+ $headers = [
+ 'afh_timestamp' => 'abusefilter-history-timestamp',
+ 'afh_user_text' => 'abusefilter-history-user',
+ 'afh_public_comments' => 'abusefilter-history-public',
+ 'afh_flags' => 'abusefilter-history-flags',
+ 'afh_actions' => 'abusefilter-history-actions',
+ 'afh_id' => 'abusefilter-history-diff',
+ ];
+
+ if ( !$this->mFilter ) {
+ // awful hack
+ $headers = [ 'afh_filter' => 'abusefilter-history-filterid' ] + $headers;
+ unset( $headers['afh_comments'] );
+ }
+
+ foreach ( $headers as &$msg ) {
+ $msg = $this->msg( $msg )->text();
+ }
+
+ return $headers;
+ }
+
+ /**
+ * @param string $name
+ * @param string $value
+ * @return string
+ */
+ public function formatValue( $name, $value ) {
+ $lang = $this->getLanguage();
+
+ $row = $this->mCurrentRow;
+
+ switch ( $name ) {
+ case 'afh_filter':
+ $formatted = $this->linkRenderer->makeLink(
+ SpecialPage::getTitleFor( 'AbuseFilter', intval( $row->afh_filter ) ),
+ $lang->formatNum( $row->afh_filter )
+ );
+ break;
+ case 'afh_timestamp':
+ $title = SpecialPage::getTitleFor( 'AbuseFilter',
+ 'history/' . $row->afh_filter . '/item/' . $row->afh_id );
+ $formatted = $this->linkRenderer->makeLink(
+ $title,
+ $lang->timeanddate( $row->afh_timestamp, true )
+ );
+ break;
+ case 'afh_user_text':
+ $formatted =
+ Linker::userLink( $row->afh_user, $row->afh_user_text ) . ' ' .
+ Linker::userToolLinks( $row->afh_user, $row->afh_user_text );
+ break;
+ case 'afh_public_comments':
+ $formatted = htmlspecialchars( $value, ENT_QUOTES, 'UTF-8', false );
+ break;
+ case 'afh_flags':
+ $formatted = AbuseFilter::formatFlags( $value );
+ break;
+ case 'afh_actions':
+ $actions = unserialize( $value );
+
+ $display_actions = '';
+
+ foreach ( $actions as $action => $parameters ) {
+ $displayAction = AbuseFilter::formatAction( $action, $parameters );
+ $display_actions .= Xml::tags( 'li', null, $displayAction );
+ }
+ $display_actions = Xml::tags( 'ul', null, $display_actions );
+
+ $formatted = $display_actions;
+ break;
+ case 'afh_id':
+ $formatted = '';
+ if ( AbuseFilter::getFirstFilterChange( $row->afh_filter ) != $value ) {
+ // Set a link to a diff with the previous version if this isn't the first edit to the filter
+ $title = $this->mPage->getTitle(
+ 'history/' . $row->afh_filter . "/diff/prev/$value" );
+ $formatted = $this->linkRenderer->makeLink(
+ $title,
+ new HtmlArmor( $this->msg( 'abusefilter-history-diff' )->parse() )
+ );
+ }
+ break;
+ default:
+ $formatted = "Unable to format $name";
+ break;
+ }
+
+ $mappings = array_flip( AbuseFilter::$history_mappings ) +
+ [ 'afh_actions' => 'actions', 'afh_id' => 'id' ];
+ $changed = explode( ',', $row->afh_changed_fields );
+
+ $fieldChanged = false;
+ if ( $name == 'afh_flags' ) {
+ // This is a bit freaky, but it works.
+ // Basically, returns true if any of those filters are in the $changed array.
+ $filters = [ 'af_enabled', 'af_hidden', 'af_deleted', 'af_global' ];
+ if ( count( array_diff( $filters, $changed ) ) < count( $filters ) ) {
+ $fieldChanged = true;
+ }
+ } elseif ( in_array( $mappings[$name], $changed ) ) {
+ $fieldChanged = true;
+ }
+
+ if ( $fieldChanged ) {
+ $formatted = Xml::tags( 'div',
+ [ 'class' => 'mw-abusefilter-history-changed' ],
+ $formatted
+ );
+ }
+
+ return $formatted;
+ }
+
+ /**
+ * @return array
+ */
+ public function getQueryInfo() {
+ $info = [
+ 'tables' => [ 'abuse_filter_history', 'abuse_filter' ],
+ // All fields but afh_deleted on abuse_filter_history
+ 'fields' => [
+ 'afh_filter',
+ 'afh_timestamp',
+ 'afh_user_text',
+ 'afh_public_comments',
+ 'afh_flags',
+ 'afh_comments',
+ 'afh_actions',
+ 'afh_id',
+ 'afh_user',
+ 'afh_changed_fields',
+ 'afh_pattern',
+ 'af_hidden'
+ ],
+ 'conds' => [],
+ 'join_conds' => [
+ 'abuse_filter' =>
+ [
+ 'LEFT JOIN',
+ 'afh_filter=af_id',
+ ],
+ ],
+ ];
+
+ if ( $this->mUser ) {
+ $info['conds']['afh_user_text'] = $this->mUser;
+ }
+
+ if ( $this->mFilter ) {
+ $info['conds']['afh_filter'] = $this->mFilter;
+ }
+
+ if ( !$this->getUser()->isAllowedAny(
+ 'abusefilter-modify', 'abusefilter-view-private' )
+ ) {
+ // Hide data the user can't see.
+ $info['conds']['af_hidden'] = 0;
+ }
+
+ return $info;
+ }
+
+ /**
+ * @return string
+ */
+ public function getIndexField() {
+ return 'afh_timestamp';
+ }
+
+ /**
+ * @return string
+ */
+ public function getDefaultSort() {
+ return 'afh_timestamp';
+ }
+
+ /**
+ * @param string $name
+ * @return bool
+ */
+ public function isFieldSortable( $name ) {
+ $sortable_fields = [ 'afh_timestamp', 'afh_user_text' ];
+ return in_array( $name, $sortable_fields );
+ }
+
+ /**
+ * Title used for self-links.
+ *
+ * @return Title
+ */
+ public function getTitle() {
+ return $this->mPage->getTitle( 'history/' . $this->mFilter );
+ }
+}
diff --git a/AbuseFilter/includes/pagers/AbuseFilterPager.php b/AbuseFilter/includes/pagers/AbuseFilterPager.php
new file mode 100644
index 00000000..2092b850
--- /dev/null
+++ b/AbuseFilter/includes/pagers/AbuseFilterPager.php
@@ -0,0 +1,296 @@
+<?php
+
+use MediaWiki\Linker\LinkRenderer;
+
+/**
+ * Class to build paginated filter list
+ */
+class AbuseFilterPager extends TablePager {
+
+ /**
+ * @var LinkRenderer
+ */
+ protected $linkRenderer;
+
+ public $mPage, $mConds, $mQuery;
+
+ /**
+ * @param AbuseFilterViewList $page
+ * @param array $conds
+ * @param LinkRenderer $linkRenderer
+ * @param array $query
+ */
+ public function __construct( $page, $conds, $linkRenderer, $query ) {
+ $this->mPage = $page;
+ $this->mConds = $conds;
+ $this->linkRenderer = $linkRenderer;
+ $this->mQuery = $query;
+ parent::__construct( $this->mPage->getContext() );
+ }
+
+ /**
+ * @return array
+ */
+ public function getQueryInfo() {
+ return [
+ 'tables' => [ 'abuse_filter' ],
+ 'fields' => [
+ // All columns but af_comments
+ 'af_id',
+ 'af_enabled',
+ 'af_deleted',
+ 'af_pattern',
+ 'af_global',
+ 'af_public_comments',
+ 'af_hidden',
+ 'af_hit_count',
+ 'af_timestamp',
+ 'af_user_text',
+ 'af_user',
+ 'af_actions',
+ 'af_group',
+ 'af_throttled'
+ ],
+ 'conds' => $this->mConds,
+ ];
+ }
+
+ /**
+ * @see Pager::getFieldNames()
+ * @return array
+ */
+ public function getFieldNames() {
+ static $headers = null;
+
+ if ( !empty( $headers ) ) {
+ return $headers;
+ }
+
+ $headers = [
+ 'af_id' => 'abusefilter-list-id',
+ 'af_public_comments' => 'abusefilter-list-public',
+ 'af_actions' => 'abusefilter-list-consequences',
+ 'af_enabled' => 'abusefilter-list-status',
+ 'af_timestamp' => 'abusefilter-list-lastmodified',
+ 'af_hidden' => 'abusefilter-list-visibility',
+ ];
+
+ if ( $this->mPage->getUser()->isAllowed( 'abusefilter-log-detail' ) ) {
+ $headers['af_hit_count'] = 'abusefilter-list-hitcount';
+ }
+
+ if ( AbuseFilterView::canViewPrivate() && !empty( $this->mQuery[0] ) ) {
+ $headers['af_pattern'] = 'abusefilter-list-pattern';
+ }
+
+ if ( count( $this->getConfig()->get( 'AbuseFilterValidGroups' ) ) > 1 ) {
+ $headers['af_group'] = 'abusefilter-list-group';
+ }
+
+ foreach ( $headers as &$msg ) {
+ $msg = $this->msg( $msg )->text();
+ }
+
+ return $headers;
+ }
+
+ /**
+ * @param string $name
+ * @param string $value
+ * @return string
+ */
+ public function formatValue( $name, $value ) {
+ $lang = $this->getLanguage();
+ $row = $this->mCurrentRow;
+
+ switch ( $name ) {
+ case 'af_id':
+ return $this->linkRenderer->makeLink(
+ SpecialPage::getTitleFor( 'AbuseFilter', intval( $value ) ),
+ $lang->formatNum( intval( $value ) )
+ );
+ case 'af_pattern':
+ if ( $this->mQuery[1] === 'LIKE' ) {
+ $position = mb_stripos( $row->af_pattern, $this->mQuery[0] );
+ if ( $position === false ) {
+ // This may happen due to problems with character encoding
+ // which aren't easy to solve
+ return htmlspecialchars( mb_substr( $row->af_pattern, 0, 50 ) );
+ }
+ $length = mb_strlen( $this->mQuery[0] );
+ } else {
+ $regex = '/' . $this->mQuery[0] . '/u';
+ if ( $this->mQuery[1] === 'IRLIKE' ) {
+ $regex .= 'i';
+ }
+
+ $matches = [];
+ Wikimedia\suppressWarnings();
+ $check = preg_match(
+ $regex,
+ $row->af_pattern,
+ $matches
+ );
+ Wikimedia\restoreWarnings();
+ // This may happen in case of catastrophic backtracking
+ if ( $check === false ) {
+ return htmlspecialchars( mb_substr( $row->af_pattern, 0, 50 ) );
+ }
+
+ $length = mb_strlen( $matches[0] );
+ $position = mb_strpos( $row->af_pattern, $matches[0] );
+ }
+
+ $remaining = 50 - $length;
+ if ( $remaining <= 0 ) {
+ // Truncate the filter pattern and only show the first 50 characters of the match
+ $pattern = '<b>' .
+ htmlspecialchars( mb_substr( $row->af_pattern, $position, 50 ) ) .
+ '</b>';
+ } else {
+ // Center the snippet on the matched string
+ $minoffset = max( $position - round( $remaining / 2 ), 0 );
+ $pattern = mb_substr( $row->af_pattern, $minoffset, 50 );
+ $pattern =
+ htmlspecialchars( mb_substr( $pattern, 0, $position - $minoffset ) ) .
+ '<b>' .
+ htmlspecialchars( mb_substr( $pattern, $position - $minoffset, $length ) ) .
+ '</b>' .
+ htmlspecialchars( mb_substr(
+ $pattern,
+ $position - $minoffset + $length,
+ $remaining - ( $position - $minoffset + $length )
+ )
+ );
+ }
+ return $pattern;
+ case 'af_public_comments':
+ return $this->linkRenderer->makeLink(
+ SpecialPage::getTitleFor( 'AbuseFilter', intval( $row->af_id ) ),
+ $value
+ );
+ case 'af_actions':
+ $actions = explode( ',', $value );
+ $displayActions = [];
+ foreach ( $actions as $action ) {
+ $displayActions[] = AbuseFilter::getActionDisplay( $action );
+ }
+ return $lang->commaList( $displayActions );
+ case 'af_enabled':
+ $statuses = [];
+ if ( $row->af_deleted ) {
+ $statuses[] = $this->msg( 'abusefilter-deleted' )->parse();
+ } elseif ( $row->af_enabled ) {
+ $statuses[] = $this->msg( 'abusefilter-enabled' )->parse();
+ if ( $row->af_throttled ) {
+ $statuses[] = $this->msg( 'abusefilter-throttled' )->parse();
+ }
+ } else {
+ $statuses[] = $this->msg( 'abusefilter-disabled' )->parse();
+ }
+
+ if ( $row->af_global && $this->getConfig()->get( 'AbuseFilterIsCentral' ) ) {
+ $statuses[] = $this->msg( 'abusefilter-status-global' )->parse();
+ }
+
+ return $lang->commaList( $statuses );
+ case 'af_hidden':
+ $msg = $value ? 'abusefilter-hidden' : 'abusefilter-unhidden';
+ return $this->msg( $msg )->parse();
+ case 'af_hit_count':
+ if ( SpecialAbuseLog::canSeeDetails( $row->af_id, $row->af_hidden ) ) {
+ $count_display = $this->msg( 'abusefilter-hitcount' )
+ ->numParams( $value )->text();
+ $link = $this->linkRenderer->makeKnownLink(
+ SpecialPage::getTitleFor( 'AbuseLog' ),
+ $count_display,
+ [],
+ [ 'wpSearchFilter' => $row->af_id ]
+ );
+ } else {
+ $link = "";
+ }
+ return $link;
+ case 'af_timestamp':
+ $userLink =
+ Linker::userLink(
+ $row->af_user,
+ $row->af_user_text
+ ) .
+ Linker::userToolLinks(
+ $row->af_user,
+ $row->af_user_text
+ );
+
+ return $this->msg( 'abusefilter-edit-lastmod-text' )
+ ->rawParams(
+ $this->mPage->getLinkToLatestDiff(
+ $row->af_id,
+ $lang->timeanddate( $value, true )
+ ),
+ $userLink,
+ $this->mPage->getLinkToLatestDiff(
+ $row->af_id,
+ $lang->date( $value, true )
+ ),
+ $this->mPage->getLinkToLatestDiff(
+ $row->af_id,
+ $lang->time( $value, true )
+ )
+ )->params(
+ wfEscapeWikiText( $row->af_user_text )
+ )->parse();
+ case 'af_group':
+ return AbuseFilter::nameGroup( $value );
+ default:
+ throw new MWException( "Unknown row type $name!" );
+ }
+ }
+
+ /**
+ * @return string
+ */
+ public function getDefaultSort() {
+ return 'af_id';
+ }
+
+ /**
+ * @return string
+ */
+ public function getTableClass() {
+ return 'TablePager mw-abusefilter-list-scrollable';
+ }
+
+ /**
+ * @see TablePager::getRowClass()
+ * @param stdClass $row
+ * @return string
+ */
+ public function getRowClass( $row ) {
+ if ( $row->af_enabled ) {
+ return $row->af_throttled ? 'mw-abusefilter-list-throttled' : 'mw-abusefilter-list-enabled';
+ } elseif ( $row->af_deleted ) {
+ return 'mw-abusefilter-list-deleted';
+ } else {
+ return 'mw-abusefilter-list-disabled';
+ }
+ }
+
+ /**
+ * @param string $name
+ * @return bool
+ */
+ public function isFieldSortable( $name ) {
+ $sortable_fields = [
+ 'af_id',
+ 'af_enabled',
+ 'af_timestamp',
+ 'af_hidden',
+ 'af_group',
+ ];
+ if ( $this->mPage->getUser()->isAllowed( 'abusefilter-log-detail' ) ) {
+ $sortable_fields[] = 'af_hit_count';
+ }
+ return in_array( $name, $sortable_fields );
+ }
+}
diff --git a/AbuseFilter/includes/pagers/AbuseLogPager.php b/AbuseFilter/includes/pagers/AbuseLogPager.php
new file mode 100644
index 00000000..5c4f1a66
--- /dev/null
+++ b/AbuseFilter/includes/pagers/AbuseLogPager.php
@@ -0,0 +1,89 @@
+<?php
+
+use Wikimedia\Rdbms\IResultWrapper;
+
+class AbuseLogPager extends ReverseChronologicalPager {
+ /**
+ * @var SpecialAbuseLog
+ */
+ public $mForm;
+
+ /**
+ * @var array
+ */
+ public $mConds;
+
+ /**
+ * @param SpecialAbuseLog $form
+ * @param array $conds
+ */
+ public function __construct( $form, $conds = [] ) {
+ $this->mForm = $form;
+ $this->mConds = $conds;
+ parent::__construct();
+ }
+
+ /**
+ * @param object $row
+ * @return string
+ */
+ public function formatRow( $row ) {
+ return $this->mForm->formatRow( $row );
+ }
+
+ /**
+ * @return array
+ */
+ public function getQueryInfo() {
+ $conds = $this->mConds;
+
+ $info = [
+ 'tables' => [ 'abuse_filter_log', 'abuse_filter' ],
+ 'fields' => '*',
+ 'conds' => $conds,
+ 'join_conds' =>
+ [ 'abuse_filter' =>
+ [
+ 'LEFT JOIN',
+ 'af_id=afl_filter',
+ ],
+ ],
+ ];
+
+ if ( !$this->mForm->canSeeHidden() ) {
+ $db = $this->mDb;
+ $info['conds'][] = SpecialAbuseLog::getNotDeletedCond( $db );
+ }
+
+ return $info;
+ }
+
+ /**
+ * @param IResultWrapper $result
+ */
+ protected function preprocessResults( $result ) {
+ if ( $this->getNumRows() === 0 ) {
+ return;
+ }
+
+ $lb = new LinkBatch();
+ $lb->setCaller( __METHOD__ );
+ foreach ( $result as $row ) {
+ // Only for local wiki results
+ if ( !$row->afl_wiki ) {
+ $lb->add( $row->afl_namespace, $row->afl_title );
+ $lb->add( NS_USER, $row->afl_user );
+ $lb->add( NS_USER_TALK, $row->afl_user_text );
+ }
+ }
+ $lb->execute();
+ $result->seek( 0 );
+ }
+
+ /**
+ * @return string
+ */
+ public function getIndexField() {
+ return 'afl_timestamp';
+ }
+}
diff --git a/AbuseFilter/includes/pagers/GlobalAbuseFilterPager.php b/AbuseFilter/includes/pagers/GlobalAbuseFilterPager.php
new file mode 100644
index 00000000..30173475
--- /dev/null
+++ b/AbuseFilter/includes/pagers/GlobalAbuseFilterPager.php
@@ -0,0 +1,81 @@
+<?php
+
+use MediaWiki\Linker\LinkRenderer;
+
+/**
+ * Class to build paginated filter list for wikis using global abuse filters
+ */
+class GlobalAbuseFilterPager extends AbuseFilterPager {
+ /**
+ * @param AbuseFilterViewList $page
+ * @param array $conds
+ * @param LinkRenderer $linkRenderer
+ */
+ public function __construct( $page, $conds, $linkRenderer ) {
+ parent::__construct( $page, $conds, $linkRenderer, [ '', 'LIKE' ] );
+ $this->mDb = wfGetDB(
+ DB_REPLICA, [], $this->getConfig()->get( 'AbuseFilterCentralDB' ) );
+ }
+
+ /**
+ * @param string $name
+ * @param string $value
+ * @return string
+ */
+ public function formatValue( $name, $value ) {
+ $lang = $this->getLanguage();
+ $row = $this->mCurrentRow;
+
+ switch ( $name ) {
+ case 'af_id':
+ return $lang->formatNum( intval( $value ) );
+ case 'af_public_comments':
+ return $this->getOutput()->parseInline( $value );
+ case 'af_actions':
+ $actions = explode( ',', $value );
+ $displayActions = [];
+ foreach ( $actions as $action ) {
+ $displayActions[] = AbuseFilter::getActionDisplay( $action );
+ }
+ return $lang->commaList( $displayActions );
+ case 'af_enabled':
+ $statuses = [];
+ if ( $row->af_deleted ) {
+ $statuses[] = $this->msg( 'abusefilter-deleted' )->parse();
+ } elseif ( $row->af_enabled ) {
+ $statuses[] = $this->msg( 'abusefilter-enabled' )->parse();
+ } else {
+ $statuses[] = $this->msg( 'abusefilter-disabled' )->parse();
+ }
+ if ( $row->af_global ) {
+ $statuses[] = $this->msg( 'abusefilter-status-global' )->parse();
+ }
+
+ return $lang->commaList( $statuses );
+ case 'af_hidden':
+ $msg = $value ? 'abusefilter-hidden' : 'abusefilter-unhidden';
+ return $this->msg( $msg )->parse();
+ case 'af_hit_count':
+ // If the rule is hidden, don't show it, even to priviledged local admins
+ if ( $row->af_hidden ) {
+ return '';
+ }
+ return $this->msg( 'abusefilter-hitcount' )->numParams( $value )->parse();
+ case 'af_timestamp':
+ $user = $row->af_user_text;
+ return $this->msg(
+ 'abusefilter-edit-lastmod-text',
+ $lang->timeanddate( $value, true ),
+ $user,
+ $lang->date( $value, true ),
+ $lang->time( $value, true ),
+ $user
+ )->parse();
+ case 'af_group':
+ // If this is global, local name probably doesn't exist, but try
+ return AbuseFilter::nameGroup( $value );
+ default:
+ throw new MWException( "Unknown row type $name!" );
+ }
+ }
+}
diff --git a/AbuseFilter/includes/parser/AFPData.php b/AbuseFilter/includes/parser/AFPData.php
index d5a0069d..de8d8270 100644
--- a/AbuseFilter/includes/parser/AFPData.php
+++ b/AbuseFilter/includes/parser/AFPData.php
@@ -7,7 +7,7 @@ class AFPData {
const DNULL = 'null';
const DBOOL = 'bool';
const DFLOAT = 'float';
- const DLIST = 'list';
+ const DARRAY = 'array';
// Translation table mapping shell-style wildcards to PCRE equivalents.
// Derived from <http://www.php.net/manual/en/function.fnmatch.php#100207>
@@ -28,7 +28,7 @@ class AFPData {
/**
* @param string $type
- * @param null $val
+ * @param mixed|null $val
*/
public function __construct( $type = self::DNULL, $val = null ) {
$this->type = $type;
@@ -36,7 +36,7 @@ class AFPData {
}
/**
- * @param $var
+ * @param mixed $var
* @return AFPData
* @throws AFPException
*/
@@ -55,7 +55,7 @@ class AFPData {
$result[] = self::newFromPHPVar( $item );
}
- return new AFPData( self::DLIST, $result );
+ return new AFPData( self::DARRAY, $result );
} elseif ( is_null( $var ) ) {
return new AFPData();
} else {
@@ -73,8 +73,8 @@ class AFPData {
}
/**
- * @param $orig AFPData
- * @param $target
+ * @param AFPData $orig
+ * @param string $target
* @return AFPData
*/
public static function castTypes( $orig, $target ) {
@@ -85,7 +85,7 @@ class AFPData {
return new AFPData();
}
- if ( $orig->type == self::DLIST ) {
+ if ( $orig->type == self::DARRAY ) {
if ( $target == self::DBOOL ) {
return new AFPData( self::DBOOL, (bool)count( $orig->data ) );
}
@@ -117,13 +117,13 @@ class AFPData {
if ( $target == self::DSTRING ) {
return new AFPData( self::DSTRING, strval( $orig->data ) );
}
- if ( $target == self::DLIST ) {
- return new AFPData( self::DLIST, [ $orig ] );
+ if ( $target == self::DARRAY ) {
+ return new AFPData( self::DARRAY, [ $orig ] );
}
}
/**
- * @param $value AFPData
+ * @param AFPData $value
* @return AFPData
*/
public static function boolInvert( $value ) {
@@ -131,17 +131,22 @@ class AFPData {
}
/**
- * @param $base AFPData
- * @param $exponent AFPData
+ * @param AFPData $base
+ * @param AFPData $exponent
* @return AFPData
*/
public static function pow( $base, $exponent ) {
- return new AFPData( self::DFLOAT, pow( $base->toFloat(), $exponent->toFloat() ) );
+ $res = pow( $base->toNumber(), $exponent->toNumber() );
+ if ( $res === (int)$res ) {
+ return new AFPData( self::DINT, $res );
+ } else {
+ return new AFPData( self::DFLOAT, $res );
+ }
}
/**
- * @param $a AFPData
- * @param $b AFPData
+ * @param AFPData $a
+ * @param AFPData $b
* @return AFPData
*/
public static function keywordIn( $a, $b ) {
@@ -156,8 +161,8 @@ class AFPData {
}
/**
- * @param $a AFPData
- * @param $b AFPData
+ * @param AFPData $a
+ * @param AFPData $b
* @return AFPData
*/
public static function keywordContains( $a, $b ) {
@@ -172,51 +177,64 @@ class AFPData {
}
/**
- * @param $value
- * @param $list
+ * @param AFPData $d1
+ * @param AFPData $d2
+ * @param bool $strict whether to also check types
* @return bool
*/
- public static function listContains( $value, $list ) {
- // Should use built-in PHP function somehow
- foreach ( $list->data as $item ) {
- if ( self::equals( $value, $item ) ) {
- return true;
+ public static function equals( $d1, $d2, $strict = false ) {
+ if ( $d1->type != self::DARRAY && $d2->type != self::DARRAY ) {
+ $typecheck = $d1->type == $d2->type || !$strict;
+ return $typecheck && $d1->toString() === $d2->toString();
+ } elseif ( $d1->type == self::DARRAY && $d2->type == self::DARRAY ) {
+ $data1 = $d1->data;
+ $data2 = $d2->data;
+ if ( count( $data1 ) !== count( $data2 ) ) {
+ return false;
+ }
+ $length = count( $data1 );
+ for ( $i = 0; $i < $length; $i++ ) {
+ $result = self::equals( $data1[$i], $data2[$i], $strict );
+ if ( $result === false ) {
+ return false;
+ }
+ }
+ return true;
+ } else {
+ // Trying to compare an array to something else
+ if ( $strict ) {
+ return false;
+ }
+ if ( $d1->type == self::DARRAY && count( $d1->data ) === 0 ) {
+ return ( $d2->type == self::DBOOL && $d2->toBool() == false ) || $d2->type == self::DNULL;
+ } elseif ( $d2->type == self::DARRAY && count( $d2->data ) === 0 ) {
+ return ( $d1->type == self::DBOOL && $d1->toBool() == false ) || $d1->type == self::DNULL;
+ } else {
+ return false;
}
}
-
- return false;
}
/**
- * @param $d1 AFPData
- * @param $d2 AFPData
- * @return bool
- */
- public static function equals( $d1, $d2 ) {
- return $d1->type != self::DLIST && $d2->type != self::DLIST &&
- $d1->toString() === $d2->toString();
- }
-
- /**
- * @param $str AFPData
- * @param $pattern AFPData
+ * @param AFPData $str
+ * @param AFPData $pattern
* @return AFPData
*/
public static function keywordLike( $str, $pattern ) {
$str = $str->toString();
$pattern = '#^' . strtr( preg_quote( $pattern->toString(), '#' ), self::$wildcardMap ) . '$#u';
- MediaWiki\suppressWarnings();
+ Wikimedia\suppressWarnings();
$result = preg_match( $pattern, $str );
- MediaWiki\restoreWarnings();
+ Wikimedia\restoreWarnings();
return new AFPData( self::DBOOL, (bool)$result );
}
/**
- * @param $str AFPData
- * @param $regex AFPData
- * @param $pos
- * @param $insensitive bool
+ * @param AFPData $str
+ * @param AFPData $regex
+ * @param int $pos
+ * @param bool $insensitive
* @return AFPData
* @throws Exception
*/
@@ -231,12 +249,14 @@ class AFPData {
$pattern .= 'i';
}
+ Wikimedia\suppressWarnings();
$result = preg_match( $pattern, $str );
+ Wikimedia\restoreWarnings();
if ( $result === false ) {
throw new AFPUserVisibleException(
'regexfailure',
$pos,
- [ 'unspecified error in preg_match()', $pattern ]
+ [ $pattern ]
);
}
@@ -244,9 +264,9 @@ class AFPData {
}
/**
- * @param $str
- * @param $regex
- * @param $pos
+ * @param AFPData $str
+ * @param AFPData $regex
+ * @param int $pos
* @return AFPData
*/
public static function keywordRegexInsensitive( $str, $regex, $pos ) {
@@ -254,7 +274,7 @@ class AFPData {
}
/**
- * @param $data AFPData
+ * @param AFPData $data
* @return AFPData
*/
public static function unaryMinus( $data ) {
@@ -266,9 +286,9 @@ class AFPData {
}
/**
- * @param $a AFPData
- * @param $b AFPData
- * @param $op string
+ * @param AFPData $a
+ * @param AFPData $b
+ * @param string $op
* @return AFPData
* @throws AFPException
*/
@@ -284,13 +304,14 @@ class AFPData {
if ( $op == '^' ) {
return new AFPData( self::DBOOL, $a xor $b );
}
- throw new AFPException( "Invalid boolean operation: {$op}" ); // Should never happen.
+ // Should never happen.
+ throw new AFPException( "Invalid boolean operation: {$op}" );
}
/**
- * @param $a AFPData
- * @param $b AFPData
- * @param $op string
+ * @param AFPData $a
+ * @param AFPData $b
+ * @param string $op
* @return AFPData
* @throws AFPException
*/
@@ -302,10 +323,10 @@ class AFPData {
return new AFPData( self::DBOOL, !self::equals( $a, $b ) );
}
if ( $op == '===' ) {
- return new AFPData( self::DBOOL, $a->type == $b->type && self::equals( $a, $b ) );
+ return new AFPData( self::DBOOL, self::equals( $a, $b, true ) );
}
if ( $op == '!==' ) {
- return new AFPData( self::DBOOL, $a->type != $b->type || !self::equals( $a, $b ) );
+ return new AFPData( self::DBOOL, !self::equals( $a, $b, true ) );
}
$a = $a->toString();
$b = $b->toString();
@@ -321,31 +342,22 @@ class AFPData {
if ( $op == '<=' ) {
return new AFPData( self::DBOOL, $a <= $b );
}
- throw new AFPException( "Invalid comparison operation: {$op}" ); // Should never happen
+ // Should never happen
+ throw new AFPException( "Invalid comparison operation: {$op}" );
}
/**
- * @param $a AFPData
- * @param $b AFPData
- * @param $op string
- * @param $pos
+ * @param AFPData $a
+ * @param AFPData $b
+ * @param string $op
+ * @param int $pos
* @return AFPData
* @throws AFPUserVisibleException
* @throws AFPException
*/
public static function mulRel( $a, $b, $op, $pos ) {
- // Figure out the type.
- if ( $a->type == self::DFLOAT || $b->type == self::DFLOAT ||
- $a->toFloat() != $a->toString() || $b->toFloat() != $b->toString()
- ) {
- $type = self::DFLOAT;
- $a = $a->toFloat();
- $b = $b->toFloat();
- } else {
- $type = self::DINT;
- $a = $a->toInt();
- $b = $b->toInt();
- }
+ $a = $a->toNumber();
+ $b = $b->toNumber();
if ( $op != '*' && $b == 0 ) {
throw new AFPUserVisibleException( 'dividebyzero', $pos, [ $a ] );
@@ -362,37 +374,49 @@ class AFPData {
throw new AFPException( "Invalid multiplication-related operation: {$op}" );
}
- if ( $type == self::DINT ) {
+ if ( $data === (int)$data ) {
$data = intval( $data );
+ $type = self::DINT;
} else {
$data = floatval( $data );
+ $type = self::DFLOAT;
}
return new AFPData( $type, $data );
}
/**
- * @param $a AFPData
- * @param $b AFPData
+ * @param AFPData $a
+ * @param AFPData $b
* @return AFPData
*/
public static function sum( $a, $b ) {
if ( $a->type == self::DSTRING || $b->type == self::DSTRING ) {
return new AFPData( self::DSTRING, $a->toString() . $b->toString() );
- } elseif ( $a->type == self::DLIST && $b->type == self::DLIST ) {
- return new AFPData( self::DLIST, array_merge( $a->toList(), $b->toList() ) );
+ } elseif ( $a->type == self::DARRAY && $b->type == self::DARRAY ) {
+ return new AFPData( self::DARRAY, array_merge( $a->toArray(), $b->toArray() ) );
} else {
- return new AFPData( self::DFLOAT, $a->toFloat() + $b->toFloat() );
+ $res = $a->toNumber() + $b->toNumber();
+ if ( $res === (int)$res ) {
+ return new AFPData( self::DINT, $res );
+ } else {
+ return new AFPData( self::DFLOAT, $res );
+ }
}
}
/**
- * @param $a AFPData
- * @param $b AFPData
+ * @param AFPData $a
+ * @param AFPData $b
* @return AFPData
*/
public static function sub( $a, $b ) {
- return new AFPData( self::DFLOAT, $a->toFloat() - $b->toFloat() );
+ $res = $a->toNumber() - $b->toNumber();
+ if ( $res === (int)$res ) {
+ return new AFPData( self::DINT, $res );
+ } else {
+ return new AFPData( self::DFLOAT, $res );
+ }
}
/** Convert shorteners */
@@ -411,8 +435,8 @@ class AFPData {
return $this->toFloat();
case self::DINT:
return $this->toInt();
- case self::DLIST:
- $input = $this->toList();
+ case self::DARRAY:
+ $input = $this->toArray();
$output = [];
foreach ( $input as $item ) {
$output[] = $item->toNative();
@@ -454,7 +478,17 @@ class AFPData {
return self::castTypes( $this, self::DINT )->data;
}
- public function toList() {
- return self::castTypes( $this, self::DLIST )->data;
+ /**
+ * @return int|float
+ */
+ public function toNumber() {
+ return $this->type == self::DINT ? $this->toInt() : $this->toFloat();
+ }
+
+ /**
+ * @return array
+ */
+ public function toArray() {
+ return self::castTypes( $this, self::DARRAY )->data;
}
}
diff --git a/AbuseFilter/includes/parser/AFPParserState.php b/AbuseFilter/includes/parser/AFPParserState.php
index 7a4f5a73..453948d1 100644
--- a/AbuseFilter/includes/parser/AFPParserState.php
+++ b/AbuseFilter/includes/parser/AFPParserState.php
@@ -3,6 +3,10 @@
class AFPParserState {
public $pos, $token;
+ /**
+ * @param AFPToken $token
+ * @param int $pos
+ */
public function __construct( $token, $pos ) {
$this->token = $token;
$this->pos = $pos;
diff --git a/AbuseFilter/includes/parser/AFPToken.php b/AbuseFilter/includes/parser/AFPToken.php
index 2f7d9c99..897f5ded 100644
--- a/AbuseFilter/includes/parser/AFPToken.php
+++ b/AbuseFilter/includes/parser/AFPToken.php
@@ -21,7 +21,7 @@
* * Entry - catches unexpected characters
* * Semicolon - ;
* * Set - :=
- * * Conditionls (IF) - if-then-else-end, cond ? a :b
+ * * Conditionals (IF) - if-then-else-end, cond ? a :b
* * BoolOps (BO) - &, |, ^
* * CompOps (CO) - ==, !=, ===, !==, >, <, >=, <=
* * SumRel (SR) - +, -
@@ -30,13 +30,12 @@
* * BoolNeg (BN) - ! operation
* * SpecialOperators (SO) - in and like
* * Unarys (U) - plus and minus in cases like -5 or -(2 * +2)
- * * ListElement (LE) - list[number]
+ * * ArrayElement (AE) - array[number]
* * Braces (B) - ( and )
* * Functions (F)
* * Atom (A) - return value
*/
class AFPToken {
- // Types of tken
const TNONE = 'T_NONE';
const TID = 'T_ID';
const TKEYWORD = 'T_KEYWORD';
@@ -53,6 +52,11 @@ class AFPToken {
public $value;
public $pos;
+ /**
+ * @param string $type
+ * @param mixed|null $value
+ * @param int $pos
+ */
public function __construct( $type = self::TNONE, $value = null, $pos = 0 ) {
$this->type = $type;
$this->value = $value;
diff --git a/AbuseFilter/includes/parser/AFPTreeNode.php b/AbuseFilter/includes/parser/AFPTreeNode.php
index e185616c..a3c2a063 100644
--- a/AbuseFilter/includes/parser/AFPTreeNode.php
+++ b/AbuseFilter/includes/parser/AFPTreeNode.php
@@ -15,12 +15,12 @@ class AFPTreeNode {
// ASSIGNMENT (formerly known as SET) is a node which is responsible for
// assigning values to variables. ASSIGNMENT is a (variable name [string],
// value [tree node]) tuple, INDEX_ASSIGNMENT (which is used to assign
- // values at list offsets) is a (variable name [string], index [tree node],
- // value [tree node]) tuple, and LIST_APPEND has the form of (variable name
+ // values at array offsets) is a (variable name [string], index [tree node],
+ // value [tree node]) tuple, and ARRAY_APPEND has the form of (variable name
// [string], value [tree node]).
const ASSIGNMENT = 'ASSIGNMENT';
const INDEX_ASSIGNMENT = 'INDEX_ASSIGNMENT';
- const LIST_APPEND = 'LIST_APPEND';
+ const ARRAY_APPEND = 'ARRAY_APPEND';
// CONDITIONAL represents both a ternary operator and an if-then-else-end
// construct. The format is (condition, evaluated-if-true,
@@ -53,13 +53,12 @@ class AFPTreeNode {
// filter language. The format is (keyword, left operand, right operand).
const KEYWORD_OPERATOR = 'KEYWORD_OPERATOR';
- // UNARY is either unary minus or unary plus. The format is (operator,
- // operand).
+ // UNARY is either unary minus or unary plus. The format is (operator, operand).
const UNARY = 'UNARY';
- // LIST_INDEX is an operation of accessing a list by an offset. The format
- // is (list, offset).
- const LIST_INDEX = 'LIST_INDEX';
+ // ARRAY_INDEX is an operation of accessing an array by an offset. The format
+ // is (array, offset).
+ const ARRAY_INDEX = 'ARRAY_INDEX';
// Since parenthesis only manipulate precedence of the operators, they are
// not explicitly represented in the tree.
@@ -69,9 +68,9 @@ class AFPTreeNode {
// elements are the arguments.
const FUNCTION_CALL = 'FUNCTION_CALL';
- // LIST_DEFINITION is a list literal. The $children field contains tree
- // nodes for the values of each of the list element used.
- const LIST_DEFINITION = 'LIST_DEFINITION';
+ // ARRAY_DEFINITION is an array literal. The $children field contains tree
+ // nodes for the values of each of the array element used.
+ const ARRAY_DEFINITION = 'ARRAY_DEFINITION';
// ATOM is a node representing a literal. The only element of $children is a
// token corresponding to the literal.
@@ -90,12 +89,20 @@ class AFPTreeNode {
// Position used for error reporting.
public $position;
+ /**
+ * @param string $type
+ * @param AFPTreeNode[]|string[]|AFPToken $children
+ * @param int $position
+ */
public function __construct( $type, $children, $position ) {
$this->type = $type;
$this->children = $children;
$this->position = $position;
}
+ /**
+ * @return string
+ */
public function toDebugString() {
return implode( "\n", $this->toDebugStringInner() );
}
diff --git a/AbuseFilter/includes/parser/AFPTreeParser.php b/AbuseFilter/includes/parser/AFPTreeParser.php
index 345adcb8..4fa35356 100644
--- a/AbuseFilter/includes/parser/AFPTreeParser.php
+++ b/AbuseFilter/includes/parser/AFPTreeParser.php
@@ -26,6 +26,9 @@ class AFPTreeParser {
$this->resetState();
}
+ /**
+ * Resets the state
+ */
public function resetState() {
$this->mTokens = [];
$this->mPos = 0;
@@ -174,7 +177,7 @@ class AFPTreeParser {
$value = $this->doLevelSet();
if ( $index === 'append' ) {
return new AFPTreeNode(
- AFPTreeNode::LIST_APPEND, [ $varname, $value ], $position );
+ AFPTreeNode::ARRAY_APPEND, [ $varname, $value ], $position );
} else {
return new AFPTreeNode(
AFPTreeNode::INDEX_ASSIGNMENT,
@@ -412,7 +415,7 @@ class AFPTreeParser {
$leftOperand = $this->doLevelUnarys();
$keyword = strtolower( $this->mCur->value );
if ( $this->mCur->type == AFPToken::TKEYWORD &&
- in_array( $keyword, array_keys( AbuseFilterParser::$mKeywords ) )
+ isset( AbuseFilterParser::$mKeywords[$keyword] )
) {
$position = $this->mPos;
$this->move();
@@ -438,24 +441,24 @@ class AFPTreeParser {
if ( $this->mCur->type == AFPToken::TOP && ( $op == "+" || $op == "-" ) ) {
$position = $this->mPos;
$this->move();
- $argument = $this->doLevelListElements();
+ $argument = $this->doLevelArrayElements();
return new AFPTreeNode( AFPTreeNode::UNARY, [ $op, $argument ], $position );
}
- return $this->doLevelListElements();
+ return $this->doLevelArrayElements();
}
/**
- * Handles accessing a list element by an offset.
+ * Handles accessing an array element by an offset.
*
* @return AFPTreeNode
* @throws AFPUserVisibleException
*/
- protected function doLevelListElements() {
- $list = $this->doLevelParenthesis();
+ protected function doLevelArrayElements() {
+ $array = $this->doLevelParenthesis();
while ( $this->mCur->type == AFPToken::TSQUAREBRACKET && $this->mCur->value == '[' ) {
$position = $this->mPos;
$index = $this->doLevelSemicolon();
- $list = new AFPTreeNode( AFPTreeNode::LIST_INDEX, [ $list, $index ], $position );
+ $array = new AFPTreeNode( AFPTreeNode::ARRAY_INDEX, [ $array, $index ], $position );
if ( !( $this->mCur->type == AFPToken::TSQUAREBRACKET && $this->mCur->value == ']' ) ) {
throw new AFPUserVisibleException( 'expectednotfound', $this->mPos,
@@ -464,7 +467,7 @@ class AFPTreeParser {
$this->move();
}
- return $list;
+ return $array;
}
/**
@@ -568,14 +571,14 @@ class AFPTreeParser {
/** @noinspection PhpMissingBreakStatementInspection */
case AFPToken::TSQUAREBRACKET:
if ( $this->mCur->value == '[' ) {
- $list = [];
+ $array = [];
while ( true ) {
$this->move();
if ( $this->mCur->type == AFPToken::TSQUAREBRACKET && $this->mCur->value == ']' ) {
break;
}
- $list[] = $this->doLevelSet();
+ $array[] = $this->doLevelSet();
if ( $this->mCur->type == AFPToken::TSQUAREBRACKET && $this->mCur->value == ']' ) {
break;
@@ -589,7 +592,7 @@ class AFPTreeParser {
}
}
- $result = new AFPTreeNode( AFPTreeNode::LIST_DEFINITION, $list, $this->mPos );
+ $result = new AFPTreeNode( AFPTreeNode::ARRAY_DEFINITION, $array, $this->mPos );
break;
}
diff --git a/AbuseFilter/includes/parser/AFPUserVisibleException.php b/AbuseFilter/includes/parser/AFPUserVisibleException.php
index 45eea745..ab4b2264 100644
--- a/AbuseFilter/includes/parser/AFPUserVisibleException.php
+++ b/AbuseFilter/includes/parser/AFPUserVisibleException.php
@@ -3,8 +3,11 @@
// Exceptions that we might conceivably want to report to ordinary users
// (i.e. exceptions that don't represent bugs in the extension itself)
class AFPUserVisibleException extends AFPException {
- public $mExceptionId;
+ /** @var string */
+ public $mExceptionID;
+ /** @var int */
public $mPosition;
+ /** @var array */
public $mParams;
/**
@@ -12,7 +15,7 @@ class AFPUserVisibleException extends AFPException {
* @param int $position
* @param array $params
*/
- function __construct( $exception_id, $position, $params ) {
+ public function __construct( $exception_id, $position, $params ) {
$this->mExceptionID = $exception_id;
$this->mPosition = $position;
$this->mParams = $params;
@@ -22,6 +25,9 @@ class AFPUserVisibleException extends AFPException {
parent::__construct( $msg );
}
+ /**
+ * @return Message
+ */
public function getMessageObj() {
// Give grep a chance to find the usages:
// abusefilter-exception-unexpectedatend, abusefilter-exception-expectednotfound
@@ -31,10 +37,11 @@ class AFPUserVisibleException extends AFPException {
// abusefilter-exception-dividebyzero, abusefilter-exception-unrecognisedvar
// abusefilter-exception-notenoughargs, abusefilter-exception-regexfailure
// abusefilter-exception-overridebuiltin, abusefilter-exception-outofbounds
- // abusefilter-exception-notlist
+ // abusefilter-exception-notarray, abusefilter-exception-unclosedcomment
+ // abusefilter-exception-invalidiprange, abusefilter-exception-disabledvar
return wfMessage(
'abusefilter-exception-' . $this->mExceptionID,
- array_merge( [ $this->mPosition ], $this->mParams )
+ $this->mPosition, ...$this->mParams
);
}
}
diff --git a/AbuseFilter/includes/parser/AbuseFilterCachingParser.php b/AbuseFilter/includes/parser/AbuseFilterCachingParser.php
index 0c5ba69e..ef634fd4 100644
--- a/AbuseFilter/includes/parser/AbuseFilterCachingParser.php
+++ b/AbuseFilter/includes/parser/AbuseFilterCachingParser.php
@@ -12,6 +12,7 @@ class AbuseFilterCachingParser extends AbuseFilterParser {
* Return the generated version of the parser for cache invalidation
* purposes. Automatically tracks list of all functions and invalidates the
* cache if it is changed.
+ * @return string
*/
public static function getCacheVersion() {
static $version = null;
@@ -30,11 +31,18 @@ class AbuseFilterCachingParser extends AbuseFilterParser {
return $version;
}
+ /**
+ * Resets the state of the parser
+ */
public function resetState() {
$this->mVars = new AbuseFilterVariableHolder;
$this->mCur = new AFPToken();
}
+ /**
+ * @param string $code
+ * @return AFPData
+ */
public function intEval( $code ) {
static $cache = null;
if ( !$cache ) {
@@ -63,7 +71,7 @@ class AbuseFilterCachingParser extends AbuseFilterParser {
* Evaluate the value of the specified AST node.
*
* @param AFPTreeNode $node The node to evaluate.
- * @return AFPData
+ * @return AFPData|AFPTreeNode|string
* @throws AFPException
* @throws AFPUserVisibleException
* @throws MWException
@@ -71,8 +79,7 @@ class AbuseFilterCachingParser extends AbuseFilterParser {
public function evalNode( AFPTreeNode $node ) {
// A lot of AbuseFilterParser features rely on $this->mCur->pos or
// $this->mPos for error reporting.
- // FIXME: this is a hack which needs to be removed when the parsers are
- // merged.
+ // FIXME: this is a hack which needs to be removed when the parsers are merged.
$this->mPos = $node->position;
$this->mCur->pos = $node->position;
@@ -102,9 +109,9 @@ class AbuseFilterCachingParser extends AbuseFilterParser {
default:
throw new AFPException( "Unknown token provided in the ATOM node" );
}
- case AFPTreeNode::LIST_DEFINITION:
+ case AFPTreeNode::ARRAY_DEFINITION:
$items = array_map( [ $this, 'evalNode' ], $node->children );
- return new AFPData( AFPData::DLIST, $items );
+ return new AFPData( AFPData::DARRAY, $items );
case AFPTreeNode::FUNCTION_CALL:
$functionName = $node->children[0];
@@ -131,23 +138,23 @@ class AbuseFilterCachingParser extends AbuseFilterParser {
return $result;
- case AFPTreeNode::LIST_INDEX:
- list( $list, $offset ) = $node->children;
+ case AFPTreeNode::ARRAY_INDEX:
+ list( $array, $offset ) = $node->children;
- $list = $this->evalNode( $list );
- if ( $list->type != AFPData::DLIST ) {
- throw new AFPUserVisibleException( 'notlist', $node->position, [] );
+ $array = $this->evalNode( $array );
+ if ( $array->type != AFPData::DARRAY ) {
+ throw new AFPUserVisibleException( 'notarray', $node->position, [] );
}
$offset = $this->evalNode( $offset )->toInt();
- $list = $list->toList();
- if ( count( $list ) <= $offset ) {
+ $array = $array->toArray();
+ if ( count( $array ) <= $offset ) {
throw new AFPUserVisibleException( 'outofbounds', $node->position,
- [ $offset, count( $list ) ] );
+ [ $offset, count( $array ) ] );
}
- return $list[$offset];
+ return $array[$offset];
case AFPTreeNode::UNARY:
list( $operation, $argument ) = $node->children;
@@ -182,8 +189,8 @@ class AbuseFilterCachingParser extends AbuseFilterParser {
list( $op, $leftOperand, $rightOperand ) = $node->children;
$leftOperand = $this->evalNode( $leftOperand );
$rightOperand = $this->evalNode( $rightOperand );
- return AFPData::mulRel( $leftOperand, $rightOperand, $op, /* FIXME */
- 0 );
+ // FIXME
+ return AFPData::mulRel( $leftOperand, $rightOperand, $op, 0 );
case AFPTreeNode::SUM_REL:
list( $op, $leftOperand, $rightOperand ) = $node->children;
@@ -234,34 +241,34 @@ class AbuseFilterCachingParser extends AbuseFilterParser {
case AFPTreeNode::INDEX_ASSIGNMENT:
list( $varName, $offset, $value ) = $node->children;
- $list = $this->mVars->getVar( $varName );
- if ( $list->type != AFPData::DLIST ) {
- throw new AFPUserVisibleException( 'notlist', $node->position, [] );
+ $array = $this->mVars->getVar( $varName );
+ if ( $array->type != AFPData::DARRAY ) {
+ throw new AFPUserVisibleException( 'notarray', $node->position, [] );
}
$offset = $this->evalNode( $offset )->toInt();
- $list = $list->toList();
- if ( count( $list ) <= $offset ) {
+ $array = $array->toArray();
+ if ( count( $array ) <= $offset ) {
throw new AFPUserVisibleException( 'outofbounds', $node->position,
- [ $offset, count( $list ) ] );
+ [ $offset, count( $array ) ] );
}
- $list[$offset] = $this->evalNode( $value );
- $this->setUserVariable( $varName, new AFPData( AFPData::DLIST, $list ) );
+ $array[$offset] = $this->evalNode( $value );
+ $this->setUserVariable( $varName, new AFPData( AFPData::DARRAY, $array ) );
return $value;
- case AFPTreeNode::LIST_APPEND:
+ case AFPTreeNode::ARRAY_APPEND:
list( $varName, $value ) = $node->children;
- $list = $this->mVars->getVar( $varName );
- if ( $list->type != AFPData::DLIST ) {
- throw new AFPUserVisibleException( 'notlist', $node->position, [] );
+ $array = $this->mVars->getVar( $varName );
+ if ( $array->type != AFPData::DARRAY ) {
+ throw new AFPUserVisibleException( 'notarray', $node->position, [] );
}
- $list = $list->toList();
- $list[] = $this->evalNode( $value );
- $this->setUserVariable( $varName, new AFPData( AFPData::DLIST, $list ) );
+ $array = $array->toArray();
+ $array[] = $this->evalNode( $value );
+ $this->setUserVariable( $varName, new AFPData( AFPData::DARRAY, $array ) );
return $value;
case AFPTreeNode::SEMICOLON:
diff --git a/AbuseFilter/includes/parser/AbuseFilterParser.php b/AbuseFilter/includes/parser/AbuseFilterParser.php
index f9bd15b3..89ddea0f 100644
--- a/AbuseFilter/includes/parser/AbuseFilterParser.php
+++ b/AbuseFilter/includes/parser/AbuseFilterParser.php
@@ -1,14 +1,19 @@
<?php
+use Wikimedia\Equivset\Equivset;
+use MediaWiki\Logger\LoggerFactory;
+
class AbuseFilterParser {
- public $mCode, $mTokens, $mPos, $mCur, $mShortCircuit, $mAllowShort, $mLen;
+ public $mCode, $mTokens, $mPos, $mShortCircuit, $mAllowShort, $mLen;
+ /** @var AFPToken The current token */
+ public $mCur;
/**
* @var AbuseFilterVariableHolder
*/
public $mVars;
- // length,lcase,ucase,ccnorm,rmdoubles,specialratio,rmspecials,norm,count
+ // length,lcase,ucase,ccnorm,rmdoubles,specialratio,rmspecials,norm,count,get_matches
public static $mFunctions = [
'lcase' => 'funcLc',
'ucase' => 'funcUc',
@@ -19,14 +24,19 @@ class AbuseFilterParser {
'bool' => 'castBool',
'norm' => 'funcNorm',
'ccnorm' => 'funcCCNorm',
+ 'ccnorm_contains_any' => 'funcCCNormContainsAny',
+ 'ccnorm_contains_all' => 'funcCCNormContainsAll',
'specialratio' => 'funcSpecialRatio',
'rmspecials' => 'funcRMSpecials',
'rmdoubles' => 'funcRMDoubles',
'rmwhitespace' => 'funcRMWhitespace',
'count' => 'funcCount',
'rcount' => 'funcRCount',
+ 'get_matches' => 'funcGetMatches',
'ip_in_range' => 'funcIPInRange',
'contains_any' => 'funcContainsAny',
+ 'contains_all' => 'funcContainsAll',
+ 'equals_to_any' => 'funcEqualsToAny',
'substr' => 'funcSubstr',
'strlen' => 'funcLen',
'strpos' => 'funcStrPos',
@@ -34,6 +44,7 @@ class AbuseFilterParser {
'rescape' => 'funcStrRegexEscape',
'set' => 'funcSetVar',
'set_var' => 'funcSetVar',
+ 'sanitize' => 'funcSanitize',
];
// Functions that affect parser state, and shouldn't be cached.
@@ -48,15 +59,20 @@ class AbuseFilterParser {
'contains' => 'keywordContains',
'rlike' => 'keywordRegex',
'irlike' => 'keywordRegexInsensitive',
- 'regex' => 'keywordRegex'
+ 'regex' => 'keywordRegex',
];
public static $funcCache = [];
/**
+ * @var Equivset
+ */
+ protected static $equivset;
+
+ /**
* Create a new instance
*
- * @param $vars AbuseFilterVariableHolder
+ * @param AbuseFilterVariableHolder|null $vars
*/
public function __construct( $vars = null ) {
$this->resetState();
@@ -65,6 +81,9 @@ class AbuseFilterParser {
}
}
+ /**
+ * Resets the state of the parser.
+ */
public function resetState() {
$this->mCode = '';
$this->mTokens = [];
@@ -75,12 +94,13 @@ class AbuseFilterParser {
}
/**
- * @param $filter
- * @return array|bool
+ * @param string $filter
+ * @return true|array True when successful, otherwise a two-element array with exception message
+ * and character position of the syntax error
*/
public function checkSyntax( $filter ) {
+ $origAS = $this->mAllowShort;
try {
- $origAS = $this->mAllowShort;
$this->mAllowShort = false;
$this->parse( $filter );
} catch ( AFPUserVisibleException $excep ) {
@@ -94,28 +114,7 @@ class AbuseFilterParser {
}
/**
- * @param $name
- * @param $value
- */
- public function setVar( $name, $value ) {
- $this->mVars->setVar( $name, $value );
- }
-
- /**
- * @param $vars
- */
- public function setVars( $vars ) {
- if ( is_array( $vars ) ) {
- foreach ( $vars as $name => $var ) {
- $this->setVar( $name, $var );
- }
- } elseif ( $vars instanceof AbuseFilterVariableHolder ) {
- $this->mVars->addHolders( $vars );
- }
- }
-
- /**
- * @return AFPToken
+ * Move to the next token
*/
protected function move() {
list( $this->mCur, $this->mPos ) = $this->mTokens[$this->mPos];
@@ -139,16 +138,9 @@ class AbuseFilterParser {
}
/**
- * @return mixed
* @throws AFPUserVisibleException
*/
protected function skipOverBraces() {
- if ( !( $this->mCur->type == AFPToken::TBRACE && $this->mCur->value == '(' ) ||
- !$this->mShortCircuit
- ) {
- return;
- }
-
$braces = 1;
while ( $this->mCur->type != AFPToken::TNONE && $braces > 0 ) {
$this->move();
@@ -166,7 +158,7 @@ class AbuseFilterParser {
}
/**
- * @param $code
+ * @param string $code
* @return bool
*/
public function parse( $code ) {
@@ -174,7 +166,7 @@ class AbuseFilterParser {
}
/**
- * @param $filter
+ * @param string $filter
* @return string
*/
public function evaluateExpression( $filter ) {
@@ -182,11 +174,11 @@ class AbuseFilterParser {
}
/**
- * @param $code
+ * @param string $code
* @return AFPData
*/
- function intEval( $code ) {
- // Setup, resetting
+ public function intEval( $code ) {
+ // Reset all class members to their default value
$this->mCode = $code;
$this->mTokens = AbuseFilterTokenizer::tokenize( $code );
$this->mPos = 0;
@@ -199,25 +191,12 @@ class AbuseFilterParser {
return $result;
}
- /**
- * @param $a
- * @param $b
- * @return int
- */
- static function lengthCompare( $a, $b ) {
- if ( strlen( $a ) == strlen( $b ) ) {
- return 0;
- }
-
- return ( strlen( $a ) < strlen( $b ) ) ? -1 : 1;
- }
-
/* Levels */
/**
* Handles unexpected characters after the expression
*
- * @param $result AFPData
+ * @param AFPData &$result
* @throws AFPUserVisibleException
*/
protected function doLevelEntry( &$result ) {
@@ -232,8 +211,9 @@ class AbuseFilterParser {
}
/**
- * Handles multiple expressions
- * @param $result AFPData
+ * Handles multiple expressions delimited by a semicolon
+ *
+ * @param AFPData &$result
*/
protected function doLevelSemicolon( &$result ) {
do {
@@ -245,9 +225,9 @@ class AbuseFilterParser {
}
/**
- * Handles multiple expressions
+ * Handles assignments (:=)
*
- * @param $result AFPData
+ * @param AFPData &$result
* @throws AFPUserVisibleException
*/
protected function doLevelSet( &$result ) {
@@ -269,11 +249,11 @@ class AbuseFilterParser {
[ $varname ]
);
}
- $list = $this->mVars->getVar( $varname );
- if ( $list->type != AFPData::DLIST ) {
- throw new AFPUserVisibleException( 'notlist', $this->mCur->pos, [] );
+ $array = $this->mVars->getVar( $varname );
+ if ( $array->type != AFPData::DARRAY ) {
+ throw new AFPUserVisibleException( 'notarray', $this->mCur->pos, [] );
}
- $list = $list->toList();
+ $array = $array->toArray();
$this->move();
if ( $this->mCur->type == AFPToken::TSQUAREBRACKET && $this->mCur->value == ']' ) {
$idx = 'new';
@@ -287,7 +267,7 @@ class AbuseFilterParser {
throw new AFPUserVisibleException( 'expectednotfound', $this->mCur->pos,
[ ']', $this->mCur->type, $this->mCur->value ] );
}
- if ( count( $list ) <= $idx ) {
+ if ( count( $array ) <= $idx ) {
throw new AFPUserVisibleException( 'outofbounds', $this->mCur->pos,
[ $idx, count( $result->data ) ] );
}
@@ -297,11 +277,11 @@ class AbuseFilterParser {
$this->move();
$this->doLevelSet( $result );
if ( $idx === 'new' ) {
- $list[] = $result;
+ $array[] = $result;
} else {
- $list[$idx] = $result;
+ $array[$idx] = $result;
}
- $this->setUserVariable( $varname, new AFPData( AFPData::DLIST, $list ) );
+ $this->setUserVariable( $varname, new AFPData( AFPData::DARRAY, $array ) );
return;
} else {
@@ -315,7 +295,9 @@ class AbuseFilterParser {
}
/**
- * @param $result AFPData
+ * Handles conditionals: if-then-else and ternary operator
+ *
+ * @param AFPData &$result
* @throws AFPUserVisibleException
*/
protected function doLevelConditions( &$result ) {
@@ -436,7 +418,9 @@ class AbuseFilterParser {
}
/**
- * @param $result AFPData
+ * Handles boolean operators (&, |, ^)
+ *
+ * @param AFPData &$result
*/
protected function doLevelBoolOps( &$result ) {
$this->doLevelCompares( $result );
@@ -446,7 +430,7 @@ class AbuseFilterParser {
$this->move();
$r2 = new AFPData();
- // We can go on quickly as either one statement with | is true or on with & is false
+ // We can go on quickly as either one statement with | is true or one with & is false
if ( ( $op == '&' && !$result->toBool() ) || ( $op == '|' && $result->toBool() ) ) {
$orig = $this->mShortCircuit;
$this->mShortCircuit = $this->mAllowShort;
@@ -463,7 +447,9 @@ class AbuseFilterParser {
}
/**
- * @param $result
+ * Handles comparison operators
+ *
+ * @param AFPData &$result
*/
protected function doLevelCompares( &$result ) {
$this->doLevelSumRels( $result );
@@ -474,7 +460,8 @@ class AbuseFilterParser {
$r2 = new AFPData();
$this->doLevelSumRels( $r2 );
if ( $this->mShortCircuit ) {
- break; // The result doesn't matter.
+ // The result doesn't matter.
+ break;
}
AbuseFilter::triggerLimiter();
$result = AFPData::compareOp( $result, $r2, $op );
@@ -482,7 +469,9 @@ class AbuseFilterParser {
}
/**
- * @param $result
+ * Handles sum-related operations (+ and -)
+ *
+ * @param AFPData &$result
*/
protected function doLevelSumRels( &$result ) {
$this->doLevelMulRels( $result );
@@ -493,7 +482,8 @@ class AbuseFilterParser {
$r2 = new AFPData();
$this->doLevelMulRels( $r2 );
if ( $this->mShortCircuit ) {
- break; // The result doesn't matter.
+ // The result doesn't matter.
+ break;
}
if ( $op == '+' ) {
$result = AFPData::sum( $result, $r2 );
@@ -505,7 +495,9 @@ class AbuseFilterParser {
}
/**
- * @param $result
+ * Handles multiplication-related operations (*, / and %)
+ *
+ * @param AFPData &$result
*/
protected function doLevelMulRels( &$result ) {
$this->doLevelPow( $result );
@@ -516,14 +508,17 @@ class AbuseFilterParser {
$r2 = new AFPData();
$this->doLevelPow( $r2 );
if ( $this->mShortCircuit ) {
- break; // The result doesn't matter.
+ // The result doesn't matter.
+ break;
}
$result = AFPData::mulRel( $result, $r2, $op, $this->mCur->pos );
}
}
/**
- * @param $result
+ * Handles powers (**)
+ *
+ * @param AFPData &$result
*/
protected function doLevelPow( &$result ) {
$this->doLevelBoolInvert( $result );
@@ -532,21 +527,25 @@ class AbuseFilterParser {
$expanent = new AFPData();
$this->doLevelBoolInvert( $expanent );
if ( $this->mShortCircuit ) {
- break; // The result doesn't matter.
+ // The result doesn't matter.
+ break;
}
$result = AFPData::pow( $result, $expanent );
}
}
/**
- * @param $result
+ * Handles boolean inversion (!)
+ *
+ * @param AFPData &$result
*/
protected function doLevelBoolInvert( &$result ) {
if ( $this->mCur->type == AFPToken::TOP && $this->mCur->value == '!' ) {
$this->move();
$this->doLevelSpecialWords( $result );
if ( $this->mShortCircuit ) {
- return; // The result doesn't matter.
+ // The result doesn't matter.
+ return;
}
$result = AFPData::boolInvert( $result );
} else {
@@ -555,13 +554,15 @@ class AbuseFilterParser {
}
/**
- * @param $result
+ * Handles keywords (in, like, rlike, contains, ...)
+ *
+ * @param AFPData &$result
*/
protected function doLevelSpecialWords( &$result ) {
$this->doLevelUnarys( $result );
$keyword = strtolower( $this->mCur->value );
if ( $this->mCur->type == AFPToken::TKEYWORD
- && in_array( $keyword, array_keys( self::$mKeywords ) )
+ && isset( self::$mKeywords[$keyword] )
) {
$func = self::$mKeywords[$keyword];
$this->move();
@@ -569,7 +570,8 @@ class AbuseFilterParser {
$this->doLevelUnarys( $r2 );
if ( $this->mShortCircuit ) {
- return; // The result doesn't matter.
+ // The result doesn't matter.
+ return;
}
AbuseFilter::triggerLimiter();
@@ -579,29 +581,34 @@ class AbuseFilterParser {
}
/**
- * @param $result
+ * Handles unary plus and minus, like in -5 or -(2 * +2)
+ *
+ * @param AFPData &$result
*/
protected function doLevelUnarys( &$result ) {
$op = $this->mCur->value;
if ( $this->mCur->type == AFPToken::TOP && ( $op == "+" || $op == "-" ) ) {
$this->move();
- $this->doLevelListElements( $result );
+ $this->doLevelArrayElements( $result );
if ( $this->mShortCircuit ) {
- return; // The result doesn't matter.
+ // The result doesn't matter.
+ return;
}
if ( $op == '-' ) {
$result = AFPData::unaryMinus( $result );
}
} else {
- $this->doLevelListElements( $result );
+ $this->doLevelArrayElements( $result );
}
}
/**
- * @param $result
+ * Handles array elements, parsing expressions like array[number]
+ *
+ * @param AFPData &$result
* @throws AFPUserVisibleException
*/
- protected function doLevelListElements( &$result ) {
+ protected function doLevelArrayElements( &$result ) {
$this->doLevelBraces( $result );
while ( $this->mCur->type == AFPToken::TSQUAREBRACKET && $this->mCur->value == '[' ) {
$idx = new AFPData();
@@ -611,21 +618,23 @@ class AbuseFilterParser {
[ ']', $this->mCur->type, $this->mCur->value ] );
}
$idx = $idx->toInt();
- if ( $result->type == AFPData::DLIST ) {
+ if ( $result->type == AFPData::DARRAY ) {
if ( count( $result->data ) <= $idx ) {
throw new AFPUserVisibleException( 'outofbounds', $this->mCur->pos,
[ $idx, count( $result->data ) ] );
}
$result = $result->data[$idx];
} else {
- throw new AFPUserVisibleException( 'notlist', $this->mCur->pos, [] );
+ throw new AFPUserVisibleException( 'notarray', $this->mCur->pos, [] );
}
$this->move();
}
}
/**
- * @param $result
+ * Handles brackets, only ( and )
+ *
+ * @param AFPData &$result
* @throws AFPUserVisibleException
*/
protected function doLevelBraces( &$result ) {
@@ -649,7 +658,9 @@ class AbuseFilterParser {
}
/**
- * @param $result
+ * Handles functions
+ *
+ * @param AFPData &$result
* @throws AFPUserVisibleException
*/
protected function doLevelFunction( &$result ) {
@@ -671,15 +682,21 @@ class AbuseFilterParser {
$this->skipOverBraces();
$this->move();
- return; // The result doesn't matter.
+ // The result doesn't matter.
+ return;
}
$args = [];
- do {
- $r = new AFPData();
- $this->doLevelSemicolon( $r );
- $args[] = $r;
- } while ( $this->mCur->type == AFPToken::TCOMMA );
+ $state = $this->getState();
+ $this->move();
+ if ( $this->mCur->type != AFPToken::TBRACE || $this->mCur->value != ')' ) {
+ $this->setState( $state );
+ do {
+ $r = new AFPData();
+ $this->doLevelSemicolon( $r );
+ $args[] = $r;
+ } while ( $this->mCur->type == AFPToken::TCOMMA );
+ }
if ( $this->mCur->type != AFPToken::TBRACE || $this->mCur->value != ')' ) {
throw new AFPUserVisibleException( 'expectednotfound',
@@ -713,9 +730,10 @@ class AbuseFilterParser {
}
/**
- * @param $result
+ * Handles the return value
+ *
+ * @param AFPData &$result
* @throws AFPUserVisibleException
- * @return AFPData
*/
protected function doLevelAtom( &$result ) {
$tok = $this->mCur->value;
@@ -752,14 +770,16 @@ class AbuseFilterParser {
}
break;
case AFPToken::TNONE:
- return; // Handled at entry level
+ // Handled at entry level
+ return;
case AFPToken::TBRACE:
if ( $this->mCur->value == ')' ) {
- return; // Handled at the entry level
+ // Handled at the entry level
+ return;
}
case AFPToken::TSQUAREBRACKET:
if ( $this->mCur->value == '[' ) {
- $list = [];
+ $array = [];
while ( true ) {
$this->move();
if ( $this->mCur->type == AFPToken::TSQUAREBRACKET && $this->mCur->value == ']' ) {
@@ -767,7 +787,7 @@ class AbuseFilterParser {
}
$item = new AFPData();
$this->doLevelSet( $item );
- $list[] = $item;
+ $array[] = $item;
if ( $this->mCur->type == AFPToken::TSQUAREBRACKET && $this->mCur->value == ']' ) {
break;
}
@@ -779,7 +799,7 @@ class AbuseFilterParser {
);
}
}
- $result = new AFPData( AFPData::DLIST, $list );
+ $result = new AFPData( AFPData::DARRAY, $array );
break;
}
default:
@@ -798,19 +818,28 @@ class AbuseFilterParser {
/* End of levels */
/**
- * @param $var
+ * @param string $var
* @return AFPData
* @throws AFPUserVisibleException
*/
protected function getVarValue( $var ) {
$var = strtolower( $var );
$builderValues = AbuseFilter::getBuilderValues();
+ $deprecatedVars = AbuseFilter::getDeprecatedVariables();
+ if ( array_key_exists( $var, $deprecatedVars ) ) {
+ $logger = LoggerFactory::getInstance( 'AbuseFilterDeprecatedVars' );
+ $logger->debug( "AbuseFilter: deprecated variable $var used." );
+ $var = $deprecatedVars[$var];
+ }
if ( !( array_key_exists( $var, $builderValues['vars'] )
|| $this->mVars->varIsSet( $var ) )
) {
+ $msg = array_key_exists( $var, AbuseFilter::$disabledVars ) ?
+ 'disabledvar' :
+ 'unrecognisedvar';
// If the variable is invalid, throw an exception
throw new AFPUserVisibleException(
- 'unrecognisedvar',
+ $msg,
$this->mCur->pos,
[ $var ]
);
@@ -820,13 +849,18 @@ class AbuseFilterParser {
}
/**
- * @param $name
- * @param $value
+ * @param string $name
+ * @param mixed $value
* @throws AFPUserVisibleException
*/
protected function setUserVariable( $name, $value ) {
$builderValues = AbuseFilter::getBuilderValues();
- if ( array_key_exists( $name, $builderValues['vars'] ) ) {
+ $deprecatedVars = AbuseFilter::getDeprecatedVariables();
+ $blacklistedValues = AbuseFilterVariableHolder::$varBlacklist;
+ if ( array_key_exists( $name, $builderValues['vars'] ) ||
+ array_key_exists( $name, AbuseFilter::$disabledVars ) ||
+ array_key_exists( $name, $deprecatedVars ) ||
+ in_array( $name, $blacklistedValues ) ) {
throw new AFPUserVisibleException( 'overridebuiltin', $this->mCur->pos, [ $name ] );
}
$this->mVars->setVar( $name, $value );
@@ -835,17 +869,17 @@ class AbuseFilterParser {
// Built-in functions
/**
- * @param $args
+ * @param array $args
* @return AFPData
* @throws AFPUserVisibleException
*/
protected function funcLc( $args ) {
global $wgContLang;
- if ( count( $args ) < 1 ) {
+ if ( count( $args ) === 0 ) {
throw new AFPUserVisibleException(
- 'notenoughargs',
+ 'noparams',
$this->mCur->pos,
- [ 'lc', 2, count( $args ) ]
+ [ 'lc', 1 ]
);
}
$s = $args[0]->toString();
@@ -854,17 +888,17 @@ class AbuseFilterParser {
}
/**
- * @param $args
+ * @param array $args
* @return AFPData
* @throws AFPUserVisibleException
*/
protected function funcUc( $args ) {
global $wgContLang;
- if ( count( $args ) < 1 ) {
+ if ( count( $args ) === 0 ) {
throw new AFPUserVisibleException(
- 'notenoughargs',
+ 'noparams',
$this->mCur->pos,
- [ 'uc', 2, count( $args ) ]
+ [ 'uc', 1 ]
);
}
$s = $args[0]->toString();
@@ -873,20 +907,20 @@ class AbuseFilterParser {
}
/**
- * @param $args
+ * @param array $args
* @return AFPData
* @throws AFPUserVisibleException
*/
protected function funcLen( $args ) {
- if ( count( $args ) < 1 ) {
+ if ( count( $args ) === 0 ) {
throw new AFPUserVisibleException(
- 'notenoughargs',
+ 'noparams',
$this->mCur->pos,
- [ 'len', 2, count( $args ) ]
+ [ 'len', 1 ]
);
}
- if ( $args[0]->type == AFPData::DLIST ) {
- // Don't use toString on lists, but count
+ if ( $args[0]->type == AFPData::DARRAY ) {
+ // Don't use toString on arrays, but count
return new AFPData( AFPData::DINT, count( $args[0]->data ) );
}
$s = $args[0]->toString();
@@ -895,37 +929,16 @@ class AbuseFilterParser {
}
/**
- * @param $args
- * @return AFPData
- * @throws AFPUserVisibleException
- */
- protected function funcSimpleNorm( $args ) {
- if ( count( $args ) < 1 ) {
- throw new AFPUserVisibleException(
- 'notenoughargs',
- $this->mCur->pos,
- [ 'simplenorm', 2, count( $args ) ]
- );
- }
- $s = $args[0]->toString();
-
- $s = preg_replace( '/[\d\W]+/', '', $s );
- $s = strtolower( $s );
-
- return new AFPData( AFPData::DSTRING, $s );
- }
-
- /**
- * @param $args
+ * @param array $args
* @return AFPData
* @throws AFPUserVisibleException
*/
protected function funcSpecialRatio( $args ) {
- if ( count( $args ) < 1 ) {
+ if ( count( $args ) === 0 ) {
throw new AFPUserVisibleException(
- 'notenoughargs',
+ 'noparams',
$this->mCur->pos,
- [ 'specialratio', 1, count( $args ) ]
+ [ 'specialratio', 1 ]
);
}
$s = $args[0]->toString();
@@ -942,20 +955,20 @@ class AbuseFilterParser {
}
/**
- * @param $args
+ * @param array $args
* @return AFPData
* @throws AFPUserVisibleException
*/
protected function funcCount( $args ) {
- if ( count( $args ) < 1 ) {
+ if ( count( $args ) === 0 ) {
throw new AFPUserVisibleException(
- 'notenoughargs',
+ 'noparams',
$this->mCur->pos,
- [ 'count', 1, count( $args ) ]
+ [ 'count', 1 ]
);
}
- if ( $args[0]->type == AFPData::DLIST && count( $args ) == 1 ) {
+ if ( $args[0]->type == AFPData::DARRAY && count( $args ) == 1 ) {
return new AFPData( AFPData::DINT, count( $args[0]->data ) );
}
@@ -965,7 +978,7 @@ class AbuseFilterParser {
$needle = $args[0]->toString();
$haystack = $args[1]->toString();
- // Bug #60203: Keep empty parameters from causing PHP warnings
+ // T62203: Keep empty parameters from causing PHP warnings
if ( $needle === '' ) {
$count = 0;
} else {
@@ -977,17 +990,17 @@ class AbuseFilterParser {
}
/**
- * @param $args
+ * @param array $args
* @return AFPData
* @throws AFPUserVisibleException
* @throws Exception
*/
protected function funcRCount( $args ) {
- if ( count( $args ) < 1 ) {
+ if ( count( $args ) === 0 ) {
throw new AFPUserVisibleException(
- 'notenoughargs',
+ 'noparams',
$this->mCur->pos,
- [ 'rcount', 1, count( $args ) ]
+ [ 'rcount', 1 ]
);
}
@@ -997,18 +1010,20 @@ class AbuseFilterParser {
$needle = $args[0]->toString();
$haystack = $args[1]->toString();
- # Munge the regex
+ // Munge the regex
$needle = preg_replace( '!(\\\\\\\\)*(\\\\)?/!', '$1\/', $needle );
$needle = "/$needle/u";
- // Omit the '$matches' argument to avoid computing them, just count.
+ // Suppress and restore needed per T177744
+ Wikimedia\suppressWarnings();
$count = preg_match_all( $needle, $haystack );
+ Wikimedia\restoreWarnings();
if ( $count === false ) {
throw new AFPUserVisibleException(
'regexfailure',
$this->mCur->pos,
- [ 'unspecified error in preg_match_all()', $needle ]
+ [ $needle ]
);
}
}
@@ -1017,7 +1032,60 @@ class AbuseFilterParser {
}
/**
- * @param $args
+ * Returns an array of matches of needle in the haystack, the first one for the whole regex,
+ * the other ones for every capturing group.
+ *
+ * @param array $args
+ * @return AFPData An array of matches.
+ * @throws AFPUserVisibleException
+ */
+ protected function funcGetMatches( $args ) {
+ if ( count( $args ) < 2 ) {
+ throw new AFPUserVisibleException(
+ 'notenoughargs',
+ $this->mCur->pos,
+ [ 'get_matches', 2, count( $args ) ]
+ );
+ }
+ $needle = $args[0]->toString();
+ $haystack = $args[1]->toString();
+
+ // Count the amount of capturing groups in the submitted pattern.
+ // This way we can return a fixed-dimension array, much easier to manage.
+ // First, strip away escaped parentheses
+ $sanitized = preg_replace( '/(\\\\\\\\)*\\\\\(/', '', $needle );
+ // Then strip starting parentheses of non-capturing groups
+ // (also atomics, lookahead and so on, even if not every of them is supported)
+ $sanitized = preg_replace( '/\(\?/', '', $sanitized );
+ // Finally create an array of falses with dimension = # of capturing groups
+ $groupscount = substr_count( $sanitized, '(' ) + 1;
+ $falsy = array_fill( 0, $groupscount, false );
+
+ // Munge the regex by escaping slashes
+ $needle = preg_replace( '!(\\\\\\\\)*(\\\\)?/!', '$1\/', $needle );
+ $needle = "/$needle/u";
+
+ // Suppress and restore are here for the same reason as T177744
+ Wikimedia\suppressWarnings();
+ $check = preg_match( $needle, $haystack, $matches );
+ Wikimedia\restoreWarnings();
+
+ if ( $check === false ) {
+ throw new AFPUserVisibleException(
+ 'regexfailure',
+ $this->mCur->pos,
+ [ $needle ]
+ );
+ }
+
+ // Returned array has non-empty positions identical to the ones returned
+ // by the third parameter of a standard preg_match call ($matches in this case).
+ // We want an union with falsy to return a fixed-dimension array.
+ return AFPData::newFromPHPVar( $matches + $falsy );
+ }
+
+ /**
+ * @param array $args
* @return AFPData
* @throws AFPUserVisibleException
*/
@@ -1033,22 +1101,30 @@ class AbuseFilterParser {
$ip = $args[0]->toString();
$range = $args[1]->toString();
+ if ( !IP::isValidRange( $range ) ) {
+ throw new AFPUserVisibleException(
+ 'invalidiprange',
+ $this->mCur->pos,
+ [ $range ]
+ );
+ }
+
$result = IP::isInRange( $ip, $range );
return new AFPData( AFPData::DBOOL, $result );
}
/**
- * @param $args
+ * @param array $args
* @return AFPData
* @throws AFPUserVisibleException
*/
protected function funcCCNorm( $args ) {
- if ( count( $args ) < 1 ) {
+ if ( count( $args ) === 0 ) {
throw new AFPUserVisibleException(
- 'notenoughargs',
+ 'noparams',
$this->mCur->pos,
- [ 'ccnorm', 1, count( $args ) ]
+ [ 'ccnorm', 1 ]
);
}
$s = $args[0]->toString();
@@ -1060,7 +1136,28 @@ class AbuseFilterParser {
}
/**
- * @param $args array
+ * @param array $args
+ * @return AFPData
+ * @throws AFPUserVisibleException
+ */
+ protected function funcSanitize( $args ) {
+ if ( count( $args ) === 0 ) {
+ throw new AFPUserVisibleException(
+ 'noparams',
+ $this->mCur->pos,
+ [ 'sanitize', 1 ]
+ );
+ }
+ $s = $args[0]->toString();
+
+ $s = html_entity_decode( $s, ENT_QUOTES, 'UTF-8' );
+ $s = Sanitizer::decodeCharReferences( $s );
+
+ return new AFPData( AFPData::DSTRING, $s );
+ }
+
+ /**
+ * @param array $args
* @return AFPData
* @throws AFPUserVisibleException
*/
@@ -1074,53 +1171,177 @@ class AbuseFilterParser {
}
$s = array_shift( $args );
- $s = $s->toString();
- $searchStrings = [];
+ return new AFPData( AFPData::DBOOL, self::contains( $s, $args, true ) );
+ }
- foreach ( $args as $arg ) {
- $searchStrings[] = $arg->toString();
+ /**
+ * @param array $args
+ * @return AFPData
+ * @throws AFPUserVisibleException
+ */
+ protected function funcContainsAll( $args ) {
+ if ( count( $args ) < 2 ) {
+ throw new AFPUserVisibleException(
+ 'notenoughargs',
+ $this->mCur->pos,
+ [ 'contains_all', 2, count( $args ) ]
+ );
}
- if ( function_exists( 'fss_prep_search' ) ) {
- $fss = fss_prep_search( $searchStrings );
- $result = fss_exec_search( $fss, $s );
+ $s = array_shift( $args );
- $ok = is_array( $result );
- } else {
- $ok = false;
- foreach ( $searchStrings as $needle ) {
- // Bug #60203: Keep empty parameters from causing PHP warnings
- if ( $needle !== '' && strpos( $s, $needle ) !== false ) {
- $ok = true;
- break;
- }
+ return new AFPData( AFPData::DBOOL, self::contains( $s, $args, false, false ) );
+ }
+
+ /**
+ * Normalize and search a string for multiple substrings in OR mode
+ *
+ * @param array $args
+ * @return AFPData
+ * @throws AFPUserVisibleException
+ */
+ protected function funcCCNormContainsAny( $args ) {
+ if ( count( $args ) < 2 ) {
+ throw new AFPUserVisibleException(
+ 'notenoughargs',
+ $this->mCur->pos,
+ [ 'ccnorm_contains_any', 2, count( $args ) ]
+ );
+ }
+
+ $s = array_shift( $args );
+
+ return new AFPData( AFPData::DBOOL, self::contains( $s, $args, true, true ) );
+ }
+
+ /**
+ * Normalize and search a string for multiple substrings in AND mode
+ *
+ * @param array $args
+ * @return AFPData
+ * @throws AFPUserVisibleException
+ */
+ protected function funcCCNormContainsAll( $args ) {
+ if ( count( $args ) < 2 ) {
+ throw new AFPUserVisibleException(
+ 'notenoughargs',
+ $this->mCur->pos,
+ [ 'ccnorm_contains_all', 2, count( $args ) ]
+ );
+ }
+
+ $s = array_shift( $args );
+
+ return new AFPData( AFPData::DBOOL, self::contains( $s, $args, false, true ) );
+ }
+
+ /**
+ * Search for substrings in a string
+ *
+ * Use is_any to determine wether to use logic OR (true) or AND (false).
+ *
+ * Use normalize = true to make use of ccnorm and
+ * normalize both sides of the search.
+ *
+ * @param AFPData $string
+ * @param AFPData[] $values
+ * @param bool $is_any
+ * @param bool $normalize
+ *
+ * @return bool
+ */
+ protected static function contains( $string, $values, $is_any = true, $normalize = false ) {
+ $string = $string->toString();
+
+ if ( $string === '' ) {
+ return false;
+ }
+
+ if ( $normalize ) {
+ $string = self::ccnorm( $string );
+ }
+
+ foreach ( $values as $needle ) {
+ $needle = $needle->toString();
+ if ( $normalize ) {
+ $needle = self::ccnorm( $needle );
+ }
+ if ( $needle === '' ) {
+ // T62203: Keep empty parameters from causing PHP warnings
+ continue;
+ }
+
+ $is_found = strpos( $string, $needle ) !== false;
+ if ( $is_found === $is_any ) {
+ // If I'm here and it's ANY (OR) => something is found.
+ // If I'm here and it's ALL (AND) => nothing is found.
+ // In both cases, we've had enough.
+ return $is_found;
}
}
- return new AFPData( AFPData::DBOOL, $ok );
+ // If I'm here and it's ANY (OR) => nothing was found: return false ($is_any is true)
+ // If I'm here and it's ALL (AND) => everything was found: return true ($is_any is false)
+ return ! $is_any;
}
/**
- * @param $s
- * @return mixed
+ * @param array $args
+ * @return AFPData
+ * @throws AFPUserVisibleException
*/
- protected function ccnorm( $s ) {
- if ( is_callable( 'AntiSpoof::normalizeString' ) ) {
- $s = AntiSpoof::normalizeString( $s );
- } else {
- // AntiSpoof isn't available, so ignore and return same string
- wfDebugLog(
- 'AbuseFilter',
- "Can't compute normalized string (ccnorm) as the AntiSpoof Extension isn't installed."
+ protected function funcEqualsToAny( $args ) {
+ if ( count( $args ) < 2 ) {
+ throw new AFPUserVisibleException(
+ 'notenoughargs',
+ $this->mCur->pos,
+ [ 'equals_to_any', 2, count( $args ) ]
);
}
- return $s;
+ $s = array_shift( $args );
+
+ return new AFPData( AFPData::DBOOL, self::equalsToAny( $s, $args ) );
}
/**
- * @param $s string
+ * Check if the given string is equals to any of the following strings
+ *
+ * @param AFPData $string
+ * @param AFPData[] $values
+ *
+ * @return bool
+ */
+ protected static function equalsToAny( $string, $values ) {
+ $string = $string->toString();
+
+ foreach ( $values as $needle ) {
+ $needle = $needle->toString();
+
+ if ( $string === $needle ) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * @param string $s
+ * @return mixed
+ */
+ protected static function ccnorm( $s ) {
+ // Instantiate a single version of the equivset so the data is only loaded once.
+ if ( !self::$equivset ) {
+ self::$equivset = new Equivset();
+ }
+
+ return self::$equivset->normalize( $s );
+ }
+
+ /**
+ * @param string $s
* @return array|string
*/
protected function rmspecials( $s ) {
@@ -1128,7 +1349,7 @@ class AbuseFilterParser {
}
/**
- * @param $s string
+ * @param string $s
* @return array|string
*/
protected function rmdoubles( $s ) {
@@ -1136,7 +1357,7 @@ class AbuseFilterParser {
}
/**
- * @param $s string
+ * @param string $s
* @return array|string
*/
protected function rmwhitespace( $s ) {
@@ -1144,16 +1365,16 @@ class AbuseFilterParser {
}
/**
- * @param $args array
+ * @param array $args
* @return AFPData
* @throws AFPUserVisibleException
*/
protected function funcRMSpecials( $args ) {
- if ( count( $args ) < 1 ) {
+ if ( count( $args ) === 0 ) {
throw new AFPUserVisibleException(
- 'notenoughargs',
+ 'noparams',
$this->mCur->pos,
- [ 'rmspecials', 1, count( $args ) ]
+ [ 'rmspecials', 1 ]
);
}
$s = $args[0]->toString();
@@ -1164,16 +1385,16 @@ class AbuseFilterParser {
}
/**
- * @param $args array
+ * @param array $args
* @return AFPData
* @throws AFPUserVisibleException
*/
protected function funcRMWhitespace( $args ) {
- if ( count( $args ) < 1 ) {
+ if ( count( $args ) === 0 ) {
throw new AFPUserVisibleException(
- 'notenoughargs',
+ 'noparams',
$this->mCur->pos,
- [ 'rmwhitespace', 1, count( $args ) ]
+ [ 'rmwhitespace', 1 ]
);
}
$s = $args[0]->toString();
@@ -1184,16 +1405,16 @@ class AbuseFilterParser {
}
/**
- * @param $args array
+ * @param array $args
* @return AFPData
* @throws AFPUserVisibleException
*/
protected function funcRMDoubles( $args ) {
- if ( count( $args ) < 1 ) {
+ if ( count( $args ) === 0 ) {
throw new AFPUserVisibleException(
- 'notenoughargs',
+ 'noparams',
$this->mCur->pos,
- [ 'rmdoubles', 1, count( $args ) ]
+ [ 'rmdoubles', 1 ]
);
}
$s = $args[0]->toString();
@@ -1204,16 +1425,16 @@ class AbuseFilterParser {
}
/**
- * @param $args array
+ * @param array $args
* @return AFPData
* @throws AFPUserVisibleException
*/
protected function funcNorm( $args ) {
- if ( count( $args ) < 1 ) {
+ if ( count( $args ) === 0 ) {
throw new AFPUserVisibleException(
- 'notenoughargs',
+ 'noparams',
$this->mCur->pos,
- [ 'norm', 1, count( $args ) ]
+ [ 'norm', 1 ]
);
}
$s = $args[0]->toString();
@@ -1227,7 +1448,7 @@ class AbuseFilterParser {
}
/**
- * @param $args array
+ * @param array $args
* @return AFPData
* @throws AFPUserVisibleException
*/
@@ -1255,7 +1476,7 @@ class AbuseFilterParser {
}
/**
- * @param $args array
+ * @param array $args
* @return AFPData
* @throws AFPUserVisibleException
*/
@@ -1271,7 +1492,7 @@ class AbuseFilterParser {
$haystack = $args[0]->toString();
$needle = $args[1]->toString();
- // Bug #60203: Keep empty parameters from causing PHP warnings
+ // T62203: Keep empty parameters from causing PHP warnings
if ( $needle === '' ) {
return new AFPData( AFPData::DINT, -1 );
}
@@ -1292,7 +1513,7 @@ class AbuseFilterParser {
}
/**
- * @param $args array
+ * @param array $args
* @return AFPData
* @throws AFPUserVisibleException
*/
@@ -1313,14 +1534,17 @@ class AbuseFilterParser {
}
/**
- * @param $args array
+ * @param array $args
* @return AFPData
* @throws AFPUserVisibleException
*/
protected function funcStrRegexEscape( $args ) {
- if ( count( $args ) < 1 ) {
- throw new AFPUserVisibleException( 'notenoughargs', $this->mCur->pos,
- [ 'rescape', 1, count( $args ) ] );
+ if ( count( $args ) === 0 ) {
+ throw new AFPUserVisibleException(
+ 'noparams',
+ $this->mCur->pos,
+ [ 'rescape', 1 ]
+ );
}
$string = $args[0]->toString();
@@ -1331,7 +1555,7 @@ class AbuseFilterParser {
}
/**
- * @param $args array
+ * @param array $args
* @return mixed
* @throws AFPUserVisibleException
*/
@@ -1353,13 +1577,17 @@ class AbuseFilterParser {
}
/**
- * @param $args array
+ * @param array $args
* @return AFPData
* @throws AFPUserVisibleException
*/
protected function castString( $args ) {
- if ( count( $args ) < 1 ) {
- throw new AFPUserVisibleException( 'noparams', $this->mCur->pos, [ __METHOD__ ] );
+ if ( count( $args ) === 0 ) {
+ throw new AFPUserVisibleException(
+ 'noparams',
+ $this->mCur->pos,
+ [ 'string', 1 ]
+ );
}
$val = $args[0];
@@ -1367,13 +1595,17 @@ class AbuseFilterParser {
}
/**
- * @param $args array
+ * @param array $args
* @return AFPData
* @throws AFPUserVisibleException
*/
protected function castInt( $args ) {
- if ( count( $args ) < 1 ) {
- throw new AFPUserVisibleException( 'noparams', $this->mCur->pos, [ __METHOD__ ] );
+ if ( count( $args ) === 0 ) {
+ throw new AFPUserVisibleException(
+ 'noparams',
+ $this->mCur->pos,
+ [ 'int', 1 ]
+ );
}
$val = $args[0];
@@ -1381,13 +1613,17 @@ class AbuseFilterParser {
}
/**
- * @param $args array
+ * @param array $args
* @return AFPData
* @throws AFPUserVisibleException
*/
protected function castFloat( $args ) {
- if ( count( $args ) < 1 ) {
- throw new AFPUserVisibleException( 'noparams', $this->mCur->pos, [ __METHOD__ ] );
+ if ( count( $args ) === 0 ) {
+ throw new AFPUserVisibleException(
+ 'noparams',
+ $this->mCur->pos,
+ [ 'float', 1 ]
+ );
}
$val = $args[0];
@@ -1395,13 +1631,17 @@ class AbuseFilterParser {
}
/**
- * @param $args array
+ * @param array $args
* @return AFPData
* @throws AFPUserVisibleException
*/
protected function castBool( $args ) {
- if ( count( $args ) < 1 ) {
- throw new AFPUserVisibleException( 'noparams', $this->mCur->pos, [ __METHOD__ ] );
+ if ( count( $args ) === 0 ) {
+ throw new AFPUserVisibleException(
+ 'noparams',
+ $this->mCur->pos,
+ [ 'bool', 1 ]
+ );
}
$val = $args[0];
diff --git a/AbuseFilter/includes/parser/AbuseFilterTokenizer.php b/AbuseFilter/includes/parser/AbuseFilterTokenizer.php
index 025314e8..d95a6864 100644
--- a/AbuseFilter/includes/parser/AbuseFilterTokenizer.php
+++ b/AbuseFilter/includes/parser/AbuseFilterTokenizer.php
@@ -19,15 +19,24 @@ class AbuseFilterTokenizer {
// ** comes before *, etc. They are sorted to make it easy to spot
// such errors.
public static $operators = [
- '!==', '!=', '!', // Inequality
- '**', '*', // Multiplication/exponentiation
- '/', '+', '-', '%', // Other arithmetic
- '&', '|', '^', // Logic
- ':=', // Setting
- '?', ':', // Ternery
- '<=', '<', // Less than
- '>=', '>', // Greater than
- '===', '==', '=', // Equality
+ // Inequality
+ '!==', '!=', '!',
+ // Multiplication/exponentiation
+ '**', '*',
+ // Other arithmetic
+ '/', '+', '-', '%',
+ // Logic
+ '&', '|', '^',
+ // Setting
+ ':=',
+ // Ternary
+ '?', ':',
+ // Less than
+ '<=', '<',
+ // Greater than
+ '>=', '>',
+ // Equality
+ '===', '==', '=',
];
public static $punctuation = [
@@ -63,7 +72,7 @@ class AbuseFilterTokenizer {
* @throws AFPException
* @throws AFPUserVisibleException
*/
- static function tokenize( $code ) {
+ public static function tokenize( $code ) {
static $tokenizerCache = null;
if ( !$tokenizerCache ) {
@@ -113,6 +122,10 @@ class AbuseFilterTokenizer {
// Read past comments
while ( preg_match( self::COMMENT_START_RE, $code, $matches, 0, $offset ) ) {
+ if ( strpos( $code, '*/', $offset ) === false ) {
+ throw new AFPUserVisibleException(
+ 'unclosedcomment', $offset, [] );
+ }
$offset = strpos( $code, '*/', $offset ) + 2;
}
@@ -148,7 +161,7 @@ class AbuseFilterTokenizer {
if ( preg_match( self::RADIX_RE, $code, $matches, 0, $offset ) ) {
$token = $matches[0];
$input = $matches[1];
- $baseChar = isset( $matches[2] ) ? $matches[2] : null;
+ $baseChar = $matches[2] ?? null;
// Sometimes the base char gets mixed in with the rest of it because
// the regex targets hex, too.
// This mostly happens with binary
@@ -233,7 +246,8 @@ class AbuseFilterTokenizer {
if ( preg_match( '/^[0-9A-Fa-f]{2}$/', $chr ) ) {
$chr = base_convert( $chr, 16, 10 );
$token .= chr( $chr );
- $offset += 2; # \xXX -- 2 done later
+ // \xXX -- 2 done later
+ $offset += 2;
} else {
$token .= 'x';
}
@@ -245,6 +259,7 @@ class AbuseFilterTokenizer {
$offset += 2;
} else {
+ // Should never happen
$token .= $code[$offset];
$offset++;
}
diff --git a/AbuseFilter/includes/special/SpecialAbuseFilter.php b/AbuseFilter/includes/special/SpecialAbuseFilter.php
index 04d58191..ccd2f8ee 100644
--- a/AbuseFilter/includes/special/SpecialAbuseFilter.php
+++ b/AbuseFilter/includes/special/SpecialAbuseFilter.php
@@ -7,10 +7,16 @@ class SpecialAbuseFilter extends SpecialPage {
parent::__construct( 'AbuseFilter', 'abusefilter-view' );
}
+ /**
+ * @return bool
+ */
public function doesWrites() {
return true;
}
+ /**
+ * @param string|null $subpage
+ */
public function execute( $subpage ) {
$out = $this->getOutput();
$request = $this->getRequest();
@@ -23,7 +29,6 @@ class SpecialAbuseFilter extends SpecialPage {
$this->loadParameters( $subpage );
$out->setPageTitle( $this->msg( 'abusefilter-management' ) );
- // Are we allowed?
$this->checkPermissions();
if ( $request->getVal( 'result' ) == 'success' ) {
@@ -83,8 +88,9 @@ class SpecialAbuseFilter extends SpecialPage {
$view = 'AbuseFilterViewHistory';
$pageType = 'recentchanges';
} elseif ( count( $params ) == 2 ) {
- # Second param is a filter ID
+ // Second param is a filter ID
$view = 'AbuseFilterViewHistory';
+ $pageType = 'recentchanges';
$this->mFilter = $params[1];
} elseif ( count( $params ) == 4 && $params[2] == 'item' ) {
$this->mFilter = $params[1];
@@ -108,14 +114,18 @@ class SpecialAbuseFilter extends SpecialPage {
}
// Links at the top
- AbuseFilter::addNavigationLinks( $this->getContext(), $pageType );
+ AbuseFilter::addNavigationLinks(
+ $this->getContext(), $pageType, $this->getLinkRenderer() );
/** @var AbuseFilterView $v */
$v = new $view( $this, $params );
$v->show();
}
- function loadParameters( $subpage ) {
+ /**
+ * @param string|null $subpage
+ */
+ public function loadParameters( $subpage ) {
$filter = $subpage;
if ( !is_numeric( $filter ) && $filter != 'new' ) {
@@ -124,6 +134,9 @@ class SpecialAbuseFilter extends SpecialPage {
$this->mFilter = $filter;
}
+ /**
+ * @return string
+ */
protected function getGroupName() {
return 'wiki';
}
diff --git a/AbuseFilter/includes/special/SpecialAbuseLog.php b/AbuseFilter/includes/special/SpecialAbuseLog.php
index a8eac3a0..ab2e2cbc 100644
--- a/AbuseFilter/includes/special/SpecialAbuseLog.php
+++ b/AbuseFilter/includes/special/SpecialAbuseLog.php
@@ -6,30 +6,74 @@ class SpecialAbuseLog extends SpecialPage {
*/
protected $mSearchUser;
+ protected $mSearchPeriodStart;
+
+ protected $mSearchPeriodEnd;
+
/**
* @var Title
*/
protected $mSearchTitle;
+ /**
+ * @var string
+ */
+ protected $mSearchAction;
+
+ /**
+ * @var string
+ */
+ protected $mSearchActionTaken;
+
protected $mSearchWiki;
protected $mSearchFilter;
protected $mSearchEntries;
+ protected $mSearchImpact;
+
public function __construct() {
parent::__construct( 'AbuseLog', 'abusefilter-log' );
}
+ /**
+ * @return bool
+ */
public function doesWrites() {
return true;
}
+ /**
+ * Main routine
+ *
+ * $parameter string is converted into the $args array, which can come in
+ * three shapes:
+ *
+ * An array of size 2: only if the URL is like Special:AbuseLog/private/id
+ * where id is the log identifier. In this case, the private details of the
+ * log (e.g. IP address) will be shown.
+ *
+ * An array of size 1: either the URL is like Special:AbuseLog/id where
+ * the id is log identifier, in which case the details of the log except for
+ * private bits (e.g. IP address) are shown, or the URL is incomplete as in
+ * Special:AbuseLog/private (without speciying id), in which case a warning
+ * is shown to the user
+ *
+ * An array of size 0 when URL is like Special:AbuseLog or an array of size
+ * 1 when the URL is like Special:AbuseFilter/ (i.e. without anything after
+ * the slash). In this case, if the parameter `hide` was passed, it will be
+ * used as the identifier of the log entry that we want to hide; otherwise,
+ * the abuse logs are shown as a list, with a search form above the list.
+ *
+ * @param string $parameter URL parameters
+ */
public function execute( $parameter ) {
$out = $this->getOutput();
$request = $this->getRequest();
- AbuseFilter::addNavigationLinks( $this->getContext(), 'log' );
+ AbuseFilter::addNavigationLinks(
+ $this->getContext(), 'log', $this->getLinkRenderer() );
$this->setHeaders();
$this->outputHeader( 'abusefilter-log-summary' );
@@ -46,7 +90,6 @@ class SpecialAbuseLog extends SpecialPage {
$errors = $this->getPageTitle()->getUserPermissionsErrors(
'abusefilter-log', $this->getUser(), true, [ 'ns-specialprotected' ] );
if ( count( $errors ) ) {
- // Go away.
$out->showPermissionsErrorPage( $errors, 'abusefilter-log' );
return;
@@ -54,68 +97,153 @@ class SpecialAbuseLog extends SpecialPage {
$detailsid = $request->getIntOrNull( 'details' );
$hideid = $request->getIntOrNull( 'hide' );
+ $args = explode( '/', $parameter );
- if ( $parameter ) {
- $detailsid = $parameter;
- }
-
- if ( $detailsid ) {
- $this->showDetails( $detailsid );
- } elseif ( $hideid ) {
- $this->showHideForm( $hideid );
+ if ( count( $args ) === 2 && $args[0] === 'private' ) {
+ $this->showPrivateDetails( $args[1] );
+ } elseif ( count( $args ) === 1 && $args[0] !== '' ) {
+ if ( $args[0] === 'private' ) {
+ $out->addWikiMsg( 'abusefilter-invalid-request-noid' );
+ } else {
+ $this->showDetails( $args[0] );
+ }
} else {
- // Show the search form.
- $this->searchForm();
-
- // Show the log itself.
- $this->showList();
+ if ( $hideid ) {
+ $this->showHideForm( $hideid );
+ } else {
+ $this->searchForm();
+ $this->showList();
+ }
}
}
- function loadParameters() {
- global $wgAbuseFilterIsCentral;
-
+ /**
+ * Loads parameters from request
+ */
+ public function loadParameters() {
$request = $this->getRequest();
- $this->mSearchUser = trim( $request->getText( 'wpSearchUser' ) );
- if ( $wgAbuseFilterIsCentral ) {
+ $searchUsername = trim( $request->getText( 'wpSearchUser' ) );
+ $userTitle = Title::newFromText( $searchUsername, NS_USER );
+ $this->mSearchUser = $userTitle ? $userTitle->getText() : null;
+ if ( $this->getConfig()->get( 'AbuseFilterIsCentral' ) ) {
$this->mSearchWiki = $request->getText( 'wpSearchWiki' );
}
- $u = User::newFromName( $this->mSearchUser );
- if ( $u ) {
- $this->mSearchUser = $u->getName(); // Username normalisation
- } elseif ( IP::isIPAddress( $this->mSearchUser ) ) {
- // It's an IP
- $this->mSearchUser = IP::sanitizeIP( $this->mSearchUser );
- } else {
- $this->mSearchUser = null;
- }
-
+ $this->mSearchPeriodStart = $request->getText( 'wpSearchPeriodStart' );
+ $this->mSearchPeriodEnd = $request->getText( 'wpSearchPeriodEnd' );
$this->mSearchTitle = $request->getText( 'wpSearchTitle' );
$this->mSearchFilter = null;
+ $this->mSearchAction = $request->getText( 'wpSearchAction' );
+ $this->mSearchActionTaken = $request->getText( 'wpSearchActionTaken' );
if ( self::canSeeDetails() ) {
$this->mSearchFilter = $request->getText( 'wpSearchFilter' );
}
$this->mSearchEntries = $request->getText( 'wpSearchEntries' );
+ $this->mSearchImpact = $request->getText( 'wpSearchImpact' );
}
- function searchForm() {
- global $wgAbuseFilterIsCentral;
+ /**
+ * @return string[]
+ */
+ private function getAllActions() {
+ $config = $this->getConfig();
+ return array_unique(
+ array_merge(
+ array_keys( $config->get( 'AbuseFilterActions' ) ),
+ array_keys( $config->get( 'AbuseFilterCustomActionsHandlers' ) )
+ )
+ );
+ }
+
+ /**
+ * @return string[]
+ */
+ private function getAllFilterableActions() {
+ return [
+ 'edit',
+ 'move',
+ 'upload',
+ 'stashupload',
+ 'delete',
+ 'createaccount',
+ 'autocreateaccount',
+ ];
+ }
+ /**
+ * Builds the search form
+ */
+ public function searchForm() {
$formDescriptor = [
'SearchUser' => [
'label-message' => 'abusefilter-log-search-user',
'type' => 'user',
+ 'ipallowed' => true,
'default' => $this->mSearchUser,
],
+ 'SearchPeriodStart' => [
+ 'label-message' => 'abusefilter-test-period-start',
+ 'type' => 'datetime',
+ 'default' => $this->mSearchPeriodStart
+ ],
+ 'SearchPeriodEnd' => [
+ 'label-message' => 'abusefilter-test-period-end',
+ 'type' => 'datetime',
+ 'default' => $this->mSearchPeriodEnd
+ ],
'SearchTitle' => [
'label-message' => 'abusefilter-log-search-title',
'type' => 'title',
'default' => $this->mSearchTitle,
- ]
+ 'required' => false
+ ],
+ 'SearchImpact' => [
+ 'label-message' => 'abusefilter-log-search-impact',
+ 'type' => 'select',
+ 'options' => [
+ $this->msg( 'abusefilter-log-search-impact-all' )->text() => 0,
+ $this->msg( 'abusefilter-log-search-impact-saved' )->text() => 1,
+ $this->msg( 'abusefilter-log-search-impact-not-saved' )->text() => 2,
+ ],
+ ],
+ ];
+ $filterableActions = $this->getAllFilterableActions();
+ $actions = array_combine( $filterableActions, $filterableActions );
+ $actions[ $this->msg( 'abusefilter-log-search-action-other' )->text() ] = 'other';
+ $actions[ $this->msg( 'abusefilter-log-search-action-any' )->text() ] = 'any';
+ $formDescriptor['SearchAction'] = [
+ 'label-message' => 'abusefilter-log-search-action-label',
+ 'type' => 'select',
+ 'options' => $actions,
+ 'default' => 'any',
+ ];
+ $options = [
+ $this->msg( 'abusefilter-log-noactions' )->text() => 'noactions',
+ $this->msg( 'abusefilter-log-search-action-taken-any' )->text() => '',
];
+ foreach ( $this->getAllActions() as $action ) {
+ $key = AbuseFilter::getActionDisplay( $action );
+ $options[$key] = $action;
+ }
+ ksort( $options );
+ $formDescriptor['SearchActionTaken'] = [
+ 'label-message' => 'abusefilter-log-search-action-taken-label',
+ 'type' => 'select',
+ 'options' => $options,
+ ];
+ if ( self::canSeeHidden() ) {
+ $formDescriptor['SearchEntries'] = [
+ 'type' => 'select',
+ 'label-message' => 'abusefilter-log-search-entries-label',
+ 'options' => [
+ $this->msg( 'abusefilter-log-search-entries-all' )->text() => 0,
+ $this->msg( 'abusefilter-log-search-entries-hidden' )->text() => 1,
+ $this->msg( 'abusefilter-log-search-entries-visible' )->text() => 2,
+ ],
+ ];
+ }
if ( self::canSeeDetails() ) {
$formDescriptor['SearchFilter'] = [
'label-message' => 'abusefilter-log-search-filter',
@@ -123,7 +251,7 @@ class SpecialAbuseLog extends SpecialPage {
'default' => $this->mSearchFilter,
];
}
- if ( $wgAbuseFilterIsCentral ) {
+ if ( $this->getConfig()->get( 'AbuseFilterIsCentral' ) ) {
// Add free form input for wiki name. Would be nice to generate
// a select with unique names in the db at some point.
$formDescriptor['SearchWiki'] = [
@@ -132,19 +260,8 @@ class SpecialAbuseLog extends SpecialPage {
'default' => $this->mSearchWiki,
];
}
- if ( self::canSeeHidden() ) {
- $formDescriptor['SearchEntries'] = [
- 'type' => 'select',
- 'label-message' => 'abusefilter-log-search-entries-label',
- 'options' => [
- $this->msg( 'abusefilter-log-search-entries-all' )->text() => 0,
- $this->msg( 'abusefilter-log-search-entries-hidden' )->text() => 1,
- $this->msg( 'abusefilter-log-search-entries-visible' )->text() => 2,
- ],
- ];
- }
- $htmlForm = HTMLForm::factory( 'ooui', $formDescriptor, $this->getContext() )
+ HTMLForm::factory( 'ooui', $formDescriptor, $this->getContext() )
->setWrapperLegendMsg( 'abusefilter-log-search' )
->setSubmitTextMsg( 'abusefilter-log-search-submit' )
->setMethod( 'get' )
@@ -153,12 +270,12 @@ class SpecialAbuseLog extends SpecialPage {
}
/**
- * @param $id
- * @return mixed
+ * @param string $id
*/
- function showHideForm( $id ) {
+ public function showHideForm( $id ) {
+ $output = $this->getOutput();
if ( !$this->getUser()->isAllowed( 'abusefilter-hide-log' ) ) {
- $this->getOutput()->addWikiMsg( 'abusefilter-log-hide-forbidden' );
+ $output->addWikiMsg( 'abusefilter-log-hide-forbidden' );
return;
}
@@ -167,7 +284,7 @@ class SpecialAbuseLog extends SpecialPage {
$row = $dbr->selectRow(
[ 'abuse_filter_log', 'abuse_filter' ],
- '*',
+ 'afl_deleted',
[ 'afl_id' => $id ],
__METHOD__,
[],
@@ -178,15 +295,24 @@ class SpecialAbuseLog extends SpecialPage {
return;
}
+ $hideReasonsOther = $this->msg( 'revdelete-reasonotherlist' )->text();
+ $hideReasons = $this->msg( 'revdelete-reason-dropdown' )->inContentLanguage()->text();
+ $hideReasons = Xml::listDropDownOptions( $hideReasons, [ 'other' => $hideReasonsOther ] );
+
$formInfo = [
'logid' => [
'type' => 'info',
- 'default' => $id,
+ 'default' => (string)$id,
'label-message' => 'abusefilter-log-hide-id',
],
+ 'dropdownreason' => [
+ 'type' => 'select',
+ 'options' => $hideReasons,
+ 'label-message' => 'abusefilter-log-hide-reason'
+ ],
'reason' => [
'type' => 'text',
- 'label-message' => 'abusefilter-log-hide-reason',
+ 'label-message' => 'abusefilter-log-hide-reason-other',
],
'hidden' => [
'type' => 'toggle',
@@ -195,19 +321,24 @@ class SpecialAbuseLog extends SpecialPage {
],
];
- $form = new HTMLForm( $formInfo, $this->getContext() );
- $form->setTitle( $this->getPageTitle() );
- $form->setWrapperLegend( $this->msg( 'abusefilter-log-hide-legend' )->text() );
- $form->addHiddenField( 'hide', $id );
- $form->setSubmitCallback( [ $this, 'saveHideForm' ] );
- $form->show();
+ HTMLForm::factory( 'ooui', $formInfo, $this->getContext() )
+ ->setTitle( $this->getPageTitle() )
+ ->setWrapperLegend( $this->msg( 'abusefilter-log-hide-legend' )->text() )
+ ->addHiddenField( 'hide', $id )
+ ->setSubmitCallback( [ $this, 'saveHideForm' ] )
+ ->show();
+
+ // Show suppress log for this entry
+ $suppressLogPage = new LogPage( 'suppress' );
+ $output->addHTML( "<h2>" . $suppressLogPage->getName()->escaped() . "</h2>\n" );
+ LogEventsList::showLogExtract( $output, 'suppress', $this->getPageTitle( $id ) );
}
/**
- * @param $fields
+ * @param array $fields
* @return bool
*/
- function saveHideForm( $fields ) {
+ public function saveHideForm( $fields ) {
$logid = $this->getRequest()->getVal( 'hide' );
$dbw = wfGetDB( DB_MASTER );
@@ -219,17 +350,30 @@ class SpecialAbuseLog extends SpecialPage {
__METHOD__
);
- $logPage = new LogPage( 'suppress' );
- $action = $fields['hidden'] ? 'hide-afl' : 'unhide-afl';
+ $reason = $fields['dropdownreason'];
+ if ( $reason === 'other' ) {
+ $reason = $fields['reason'];
+ } elseif ( $fields['reason'] !== '' ) {
+ $reason .=
+ $this->msg( 'colon-separator' )->inContentLanguage()->text() . $fields['reason'];
+ }
- $logPage->addEntry( $action, $this->getPageTitle( $logid ), $fields['reason'] );
+ $action = $fields['hidden'] ? 'hide-afl' : 'unhide-afl';
+ $logEntry = new ManualLogEntry( 'suppress', $action );
+ $logEntry->setPerformer( $this->getUser() );
+ $logEntry->setTarget( $this->getPageTitle( $logid ) );
+ $logEntry->setComment( $reason );
+ $logEntry->insert();
$this->getOutput()->redirect( SpecialPage::getTitleFor( 'AbuseLog' )->getFullURL() );
return true;
}
- function showList() {
+ /**
+ * Shows the results list
+ */
+ public function showList() {
$out = $this->getOutput();
// Generate conditions list.
@@ -247,6 +391,17 @@ class SpecialAbuseLog extends SpecialPage {
}
}
+ $dbr = wfGetDB( DB_REPLICA );
+ if ( $this->mSearchPeriodStart ) {
+ $conds[] = 'afl_timestamp >= ' .
+ $dbr->addQuotes( $dbr->timestamp( strtotime( $this->mSearchPeriodStart ) ) );
+ }
+
+ if ( $this->mSearchPeriodEnd ) {
+ $conds[] = 'afl_timestamp <= ' .
+ $dbr->addQuotes( $dbr->timestamp( strtotime( $this->mSearchPeriodEnd ) ) );
+ }
+
if ( $this->mSearchWiki ) {
if ( $this->mSearchWiki == wfWikiID() ) {
$conds['afl_wiki'] = null;
@@ -291,7 +446,47 @@ class SpecialAbuseLog extends SpecialPage {
if ( $this->mSearchEntries == '1' ) {
$conds['afl_deleted'] = 1;
} elseif ( $this->mSearchEntries == '2' ) {
- $conds[] = self::getNotDeletedCond( wfGetDB( DB_REPLICA ) );
+ $conds[] = self::getNotDeletedCond( $dbr );
+ }
+ }
+
+ if ( in_array( $this->mSearchImpact, [ '1', '2' ] ) ) {
+ $unsuccessfulActionConds = $dbr->makeList( [
+ 'afl_rev_id' => null,
+ 'afl_log_id' => null,
+ ], LIST_AND );
+ if ( $this->mSearchImpact == '1' ) {
+ $conds[] = "NOT ( $unsuccessfulActionConds )";
+ } else {
+ $conds[] = $unsuccessfulActionConds;
+ }
+ }
+
+ if ( $this->mSearchActionTaken ) {
+ if ( in_array( $this->mSearchActionTaken, $this->getAllActions() ) ) {
+ $list = [ 'afl_actions' => $this->mSearchActionTaken ];
+ $list[] = 'afl_actions' . $dbr->buildLike(
+ $this->mSearchActionTaken, ',', $dbr->anyString() );
+ $list[] = 'afl_actions' . $dbr->buildLike(
+ $dbr->anyString(), ',', $this->mSearchActionTaken );
+ $list[] = 'afl_actions' . $dbr->buildLike(
+ $dbr->anyString(),
+ ',', $this->mSearchActionTaken, ',',
+ $dbr->anyString()
+ );
+ $conds[] = $dbr->makeList( $list, LIST_OR );
+ } elseif ( $this->mSearchActionTaken === 'noactions' ) {
+ $conds['afl_actions'] = '';
+ }
+ }
+
+ if ( $this->mSearchAction ) {
+ $filterableActions = $this->getAllFilterableActions();
+ if ( in_array( $this->mSearchAction, $filterableActions ) ) {
+ $conds['afl_action'] = $this->mSearchAction;
+ } elseif ( $this->mSearchAction === 'other' ) {
+ $list = $dbr->makeList( [ 'afl_action' => $filterableActions ], LIST_OR );
+ $conds[] = "NOT ( $list )";
}
}
@@ -308,10 +503,9 @@ class SpecialAbuseLog extends SpecialPage {
}
/**
- * @param $id
- * @return mixed
+ * @param string $id
*/
- function showDetails( $id ) {
+ public function showDetails( $id ) {
$out = $this->getOutput();
$dbr = wfGetDB( DB_REPLICA );
@@ -343,7 +537,7 @@ class SpecialAbuseLog extends SpecialPage {
return;
}
- if ( self::isHidden( $row ) && !self::canSeeHidden() ) {
+ if ( self::isHidden( $row ) === true && !self::canSeeHidden() ) {
$out->addWikiMsg( 'abusefilter-log-details-hidden' );
return;
@@ -359,7 +553,9 @@ class SpecialAbuseLog extends SpecialPage {
$output = Xml::element(
'legend',
null,
- $this->msg( 'abusefilter-log-details-legend', $id )->text()
+ $this->msg( 'abusefilter-log-details-legend' )
+ ->numParams( $id )
+ ->text()
);
$output .= Xml::tags( 'p', null, $this->formatRow( $row, false ) );
@@ -376,8 +572,10 @@ class SpecialAbuseLog extends SpecialPage {
$diffEngine->showDiffStyle();
- $formattedDiff = $diffEngine->generateTextDiffBody( $old_wikitext, $new_wikitext );
- $formattedDiff = $diffEngine->addHeader( $formattedDiff, '', '' );
+ $formattedDiff = $diffEngine->addHeader(
+ $diffEngine->generateTextDiffBody( $old_wikitext, $new_wikitext ),
+ '', ''
+ );
$output .=
Xml::tags(
@@ -395,45 +593,276 @@ class SpecialAbuseLog extends SpecialPage {
$output .= AbuseFilter::buildVarDumpTable( $vars, $this->getContext() );
if ( self::canSeePrivate() ) {
- // Private stuff, like IPs.
- $header =
- Xml::element( 'th', null, $this->msg( 'abusefilter-log-details-var' )->text() ) .
- Xml::element( 'th', null, $this->msg( 'abusefilter-log-details-val' )->text() );
- $output .= Xml::element( 'h3', null, $this->msg( 'abusefilter-log-details-private' )->text() );
- $output .=
- Xml::openElement( 'table',
- [
- 'class' => 'wikitable mw-abuselog-private',
- 'style' => 'width: 80%;'
- ]
+ $formDescriptor = [
+ 'Reason' => [
+ 'label-message' => 'abusefilter-view-private-reason',
+ 'type' => 'text',
+ 'size' => 45,
+ ],
+ ];
+
+ $htmlForm = HTMLForm::factory( 'ooui', $formDescriptor, $this->getContext() );
+ $htmlForm->setWrapperLegendMsg( 'abusefilter-view-private' )
+ ->setAction( $this->getPageTitle( 'private/' . $id )->getLocalURL() )
+ ->setSubmitTextMsg( 'abusefilter-view-private-submit' )
+ ->setMethod( 'post' )
+ ->prepareForm();
+
+ $output .= $htmlForm->getHTML( false );
+ }
+
+ $out->addHTML( Xml::tags( 'fieldset', null, $output ) );
+ }
+
+ /**
+ * @param string $id
+ * @return void
+ */
+ public function showPrivateDetails( $id ) {
+ $lang = $this->getLanguage();
+ $out = $this->getOutput();
+ $request = $this->getRequest();
+
+ $dbr = wfGetDB( DB_REPLICA );
+
+ $reason = $request->getText( 'wpReason' );
+
+ // Make sure it is a valid request
+ $token = $request->getVal( 'wpEditToken' );
+ if ( !$request->wasPosted() || !$this->getUser()->matchEditToken( $token ) ) {
+ $out->addHTML(
+ Xml::tags(
+ 'p',
+ null,
+ Html::errorBox( $this->msg( 'abusefilter-invalid-request' )->params( $id )->parse() )
+ )
+ );
+
+ return;
+ }
+
+ if ( !$this->checkReason( $reason ) ) {
+ $out->addWikiMsg( 'abusefilter-noreason' );
+ $this->showDetails( $id );
+ return;
+ }
+
+ $row = $dbr->selectRow(
+ [ 'abuse_filter_log', 'abuse_filter' ],
+ [ 'afl_id', 'afl_filter', 'afl_user_text', 'afl_timestamp', 'afl_ip', 'af_id',
+ 'af_public_comments', 'af_hidden' ],
+ [ 'afl_id' => $id ],
+ __METHOD__,
+ [],
+ [ 'abuse_filter' => [ 'LEFT JOIN', 'af_id=afl_filter' ] ]
+ );
+
+ if ( !$row ) {
+ $out->addWikiMsg( 'abusefilter-log-nonexistent' );
+
+ return;
+ }
+
+ if ( AbuseFilter::decodeGlobalName( $row->afl_filter ) ) {
+ $filter_hidden = null;
+ } else {
+ $filter_hidden = $row->af_hidden;
+ }
+
+ if ( !self::canSeeDetails( $row->afl_filter, $filter_hidden ) ) {
+ $out->addWikiMsg( 'abusefilter-log-cannot-see-details' );
+
+ return;
+ }
+
+ if ( !self::canSeePrivate() ) {
+ $out->addWikiMsg( 'abusefilter-log-cannot-see-private-details' );
+
+ return;
+ }
+
+ // Log accessing private details
+ if ( $this->getConfig()->get( 'AbuseFilterPrivateLog' ) ) {
+ $user = $this->getUser();
+ self::addLogEntry( $id, $reason, $user );
+ }
+
+ // Show private details (IP).
+ $output = Xml::element(
+ 'legend',
+ null,
+ $this->msg( 'abusefilter-log-details-private' )->text()
+ );
+
+ $header =
+ Xml::element( 'th', null, $this->msg( 'abusefilter-log-details-var' )->text() ) .
+ Xml::element( 'th', null, $this->msg( 'abusefilter-log-details-val' )->text() );
+
+ $output .=
+ Xml::openElement( 'table',
+ [
+ 'class' => 'wikitable mw-abuselog-private',
+ 'style' => 'width: 80%;'
+ ]
+ ) .
+ Xml::openElement( 'tbody' );
+ $output .= $header;
+
+ // Log ID
+ $linkRenderer = $this->getLinkRenderer();
+ $output .=
+ Xml::tags( 'tr', null,
+ Xml::element( 'td',
+ [ 'style' => 'width: 30%;' ],
+ $this->msg( 'abusefilter-log-details-id' )->text()
) .
- Xml::openElement( 'tbody' );
- $output .= $header;
+ Xml::openElement( 'td' ) .
+ $linkRenderer->makeKnownLink(
+ $this->getPageTitle( $row->afl_id ),
+ $lang->formatNum( $row->afl_id )
+ ) .
+ Xml::closeElement( 'td' )
+ );
+
+ // Timestamp
+ $output .=
+ Xml::tags( 'tr', null,
+ Xml::element( 'td',
+ [ 'style' => 'width: 30%;' ],
+ $this->msg( 'abusefilter-edit-builder-vars-timestamp-expanded' )->text()
+ ) .
+ Xml::element( 'td',
+ null,
+ $lang->timeanddate( $row->afl_timestamp, true )
+ )
+ );
+
+ // User
+ $output .=
+ Xml::tags( 'tr', null,
+ Xml::element( 'td',
+ [ 'style' => 'width: 30%;' ],
+ $this->msg( 'abusefilter-edit-builder-vars-user-name' )->text()
+ ) .
+ Xml::element( 'td',
+ null,
+ $row->afl_user_text
+ )
+ );
+
+ // Filter ID
+ $output .=
+ Xml::tags( 'tr', null,
+ Xml::element( 'td',
+ [ 'style' => 'width: 30%;' ],
+ $this->msg( 'abusefilter-list-id' )->text()
+ ) .
+ Xml::openElement( 'td' ) .
+ $linkRenderer->makeKnownLink(
+ SpecialPage::getTitleFor( 'AbuseFilter', $row->af_id ),
+ $lang->formatNum( $row->af_id )
+ ) .
+ Xml::closeElement( 'td' )
+ );
+
+ // Filter description
+ $output .=
+ Xml::tags( 'tr', null,
+ Xml::element( 'td',
+ [ 'style' => 'width: 30%;' ],
+ $this->msg( 'abusefilter-list-public' )->text()
+ ) .
+ Xml::element( 'td',
+ null,
+ $row->af_public_comments
+ )
+ );
- // IP address
+ // IP address
+ if ( $row->afl_ip !== '' ) {
+ if ( ExtensionRegistry::getInstance()->isLoaded( 'CheckUser' ) &&
+ $this->getUser()->isAllowed( 'checkuser' ) ) {
+ $CULink = '&nbsp;&middot;&nbsp;' . $linkRenderer->makeKnownLink(
+ SpecialPage::getTitleFor(
+ 'CheckUser',
+ $row->afl_ip
+ ),
+ $this->msg( 'abusefilter-log-details-checkuser' )->text()
+ );
+ } else {
+ $CULink = '';
+ }
$output .=
Xml::tags( 'tr', null,
Xml::element( 'td',
[ 'style' => 'width: 30%;' ],
$this->msg( 'abusefilter-log-details-ip' )->text()
) .
- Xml::element( 'td', null, $row->afl_ip )
+ Xml::tags(
+ 'td',
+ null,
+ self::getUserLinks( 0, $row->afl_ip ) . $CULink
+ )
+ );
+ } else {
+ $output .=
+ Xml::tags( 'tr', null,
+ Xml::element( 'td',
+ [ 'style' => 'width: 30%;' ],
+ $this->msg( 'abusefilter-log-details-ip' )->text()
+ ) .
+ Xml::element(
+ 'td',
+ null,
+ $this->msg( 'abusefilter-log-ip-not-available' )->text()
+ )
);
-
- $output .= Xml::closeElement( 'tbody' ) . Xml::closeElement( 'table' );
}
+ $output .= Xml::closeElement( 'tbody' ) . Xml::closeElement( 'table' );
+
$output = Xml::tags( 'fieldset', null, $output );
$out->addHTML( $output );
}
/**
- * @param $filter_id null
- * @param $filter_hidden null
+ * If specifying a reason for viewing private details of abuse log is required
+ * then it makes sure that a reason is provided.
+ *
+ * @param string $reason
* @return bool
*/
- static function canSeeDetails( $filter_id = null, $filter_hidden = null ) {
+ protected function checkReason( $reason ) {
+ return ( !$this->getConfig()->get( 'AbuseFilterForceSummary' ) || strlen( $reason ) > 0 );
+ }
+
+ /**
+ * @param int $logID int The ID of the AbuseFilter log that was accessed
+ * @param string $reason The reason provided for accessing private details
+ * @param User $user The user who accessed the private details
+ * @return void
+ */
+ public static function addLogEntry( $logID, $reason, $user ) {
+ $target = self::getTitleFor( 'AbuseLog', $logID );
+
+ $logEntry = new ManualLogEntry( 'abusefilterprivatedetails', 'access' );
+ $logEntry->setPerformer( $user );
+ $logEntry->setTarget( $target );
+ $logEntry->setParameters( [
+ '4::logid' => $logID,
+ ] );
+ $logEntry->setComment( $reason );
+
+ $logEntry->insert();
+ }
+
+ /**
+ * @param string|null $filter_id
+ * @param bool|int|null $filter_hidden
+ * @return bool
+ */
+ public static function canSeeDetails( $filter_id = null, $filter_hidden = null ) {
global $wgUser;
if ( $filter_id !== null ) {
@@ -453,7 +882,7 @@ class SpecialAbuseLog extends SpecialPage {
/**
* @return bool
*/
- static function canSeePrivate() {
+ public static function canSeePrivate() {
global $wgUser;
return $wgUser->isAllowed( 'abusefilter-private' );
@@ -462,18 +891,18 @@ class SpecialAbuseLog extends SpecialPage {
/**
* @return bool
*/
- static function canSeeHidden() {
+ public static function canSeeHidden() {
global $wgUser;
return $wgUser->isAllowed( 'abusefilter-hidden-log' );
}
/**
- * @param $row
- * @param $isListItem bool
+ * @param stdClass $row
+ * @param bool $isListItem
* @return String
*/
- function formatRow( $row, $isListItem = true ) {
+ public function formatRow( $row, $isListItem = true ) {
$user = $this->getUser();
$lang = $this->getLanguage();
@@ -484,7 +913,7 @@ class SpecialAbuseLog extends SpecialPage {
$diffLink = false;
$isHidden = self::isHidden( $row );
- if ( !self::canSeeHidden() && $isHidden ) {
+ if ( !self::canSeeHidden() && $isHidden === true ) {
return '';
}
@@ -524,7 +953,7 @@ class SpecialAbuseLog extends SpecialPage {
$actions_taken = $row->afl_actions;
if ( !strlen( trim( $actions_taken ) ) ) {
- $actions_taken = $this->msg( 'abusefilter-log-noactions' )->text();
+ $actions_taken = $this->msg( 'abusefilter-log-noactions' )->escaped();
} else {
$actions = explode( ',', $actions_taken );
$displayActions = [];
@@ -539,11 +968,12 @@ class SpecialAbuseLog extends SpecialPage {
if ( $globalIndex ) {
// Pull global filter description
- $parsed_comments =
- $this->getOutput()->parseInline( AbuseFilter::getGlobalFilterDescription( $globalIndex ) );
+ $escaped_comments = Sanitizer::escapeHtmlAllowEntities(
+ AbuseFilter::getGlobalFilterDescription( $globalIndex ) );
$filter_hidden = null;
} else {
- $parsed_comments = $this->getOutput()->parseInline( $row->af_public_comments );
+ $escaped_comments = Sanitizer::escapeHtmlAllowEntities(
+ $row->af_public_comments );
$filter_hidden = $row->af_hidden;
}
@@ -579,11 +1009,10 @@ class SpecialAbuseLog extends SpecialPage {
}
if ( $globalIndex ) {
- global $wgAbuseFilterCentralDB;
- $globalURL =
- WikiMap::getForeignURL( $wgAbuseFilterCentralDB,
- 'Special:AbuseFilter/' . $globalIndex );
-
+ $globalURL = WikiMap::getForeignURL(
+ $this->getConfig()->get( 'AbuseFilterCentralDB' ),
+ 'Special:AbuseFilter/' . $globalIndex
+ );
$linkText = $this->msg( 'abusefilter-log-detailedentry-global' )
->numParams( $globalIndex )->escaped();
$filterLink = Linker::makeExternalLink( $globalURL, $linkText );
@@ -600,7 +1029,7 @@ class SpecialAbuseLog extends SpecialPage {
$row->afl_action,
$pageLink,
$actions_taken,
- $parsed_comments,
+ $escaped_comments,
$lang->pipeList( $actionLinks )
)->params( $row->afl_user_text )->parse();
} else {
@@ -615,8 +1044,9 @@ class SpecialAbuseLog extends SpecialPage {
$row->afl_action,
$pageLink,
$actions_taken,
- $parsed_comments,
- $diffLink // Passing $7 to 'abusefilter-log-entry' will do nothing, as it's not used.
+ $escaped_comments,
+ // Passing $7 to 'abusefilter-log-entry' will do nothing, as it's not used.
+ $diffLink
)->params( $row->afl_user_text )->parse();
}
@@ -636,6 +1066,11 @@ class SpecialAbuseLog extends SpecialPage {
}
}
+ /**
+ * @param int $userId
+ * @param string $userName
+ * @return string
+ */
protected static function getUserLinks( $userId, $userName ) {
static $cache = [];
@@ -648,7 +1083,7 @@ class SpecialAbuseLog extends SpecialPage {
}
/**
- * @param $db DatabaseBase
+ * @param \Wikimedia\Rdbms\IDatabase $db
* @return string
*/
public static function getNotDeletedCond( $db ) {
@@ -665,12 +1100,17 @@ class SpecialAbuseLog extends SpecialPage {
/**
* Given a log entry row, decides whether or not it can be viewed by the public.
*
- * @param $row stdClass The abuse_filter_log row object.
+ * @param stdClass $row The abuse_filter_log row object.
*
* @return bool|string true if the item is explicitly hidden, false if it is not.
* The string 'implicit' if it is hidden because the corresponding revision is hidden.
*/
public static function isHidden( $row ) {
+ // First, check if the entry is hidden. Since this is an oversight-level deletion,
+ // it's more important than the associated revision being deleted.
+ if ( $row->afl_deleted ) {
+ return true;
+ }
if ( $row->afl_rev_id ) {
$revision = Revision::newFromId( $row->afl_rev_id );
if ( $revision && $revision->getVisibility() != 0 ) {
@@ -678,87 +1118,13 @@ class SpecialAbuseLog extends SpecialPage {
}
}
- return (bool)$row->afl_deleted;
- }
-
- protected function getGroupName() {
- return 'changes';
- }
-}
-
-class AbuseLogPager extends ReverseChronologicalPager {
- /**
- * @var SpecialAbuseLog
- */
- public $mForm;
-
- /**
- * @var array
- */
- public $mConds;
-
- /**
- * @param SpecialAbuseLog $form
- * @param array $conds
- * @param bool $details
- */
- function __construct( $form, $conds = [], $details = false ) {
- $this->mForm = $form;
- $this->mConds = $conds;
- parent::__construct();
- }
-
- function formatRow( $row ) {
- return $this->mForm->formatRow( $row );
- }
-
- function getQueryInfo() {
- $conds = $this->mConds;
-
- $info = [
- 'tables' => [ 'abuse_filter_log', 'abuse_filter' ],
- 'fields' => '*',
- 'conds' => $conds,
- 'join_conds' =>
- [ 'abuse_filter' =>
- [
- 'LEFT JOIN',
- 'af_id=afl_filter',
- ],
- ],
- ];
-
- if ( !$this->mForm->canSeeHidden() ) {
- $db = $this->mDb;
- $info['conds'][] = SpecialAbuseLog::getNotDeletedCond( $db );
- }
-
- return $info;
+ return false;
}
/**
- * @param ResultWrapper $result
+ * @return string
*/
- protected function preprocessResults( $result ) {
- if ( $this->getNumRows() === 0 ) {
- return;
- }
-
- $lb = new LinkBatch();
- $lb->setCaller( __METHOD__ );
- foreach ( $result as $row ) {
- // Only for local wiki results
- if ( !$row->afl_wiki ) {
- $lb->add( $row->afl_namespace, $row->afl_title );
- $lb->add( NS_USER, $row->afl_user );
- $lb->add( NS_USER_TALK, $row->afl_user_text );
- }
- }
- $lb->execute();
- $result->seek( 0 );
- }
-
- function getIndexField() {
- return 'afl_timestamp';
+ protected function getGroupName() {
+ return 'changes';
}
}