Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 139
0.00% covered (danger)
0.00%
0 / 7
CRAP
0.00% covered (danger)
0.00%
0 / 1
AbuseFilterHistoryPager
0.00% covered (danger)
0.00%
0 / 139
0.00% covered (danger)
0.00%
0 / 7
1260
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
2
 getFieldNames
0.00% covered (danger)
0.00%
0 / 16
0.00% covered (danger)
0.00%
0 / 1
20
 formatValue
0.00% covered (danger)
0.00%
0 / 54
0.00% covered (danger)
0.00%
0 / 1
210
 getQueryInfo
0.00% covered (danger)
0.00%
0 / 35
0.00% covered (danger)
0.00%
0 / 1
20
 preprocessResults
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
12
 getDefaultSort
n/a
0 / 0
n/a
0 / 0
1
 isFieldSortable
n/a
0 / 0
n/a
0 / 0
1
 getCellAttrs
0.00% covered (danger)
0.00%
0 / 15
0.00% covered (danger)
0.00%
0 / 1
30
 getTitle
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
6
1<?php
2
3namespace MediaWiki\Extension\AbuseFilter\Pager;
4
5use HtmlArmor;
6use IContextSource;
7use MediaWiki\Cache\LinkBatchFactory;
8use MediaWiki\Extension\AbuseFilter\AbuseFilter;
9use MediaWiki\Extension\AbuseFilter\AbuseFilterServices;
10use MediaWiki\Extension\AbuseFilter\FilterLookup;
11use MediaWiki\Extension\AbuseFilter\Special\SpecialAbuseFilter;
12use MediaWiki\Extension\AbuseFilter\SpecsFormatter;
13use MediaWiki\Linker\Linker;
14use MediaWiki\Linker\LinkRenderer;
15use MediaWiki\MediaWikiServices;
16use MediaWiki\Pager\TablePager;
17use MediaWiki\Title\Title;
18use MediaWiki\User\UserRigorOptions;
19use UnexpectedValueException;
20use Wikimedia\Rdbms\IResultWrapper;
21use Xml;
22
23class AbuseFilterHistoryPager extends TablePager {
24
25    /** @var LinkBatchFactory */
26    private $linkBatchFactory;
27
28    /** @var FilterLookup */
29    private $filterLookup;
30
31    /** @var SpecsFormatter */
32    private $specsFormatter;
33
34    /** @var int|null The filter ID */
35    private $filter;
36
37    /** @var string|null The user whose changes we're looking up for */
38    private $user;
39
40    /** @var bool */
41    private $canViewPrivateFilters;
42
43    /**
44     * @param IContextSource $context
45     * @param LinkRenderer $linkRenderer
46     * @param LinkBatchFactory $linkBatchFactory
47     * @param FilterLookup $filterLookup
48     * @param SpecsFormatter $specsFormatter
49     * @param ?int $filter
50     * @param ?string $user User name
51     * @param bool $canViewPrivateFilters
52     */
53    public function __construct(
54        IContextSource $context,
55        LinkRenderer $linkRenderer,
56        LinkBatchFactory $linkBatchFactory,
57        FilterLookup $filterLookup,
58        SpecsFormatter $specsFormatter,
59        ?int $filter,
60        ?string $user,
61        bool $canViewPrivateFilters = false
62    ) {
63        // needed by parent's constructor call
64        $this->filter = $filter;
65        parent::__construct( $context, $linkRenderer );
66        $this->linkBatchFactory = $linkBatchFactory;
67        $this->filterLookup = $filterLookup;
68        $this->specsFormatter = $specsFormatter;
69        $this->user = $user;
70        $this->canViewPrivateFilters = $canViewPrivateFilters;
71        $this->mDefaultDirection = true;
72    }
73
74    /**
75     * Note: this method is called by parent::__construct
76     * @return array
77     * @see MediaWiki\Pager\Pager::getFieldNames()
78     */
79    public function getFieldNames() {
80        static $headers = null;
81
82        if ( $headers !== null ) {
83            return $headers;
84        }
85
86        $headers = [
87            'afh_timestamp' => 'abusefilter-history-timestamp',
88            'afh_user_text' => 'abusefilter-history-user',
89            'afh_public_comments' => 'abusefilter-history-public',
90            'afh_flags' => 'abusefilter-history-flags',
91            'afh_actions' => 'abusefilter-history-actions',
92            'afh_id' => 'abusefilter-history-diff',
93        ];
94
95        if ( !$this->filter ) {
96            // awful hack
97            $headers = [ 'afh_filter' => 'abusefilter-history-filterid' ] + $headers;
98        }
99
100        foreach ( $headers as &$msg ) {
101            $msg = $this->msg( $msg )->text();
102        }
103
104        return $headers;
105    }
106
107    /**
108     * @param string $name
109     * @param string|null $value
110     * @return string
111     */
112    public function formatValue( $name, $value ) {
113        $lang = $this->getLanguage();
114        $linkRenderer = $this->getLinkRenderer();
115
116        $row = $this->mCurrentRow;
117
118        switch ( $name ) {
119            case 'afh_filter':
120                $formatted = $linkRenderer->makeLink(
121                    SpecialAbuseFilter::getTitleForSubpage( $row->afh_filter ),
122                    $lang->formatNum( $row->afh_filter )
123                );
124                break;
125            case 'afh_timestamp':
126                $title = SpecialAbuseFilter::getTitleForSubpage(
127                    'history/' . $row->afh_filter . '/item/' . $row->afh_id );
128                $formatted = $linkRenderer->makeLink(
129                    $title,
130                    $lang->userTimeAndDate( $row->afh_timestamp, $this->getUser() )
131                );
132                break;
133            case 'afh_user_text':
134                $formatted =
135                    Linker::userLink( $row->afh_user, $row->afh_user_text ) . ' ' .
136                    Linker::userToolLinks( $row->afh_user, $row->afh_user_text );
137                break;
138            case 'afh_public_comments':
139                $formatted = htmlspecialchars( $value, ENT_QUOTES, 'UTF-8', false );
140                break;
141            case 'afh_flags':
142                $formatted = $this->specsFormatter->formatFlags( $value, $lang );
143                break;
144            case 'afh_actions':
145                $actions = unserialize( $value );
146
147                $display_actions = '';
148
149                foreach ( $actions as $action => $parameters ) {
150                    $displayAction = $this->specsFormatter->formatAction( $action, $parameters, $lang );
151                    $display_actions .= Xml::tags( 'li', null, $displayAction );
152                }
153                $display_actions = Xml::tags( 'ul', null, $display_actions );
154
155                $formatted = $display_actions;
156                break;
157            case 'afh_id':
158                // Set a link to a diff with the previous version if this isn't the first edit to the filter.
159                // Like in AbuseFilterViewDiff, don't show it if the user cannot see private filters and any
160                // of the versions is hidden.
161                $formatted = '';
162                if ( $this->filterLookup->getFirstFilterVersionID( $row->afh_filter ) !== (int)$value ) {
163                    // @todo Should we also hide actions?
164                    $prevFilter = $this->filterLookup->getClosestVersion(
165                        $row->afh_id, $row->afh_filter, FilterLookup::DIR_PREV );
166                    if ( $this->canViewPrivateFilters ||
167                        (
168                            !in_array( 'hidden', explode( ',', $row->afh_flags ) ) &&
169                            !$prevFilter->isHidden()
170                        )
171                    ) {
172                        $title = SpecialAbuseFilter::getTitleForSubpage(
173                            'history/' . $row->afh_filter . "/diff/prev/$value" );
174                        $formatted = $linkRenderer->makeLink(
175                            $title,
176                            new HtmlArmor( $this->msg( 'abusefilter-history-diff' )->parse() )
177                        );
178                    }
179                }
180                break;
181            default:
182                throw new UnexpectedValueException( "Unknown row type $name!" );
183        }
184
185        return $formatted;
186    }
187
188    /**
189     * @return array
190     */
191    public function getQueryInfo() {
192        $afActorMigration = AbuseFilterServices::getActorMigration();
193        $actorQuery = $afActorMigration->getJoin( 'afh_user' );
194        $info = [
195            'tables' => [ 'abuse_filter_history', 'abuse_filter' ] + $actorQuery['tables'],
196            // All fields but afh_deleted on abuse_filter_history
197            'fields' => [
198                'afh_filter',
199                'afh_timestamp',
200                'afh_public_comments',
201                'afh_flags',
202                'afh_comments',
203                'afh_actions',
204                'afh_id',
205                'afh_changed_fields',
206                'afh_pattern',
207                'af_hidden'
208            ] + $actorQuery['fields'],
209            'conds' => [],
210            'join_conds' => [
211                'abuse_filter' =>
212                    [
213                        'LEFT JOIN',
214                        'afh_filter=af_id',
215                    ],
216            ] + $actorQuery['joins'],
217        ];
218
219        if ( $this->user !== null ) {
220            $user = MediaWikiServices::getInstance()->getUserFactory()
221                ->newFromName( $this->user, UserRigorOptions::RIGOR_NONE );
222            $whereQuery = $afActorMigration->getWhere( $this->mDb, 'afh_user', $user );
223            $info['conds'][] = $whereQuery['conds'];
224        }
225
226        if ( $this->filter ) {
227            $info['conds']['afh_filter'] = $this->filter;
228        }
229
230        if ( !$this->canViewPrivateFilters ) {
231            // Hide data the user can't see.
232            $info['conds']['af_hidden'] = 0;
233        }
234
235        return $info;
236    }
237
238    /**
239     * @param IResultWrapper $result
240     */
241    protected function preprocessResults( $result ) {
242        if ( $this->getNumRows() === 0 ) {
243            return;
244        }
245
246        $lb = $this->linkBatchFactory->newLinkBatch();
247        $lb->setCaller( __METHOD__ );
248        foreach ( $result as $row ) {
249            $lb->add( NS_USER, $row->afh_user_text );
250            $lb->add( NS_USER_TALK, $row->afh_user_text );
251        }
252        $lb->execute();
253        $result->seek( 0 );
254    }
255
256    /**
257     * @codeCoverageIgnore Merely declarative
258     * @inheritDoc
259     */
260    public function getDefaultSort() {
261        return 'afh_timestamp';
262    }
263
264    /**
265     * @codeCoverageIgnore Merely declarative
266     * @inheritDoc
267     */
268    public function isFieldSortable( $field ) {
269        return $field === 'afh_timestamp';
270    }
271
272    /**
273     * @param string $field
274     * @param string $value
275     * @return array
276     * @see TablePager::getCellAttrs
277     */
278    public function getCellAttrs( $field, $value ) {
279        $row = $this->mCurrentRow;
280        $mappings = array_flip( AbuseFilter::HISTORY_MAPPINGS ) +
281            [ 'afh_actions' => 'actions', 'afh_id' => 'id' ];
282        $changed = explode( ',', $row->afh_changed_fields );
283
284        $fieldChanged = false;
285        if ( $field === 'afh_flags' ) {
286            // The field is changed if any of these filters are in the $changed array.
287            $filters = [ 'af_enabled', 'af_hidden', 'af_deleted', 'af_global' ];
288            if ( count( array_intersect( $filters, $changed ) ) ) {
289                $fieldChanged = true;
290            }
291        } elseif ( in_array( $mappings[$field], $changed ) ) {
292            $fieldChanged = true;
293        }
294
295        $class = $fieldChanged ? ' mw-abusefilter-history-changed' : '';
296        $attrs = parent::getCellAttrs( $field, $value );
297        $attrs['class'] .= $class;
298        return $attrs;
299    }
300
301    /**
302     * Title used for self-links.
303     *
304     * @return Title
305     */
306    public function getTitle() {
307        $subpage = $this->filter ? ( 'history/' . $this->filter ) : 'history';
308        return SpecialAbuseFilter::getTitleForSubpage( $subpage );
309    }
310}