diff options
Diffstat (limited to 'www/wiki/extensions/AbuseFilter/includes/pagers')
5 files changed, 685 insertions, 0 deletions
diff --git a/www/wiki/extensions/AbuseFilter/includes/pagers/AbuseFilterExaminePager.php b/www/wiki/extensions/AbuseFilter/includes/pagers/AbuseFilterExaminePager.php new file mode 100644 index 00000000..495bd4f8 --- /dev/null +++ b/www/wiki/extensions/AbuseFilter/includes/pagers/AbuseFilterExaminePager.php @@ -0,0 +1,72 @@ +<?php + +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 + * @return array + */ + 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 ); + } + + function getIndexField() { + return 'rc_id'; + } + + function getTitle() { + return $this->mPage->getTitle( 'examine' ); + } + + function getEmptyBody() { + return $this->msg( 'abusefilter-examine-noresults' )->parseAsBlock(); + } +} diff --git a/www/wiki/extensions/AbuseFilter/includes/pagers/AbuseFilterHistoryPager.php b/www/wiki/extensions/AbuseFilter/includes/pagers/AbuseFilterHistoryPager.php new file mode 100644 index 00000000..375b940e --- /dev/null +++ b/www/wiki/extensions/AbuseFilter/includes/pagers/AbuseFilterHistoryPager.php @@ -0,0 +1,204 @@ +<?php + +class AbuseFilterHistoryPager extends TablePager { + + protected $linkRenderer; + /** + * @param string $filter + * @param ContextSource $page + * @param string $user 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 ); + } +} diff --git a/www/wiki/extensions/AbuseFilter/includes/pagers/AbuseFilterPager.php b/www/wiki/extensions/AbuseFilter/includes/pagers/AbuseFilterPager.php new file mode 100644 index 00000000..f4e62ad7 --- /dev/null +++ b/www/wiki/extensions/AbuseFilter/includes/pagers/AbuseFilterPager.php @@ -0,0 +1,260 @@ +<?php + +/** + * Class to build paginated filter list + */ +class AbuseFilterPager extends TablePager { + + /** + * @var \MediaWiki\Linker\LinkRenderer + */ + protected $linkRenderer; + + function __construct( $page, $conds, $linkRenderer, $query ) { + $this->mPage = $page; + $this->mConds = $conds; + $this->linkRenderer = $linkRenderer; + $this->mQuery = $query; + parent::__construct( $this->mPage->getContext() ); + } + + function getQueryInfo() { + return [ + 'tables' => [ 'abuse_filter' ], + 'fields' => [ + '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', + ], + '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'; + } + + if ( AbuseFilterView::canViewPrivate() && !empty( $this->mQuery[0] ) ) { + $headers['af_pattern'] = 'abusefilter-list-pattern'; + } + + 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_pattern': + if ( $this->mQuery[1] === 'LIKE' ) { + $position = mb_strpos( + strtolower( $row->af_pattern ), + strtolower( $this->mQuery[0] ), + 0, + 'UTF8' + ); + 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, 'UTF8' ) ); + } + $length = mb_strlen( $this->mQuery[0], 'UTF8' ); + } elseif ( $this->mQuery[1] === 'RLIKE' ) { + Wikimedia\suppressWarnings(); + $check = preg_match( + '/' . $this->mQuery[0] . '/', + $row->af_pattern, + $matches, + PREG_OFFSET_CAPTURE + ); + Wikimedia\restoreWarnings(); + // This may happen in case of catastrophic backtracking + if ( $check === false ) { + return htmlspecialchars( mb_substr( $row->af_pattern, 0, 50, 'UTF8' ) ); + } + $length = mb_strlen( $matches[0][0], 'UTF8' ); + $position = $matches[0][1]; + } elseif ( $this->mQuery[1] === 'IRLIKE' ) { + Wikimedia\suppressWarnings(); + $check = preg_match( + '/' . $this->mQuery[0] . '/i', + $row->af_pattern, + $matches, + PREG_OFFSET_CAPTURE + ); + Wikimedia\restoreWarnings(); + // This may happen in case of catastrophic backtracking + if ( $check === false ) { + return htmlspecialchars( mb_substr( $row->af_pattern, 0, 50, 'UTF8' ) ); + } + $length = mb_strlen( $matches[0][0], 'UTF8' ); + $position = $matches[0][1]; + } + $remaining = 50 - $length; + if ( $remaining <= 0 ) { + $pattern = '<b>' . + htmlspecialchars( mb_substr( $row->af_pattern, 0, 50, 'UTF8' ) ) . + '</b>'; + } else { + $minoffset = max( $position - round( $remaining / 2 ), 0 ); + $pattern = mb_substr( $row->af_pattern, $minoffset, 50, 'UTF8' ); + $pattern = + htmlspecialchars( mb_substr( $pattern, 0, $position - $minoffset, 'UTF8' ) ) . + '<b>' . + htmlspecialchars( mb_substr( $pattern, $position - $minoffset, $length, 'UTF8' ) ) . + '</b>' . + htmlspecialchars( mb_substr( + $pattern, + $position - $minoffset + $length, + $remaining - ( $position - $minoffset + $length ), + 'UTF8' + ) + ); + } + 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 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 getTableClass() { + return 'TablePager mw-abusefilter-list-scrollable'; + } + + 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 ); + } +} diff --git a/www/wiki/extensions/AbuseFilter/includes/pagers/AbuseLogPager.php b/www/wiki/extensions/AbuseFilter/includes/pagers/AbuseLogPager.php new file mode 100644 index 00000000..8b4513fe --- /dev/null +++ b/www/wiki/extensions/AbuseFilter/includes/pagers/AbuseLogPager.php @@ -0,0 +1,79 @@ +<?php + +use Wikimedia\Rdbms\ResultWrapper; + +class AbuseLogPager extends ReverseChronologicalPager { + /** + * @var SpecialAbuseLog + */ + public $mForm; + + /** + * @var array + */ + public $mConds; + + /** + * @param SpecialAbuseLog $form + * @param array $conds + */ + function __construct( $form, $conds = [] ) { + $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; + } + + /** + * @param ResultWrapper $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 ); + } + + function getIndexField() { + return 'afl_timestamp'; + } +} diff --git a/www/wiki/extensions/AbuseFilter/includes/pagers/GlobalAbuseFilterPager.php b/www/wiki/extensions/AbuseFilter/includes/pagers/GlobalAbuseFilterPager.php new file mode 100644 index 00000000..36c84a01 --- /dev/null +++ b/www/wiki/extensions/AbuseFilter/includes/pagers/GlobalAbuseFilterPager.php @@ -0,0 +1,70 @@ +<?php + +/** + * 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, [ '', 'LIKE' ] ); + 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 )->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!" ); + } + } +} |