diff options
Diffstat (limited to 'AbuseFilter/includes/pagers/AbuseFilterPager.php')
-rw-r--r-- | AbuseFilter/includes/pagers/AbuseFilterPager.php | 247 |
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' ]; + } } |