summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
Diffstat (limited to 'AbuseFilter/includes/pagers/AbuseFilterPager.php')
-rw-r--r--AbuseFilter/includes/pagers/AbuseFilterPager.php247
1 files changed, 172 insertions, 75 deletions
diff --git a/AbuseFilter/includes/pagers/AbuseFilterPager.php b/AbuseFilter/includes/pagers/AbuseFilterPager.php
index 2092b850..482f5d78 100644
--- a/AbuseFilter/includes/pagers/AbuseFilterPager.php
+++ b/AbuseFilter/includes/pagers/AbuseFilterPager.php
@@ -1,6 +1,7 @@
<?php
use MediaWiki\Linker\LinkRenderer;
+use Wikimedia\AtEase\AtEase;
/**
* Class to build paginated filter list
@@ -8,24 +9,43 @@ use MediaWiki\Linker\LinkRenderer;
class AbuseFilterPager extends TablePager {
/**
- * @var LinkRenderer
+ * @var AbuseFilterViewList The associated page
*/
- protected $linkRenderer;
-
- public $mPage, $mConds, $mQuery;
+ public $mPage;
+ /**
+ * @var array Query WHERE conditions
+ */
+ public $mConds;
+ /**
+ * @var string The pattern being searched
+ */
+ private $mSearchPattern;
+ /**
+ * @var string The pattern search mode (LIKE, RLIKE or IRLIKE)
+ */
+ private $mSearchMode;
/**
* @param AbuseFilterViewList $page
* @param array $conds
* @param LinkRenderer $linkRenderer
- * @param array $query
+ * @param string $searchPattern Empty string if no pattern was specified
+ * @param string $searchMode
*/
- public function __construct( $page, $conds, $linkRenderer, $query ) {
+ public function __construct(
+ AbuseFilterViewList $page,
+ $conds,
+ LinkRenderer $linkRenderer,
+ string $searchPattern,
+ string $searchMode
+ ) {
$this->mPage = $page;
$this->mConds = $conds;
- $this->linkRenderer = $linkRenderer;
- $this->mQuery = $query;
- parent::__construct( $this->mPage->getContext() );
+ $this->mSearchPattern = $searchPattern;
+ $this->mSearchMode = $searchMode;
+ // needs to be at the end, some attributes are needed by methods
+ // called from ancestors' constructors
+ parent::__construct( $page->getContext(), $linkRenderer );
}
/**
@@ -56,6 +76,61 @@ class AbuseFilterPager extends TablePager {
}
/**
+ * @inheritDoc
+ * This is the same as the parent implementation if no search pattern was specified.
+ * Otherwise, it does a query with no limit and then slices the results à la ContribsPager.
+ */
+ public function reallyDoQuery( $offset, $limit, $order ) {
+ if ( !strlen( $this->mSearchPattern ) ) {
+ return parent::reallyDoQuery( $offset, $limit, $order );
+ }
+
+ list( $tables, $fields, $conds, $fname, $options, $join_conds ) =
+ $this->buildQueryInfo( $offset, $limit, $order );
+
+ unset( $options['LIMIT'] );
+ $res = $this->mDb->select( $tables, $fields, $conds, $fname, $options, $join_conds );
+
+ $filtered = [];
+ foreach ( $res as $row ) {
+ if ( $this->matchesPattern( $row->af_pattern ) ) {
+ $filtered[ $row->af_id ] = $row;
+ }
+ }
+
+ // sort results and enforce limit like ContribsPager
+ if ( $order === self::QUERY_ASCENDING ) {
+ ksort( $filtered );
+ } else {
+ krsort( $filtered );
+ }
+ $filtered = array_slice( $filtered, 0, $limit );
+ $filtered = array_values( $filtered );
+ return new FakeResultWrapper( $filtered );
+ }
+
+ /**
+ * Check whether $subject matches the given $pattern.
+ *
+ * @param string $subject
+ * @return bool
+ * @throws LogicException
+ */
+ private function matchesPattern( $subject ) {
+ $pattern = $this->mSearchPattern;
+ switch ( $this->mSearchMode ) {
+ case 'RLIKE':
+ return (bool)preg_match( "/$pattern/u", $subject );
+ case 'IRLIKE':
+ return (bool)preg_match( "/$pattern/ui", $subject );
+ case 'LIKE':
+ return mb_stripos( $subject, $pattern ) !== false;
+ default:
+ throw new LogicException( "Unknown search type {$this->mSearchMode}" );
+ }
+ }
+
+ /**
* @see Pager::getFieldNames()
* @return array
*/
@@ -75,11 +150,12 @@ class AbuseFilterPager extends TablePager {
'af_hidden' => 'abusefilter-list-visibility',
];
- if ( $this->mPage->getUser()->isAllowed( 'abusefilter-log-detail' ) ) {
+ $user = $this->getUser();
+ if ( SpecialAbuseLog::canSeeDetails( $user ) ) {
$headers['af_hit_count'] = 'abusefilter-list-hitcount';
}
- if ( AbuseFilterView::canViewPrivate() && !empty( $this->mQuery[0] ) ) {
+ if ( AbuseFilter::canViewPrivate( $user ) && $this->mSearchPattern !== '' ) {
$headers['af_pattern'] = 'abusefilter-list-pattern';
}
@@ -101,79 +177,29 @@ class AbuseFilterPager extends TablePager {
*/
public function formatValue( $name, $value ) {
$lang = $this->getLanguage();
+ $user = $this->getUser();
+ $linkRenderer = $this->getLinkRenderer();
$row = $this->mCurrentRow;
switch ( $name ) {
case 'af_id':
- return $this->linkRenderer->makeLink(
- SpecialPage::getTitleFor( 'AbuseFilter', intval( $value ) ),
+ return $linkRenderer->makeLink(
+ SpecialPage::getTitleFor( 'AbuseFilter', $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;
+ return $this->getHighlightedPattern( $row );
case 'af_public_comments':
- return $this->linkRenderer->makeLink(
- SpecialPage::getTitleFor( 'AbuseFilter', intval( $row->af_id ) ),
+ return $linkRenderer->makeLink(
+ SpecialPage::getTitleFor( 'AbuseFilter', $row->af_id ),
$value
);
case 'af_actions':
$actions = explode( ',', $value );
$displayActions = [];
+ $context = $this->getContext();
foreach ( $actions as $action ) {
- $displayActions[] = AbuseFilter::getActionDisplay( $action );
+ $displayActions[] = AbuseFilter::getActionDisplay( $action, $context );
}
return $lang->commaList( $displayActions );
case 'af_enabled':
@@ -198,10 +224,13 @@ class AbuseFilterPager extends TablePager {
$msg = $value ? 'abusefilter-hidden' : 'abusefilter-unhidden';
return $this->msg( $msg )->parse();
case 'af_hit_count':
- if ( SpecialAbuseLog::canSeeDetails( $row->af_id, $row->af_hidden ) ) {
+ // Global here is used to determine whether the log entry is for an external, global
+ // filter, but all filters shown on Special:AbuseFilter are local.
+ $global = false;
+ if ( SpecialAbuseLog::canSeeDetails( $user, $row->af_id, $global, $row->af_hidden ) ) {
$count_display = $this->msg( 'abusefilter-hitcount' )
->numParams( $value )->text();
- $link = $this->linkRenderer->makeKnownLink(
+ $link = $linkRenderer->makeKnownLink(
SpecialPage::getTitleFor( 'AbuseLog' ),
$count_display,
[],
@@ -239,7 +268,7 @@ class AbuseFilterPager extends TablePager {
)
)->params(
wfEscapeWikiText( $row->af_user_text )
- )->parse();
+ )->parse();
case 'af_group':
return AbuseFilter::nameGroup( $value );
default:
@@ -248,6 +277,65 @@ class AbuseFilterPager extends TablePager {
}
/**
+ * Get the filter pattern with <b> elements surrounding the searched pattern
+ *
+ * @param stdClass $row
+ * @return string
+ */
+ private function getHighlightedPattern( stdClass $row ) {
+ $maxLen = 50;
+ if ( $this->mSearchMode === 'LIKE' ) {
+ $position = mb_stripos( $row->af_pattern, $this->mSearchPattern );
+ $length = mb_strlen( $this->mSearchPattern );
+ } else {
+ $regex = '/' . $this->mSearchPattern . '/u';
+ if ( $this->mSearchMode === 'IRLIKE' ) {
+ $regex .= 'i';
+ }
+
+ $matches = [];
+ AtEase::suppressWarnings();
+ $check = preg_match(
+ $regex,
+ $row->af_pattern,
+ $matches
+ );
+ AtEase::restoreWarnings();
+ // This may happen in case of catastrophic backtracking, or regexps matching
+ // the empty string.
+ if ( $check === false || strlen( $matches[0] ) === 0 ) {
+ return htmlspecialchars( mb_substr( $row->af_pattern, 0, 50 ) );
+ }
+
+ $length = mb_strlen( $matches[0] );
+ $position = mb_strpos( $row->af_pattern, $matches[0] );
+ }
+
+ $remaining = $maxLen - $length;
+ if ( $remaining <= 0 ) {
+ $pattern = '<b>' .
+ htmlspecialchars( mb_substr( $row->af_pattern, $position, $maxLen ) ) .
+ '</b>';
+ } else {
+ // Center the snippet on the matched string
+ $minoffset = max( $position - round( $remaining / 2 ), 0 );
+ $pattern = mb_substr( $row->af_pattern, $minoffset, $maxLen );
+ $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;
+ }
+
+ /**
* @return string
*/
public function getDefaultSort() {
@@ -258,7 +346,7 @@ class AbuseFilterPager extends TablePager {
* @return string
*/
public function getTableClass() {
- return 'TablePager mw-abusefilter-list-scrollable';
+ return parent::getTableClass() . ' mw-abusefilter-list-scrollable';
}
/**
@@ -288,9 +376,18 @@ class AbuseFilterPager extends TablePager {
'af_hidden',
'af_group',
];
- if ( $this->mPage->getUser()->isAllowed( 'abusefilter-log-detail' ) ) {
+ if ( SpecialAbuseLog::canSeeDetails( $this->getUser() ) ) {
$sortable_fields[] = 'af_hit_count';
+ $sortable_fields[] = 'af_public_comments';
}
return in_array( $name, $sortable_fields );
}
+
+ /**
+ * @see IndexPager::getExtraSortFields
+ * @return array
+ */
+ public function getExtraSortFields() {
+ return [ 'af_enabled' => 'af_deleted' ];
+ }
}