Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
31.77% covered (danger)
31.77%
61 / 192
11.11% covered (danger)
11.11%
1 / 9
CRAP
0.00% covered (danger)
0.00%
0 / 1
AbuseFilterViewDiff
31.77% covered (danger)
31.77%
61 / 192
11.11% covered (danger)
11.11%
1 / 9
718.09
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 show
41.03% covered (danger)
41.03%
16 / 39
0.00% covered (danger)
0.00%
0 / 1
17.05
 loadData
69.70% covered (warning)
69.70%
23 / 33
0.00% covered (danger)
0.00%
0 / 1
16.01
 loadSpec
78.26% covered (warning)
78.26%
18 / 23
0.00% covered (danger)
0.00%
0 / 1
13.48
 getVersionHeading
0.00% covered (danger)
0.00%
0 / 19
0.00% covered (danger)
0.00%
0 / 1
2
 formatDiff
0.00% covered (danger)
0.00%
0 / 44
0.00% covered (danger)
0.00%
0 / 1
42
 stringifyActions
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
6
 getHeaderRow
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
2
 getDiffRow
0.00% covered (danger)
0.00%
0 / 18
0.00% covered (danger)
0.00%
0 / 1
20
1<?php
2
3namespace MediaWiki\Extension\AbuseFilter\View;
4
5use DifferenceEngine;
6use MediaWiki\Content\TextContent;
7use MediaWiki\Context\IContextSource;
8use MediaWiki\Extension\AbuseFilter\AbuseFilterPermissionManager;
9use MediaWiki\Extension\AbuseFilter\Filter\ClosestFilterVersionNotFoundException;
10use MediaWiki\Extension\AbuseFilter\Filter\FilterNotFoundException;
11use MediaWiki\Extension\AbuseFilter\Filter\FilterVersionNotFoundException;
12use MediaWiki\Extension\AbuseFilter\Filter\HistoryFilter;
13use MediaWiki\Extension\AbuseFilter\FilterLookup;
14use MediaWiki\Extension\AbuseFilter\SpecsFormatter;
15use MediaWiki\Extension\AbuseFilter\TableDiffFormatterFullContext;
16use MediaWiki\Html\Html;
17use MediaWiki\Linker\Linker;
18use MediaWiki\Linker\LinkRenderer;
19use OOUI;
20use Wikimedia\Diff\Diff;
21
22class AbuseFilterViewDiff extends AbuseFilterView {
23    /**
24     * @var HistoryFilter|null The old version of the filter
25     */
26    public $oldVersion;
27    /**
28     * @var HistoryFilter|null The new version of the filter
29     */
30    public $newVersion;
31    /**
32     * @var int|null The history ID of the next version, if any
33     */
34    public $nextHistoryId;
35    /**
36     * @var int|null The ID of the filter
37     */
38    private $filter;
39    /**
40     * @var SpecsFormatter
41     */
42    private $specsFormatter;
43    /**
44     * @var FilterLookup
45     */
46    private $filterLookup;
47
48    /**
49     * @param AbuseFilterPermissionManager $afPermManager
50     * @param SpecsFormatter $specsFormatter
51     * @param FilterLookup $filterLookup
52     * @param IContextSource $context
53     * @param LinkRenderer $linkRenderer
54     * @param string $basePageName
55     * @param array $params
56     */
57    public function __construct(
58        AbuseFilterPermissionManager $afPermManager,
59        SpecsFormatter $specsFormatter,
60        FilterLookup $filterLookup,
61        IContextSource $context,
62        LinkRenderer $linkRenderer,
63        string $basePageName,
64        array $params
65    ) {
66        parent::__construct( $afPermManager, $context, $linkRenderer, $basePageName, $params );
67        $this->specsFormatter = $specsFormatter;
68        $this->specsFormatter->setMessageLocalizer( $this->getContext() );
69        $this->filterLookup = $filterLookup;
70    }
71
72    /**
73     * Shows the page
74     */
75    public function show() {
76        $show = $this->loadData();
77        $out = $this->getOutput();
78        $out->enableOOUI();
79        $out->addModuleStyles( [ 'oojs-ui.styles.icons-movement' ] );
80
81        $links = [];
82        if ( $this->filter ) {
83            $links['abusefilter-history-backedit'] = $this->getTitle( $this->filter )->getFullURL();
84            $links['abusefilter-diff-backhistory'] = $this->getTitle( "history/$this->filter" )->getFullURL();
85        }
86
87        foreach ( $links as $msg => $href ) {
88            $links[$msg] = new OOUI\ButtonWidget( [
89                'label' => $this->msg( $msg )->text(),
90                'href' => $href
91            ] );
92        }
93
94        $backlinks = new OOUI\HorizontalLayout( [ 'items' => array_values( $links ) ] );
95        $out->addHTML( $backlinks );
96
97        if ( $show ) {
98            $out->addHTML( $this->formatDiff() );
99            // Next and previous change links
100            $buttons = [];
101            $oldHistoryID = $this->oldVersion->getHistoryID();
102            if ( $this->filterLookup->getFirstFilterVersionID( $this->filter ) !== $oldHistoryID ) {
103                // Create a "previous change" link if this isn't the first change of the given filter
104                $href = $this->getTitle( "history/$this->filter/diff/prev/$oldHistoryID" )->getFullURL();
105                $buttons[] = new OOUI\ButtonWidget( [
106                    'label' => $this->msg( 'abusefilter-diff-prev' )->text(),
107                    'href' => $href,
108                    'icon' => 'previous'
109                ] );
110            }
111
112            if ( $this->nextHistoryId !== null ) {
113                // Create a "next change" link if this isn't the last change of the given filter
114                $href = $this->getTitle( "history/$this->filter/diff/prev/$this->nextHistoryId" )->getFullURL();
115                $buttons[] = new OOUI\ButtonWidget( [
116                    'label' => $this->msg( 'abusefilter-diff-next' )->text(),
117                    'href' => $href,
118                    'icon' => 'next'
119                ] );
120            }
121
122            if ( count( $buttons ) > 0 ) {
123                $buttons = new OOUI\HorizontalLayout( [
124                    'items' => $buttons,
125                    'classes' => [ 'mw-abusefilter-history-buttons' ]
126                ] );
127                $out->addHTML( $buttons );
128            }
129        }
130    }
131
132    /**
133     * @return bool
134     */
135    public function loadData() {
136        $oldSpec = $this->mParams[3];
137        $newSpec = $this->mParams[4];
138
139        if ( !is_numeric( $this->mParams[1] ) ) {
140            $this->getOutput()->addWikiMsg( 'abusefilter-diff-invalid' );
141            return false;
142        }
143        $this->filter = (int)$this->mParams[1];
144
145        $this->oldVersion = $this->loadSpec( $oldSpec, $newSpec );
146        $this->newVersion = $this->loadSpec( $newSpec, $oldSpec );
147
148        if ( $this->oldVersion === null || $this->newVersion === null ) {
149            $this->getOutput()->addWikiMsg( 'abusefilter-diff-invalid' );
150            return false;
151        }
152
153        if ( !$this->afPermManager->canViewPrivateFilters( $this->getAuthority() ) &&
154            ( $this->oldVersion->isHidden() || $this->newVersion->isHidden() )
155        ) {
156            $this->getOutput()->addWikiMsg( 'abusefilter-history-error-hidden' );
157            return false;
158        }
159
160        if (
161            (
162                $this->oldVersion->isProtected() &&
163                !$this->afPermManager->canViewProtectedVariablesInFilter(
164                    $this->getAuthority(), $this->oldVersion
165                )->isGood()
166            ) ||
167            (
168                $this->newVersion->isProtected() &&
169                !$this->afPermManager->canViewProtectedVariablesInFilter(
170                    $this->getAuthority(), $this->newVersion
171                )->isGood()
172            )
173        ) {
174            $this->getOutput()->addWikiMsg( 'abusefilter-history-error-protected' );
175            return false;
176        }
177
178        try {
179            $this->nextHistoryId = $this->filterLookup->getClosestVersion(
180                $this->newVersion->getHistoryID(),
181                $this->filter,
182                FilterLookup::DIR_NEXT
183            )->getHistoryID();
184        } catch ( ClosestFilterVersionNotFoundException $_ ) {
185            $this->nextHistoryId = null;
186        }
187
188        return true;
189    }
190
191    /**
192     * @param string $spec
193     * @param string $otherSpec
194     * @return HistoryFilter|null
195     */
196    public function loadSpec( $spec, $otherSpec ): ?HistoryFilter {
197        static $dependentSpecs = [ 'prev', 'next' ];
198        static $cache = [];
199
200        if ( isset( $cache[$spec] ) ) {
201            return $cache[$spec];
202        }
203
204        $filterObj = null;
205        if ( ( $spec === 'prev' || $spec === 'next' ) && !in_array( $otherSpec, $dependentSpecs ) ) {
206            $other = $this->loadSpec( $otherSpec, $spec );
207
208            if ( !$other ) {
209                return null;
210            }
211
212            $dir = $spec === 'prev' ? FilterLookup::DIR_PREV : FilterLookup::DIR_NEXT;
213            try {
214                $filterObj = $this->filterLookup->getClosestVersion( $other->getHistoryID(), $this->filter, $dir );
215            } catch ( ClosestFilterVersionNotFoundException $_ ) {
216                $t = $this->getTitle( "history/$this->filter/item/" . $other->getHistoryID() );
217                $this->getOutput()->redirect( $t->getFullURL() );
218                return null;
219            }
220        }
221
222        if ( $filterObj === null ) {
223            try {
224                if ( is_numeric( $spec ) ) {
225                    $filterObj = $this->filterLookup->getFilterVersion( (int)$spec );
226                } elseif ( $spec === 'cur' ) {
227                    $filterObj = $this->filterLookup->getLastHistoryVersion( $this->filter );
228                }
229            } catch ( FilterNotFoundException | FilterVersionNotFoundException $_ ) {
230            }
231        }
232
233        $cache[$spec] = $filterObj;
234        return $cache[$spec];
235    }
236
237    /**
238     * @param HistoryFilter $filterVersion
239     * @return string raw html for the <th> element
240     */
241    private function getVersionHeading( HistoryFilter $filterVersion ) {
242        $text = $this->getLanguage()->userTimeAndDate(
243            $filterVersion->getTimestamp(),
244            $this->getUser()
245        );
246        $history_id = $filterVersion->getHistoryID();
247        $title = $this->getTitle( "history/$this->filter/item/$history_id" );
248
249        $versionLink = $this->linkRenderer->makeLink( $title, $text );
250        $userLink = Linker::userLink(
251            $filterVersion->getUserID(),
252            $filterVersion->getUserName()
253        );
254        return Html::rawElement(
255            'th',
256            [],
257            $this->msg( 'abusefilter-diff-version' )
258                ->rawParams( $versionLink, $userLink )
259                ->params( $filterVersion->getUserName() )
260                ->parse()
261        );
262    }
263
264    /**
265     * @return string
266     */
267    public function formatDiff() {
268        $oldVersion = $this->oldVersion;
269        $newVersion = $this->newVersion;
270
271        // headings
272        $headings = Html::rawElement(
273            'th',
274            [],
275            $this->msg( 'abusefilter-diff-item' )->parse()
276        );
277        $headings .= $this->getVersionHeading( $oldVersion );
278        $headings .= $this->getVersionHeading( $newVersion );
279        $headings = Html::rawElement( 'tr', [], $headings );
280
281        $body = '';
282        // Basic info
283        $info = $this->getDiffRow( 'abusefilter-edit-description', $oldVersion->getName(), $newVersion->getName() );
284        $info .= $this->getDiffRow(
285            'abusefilter-edit-group',
286            $this->specsFormatter->nameGroup( $oldVersion->getGroup() ),
287            $this->specsFormatter->nameGroup( $newVersion->getGroup() )
288        );
289        $info .= $this->getDiffRow(
290            'abusefilter-edit-flags',
291            $this->specsFormatter->formatFilterFlags( $oldVersion, $this->getLanguage() ),
292            $this->specsFormatter->formatFilterFlags( $newVersion, $this->getLanguage() )
293        );
294
295        $info .= $this->getDiffRow( 'abusefilter-edit-notes', $oldVersion->getComments(), $newVersion->getComments() );
296        if ( $info !== '' ) {
297            $body .= $this->getHeaderRow( 'abusefilter-diff-info' ) . $info;
298        }
299
300        $pattern = $this->getDiffRow( 'abusefilter-edit-rules', $oldVersion->getRules(), $newVersion->getRules() );
301        if ( $pattern !== '' ) {
302            $body .= $this->getHeaderRow( 'abusefilter-diff-pattern' ) . $pattern;
303        }
304
305        $actions = $this->getDiffRow(
306            'abusefilter-edit-consequences',
307            $this->stringifyActions( $oldVersion->getActions() ) ?: [ '' ],
308            $this->stringifyActions( $newVersion->getActions() ) ?: [ '' ]
309        );
310        if ( $actions !== '' ) {
311            $body .= $this->getHeaderRow( 'abusefilter-edit-consequences' ) . $actions;
312        }
313
314        $tableHead = Html::rawElement( 'thead', [], $headings );
315        $tableBody = Html::rawElement( 'tbody', [], $body );
316        $table = Html::rawElement(
317            'table',
318            [ 'class' => 'wikitable' ],
319            $tableHead . $tableBody
320        );
321
322        return Html::rawElement( 'h2', [], $this->msg( 'abusefilter-diff-title' )->parse() ) .
323            $table;
324    }
325
326    /**
327     * @param string[][] $actions
328     * @return string[]
329     */
330    private function stringifyActions( array $actions ): array {
331        $lines = [];
332
333        ksort( $actions );
334        $language = $this->getLanguage();
335        foreach ( $actions as $action => $parameters ) {
336            $lines[] = $this->specsFormatter->formatAction( $action, $parameters, $language );
337        }
338
339        return $lines;
340    }
341
342    /**
343     * @param string $key
344     * @return string
345     */
346    public function getHeaderRow( $key ) {
347        $msg = $this->msg( $key )->parse();
348        return Html::rawElement(
349            'tr',
350            [ 'class' => 'mw-abusefilter-diff-header' ],
351            Html::rawElement( 'th', [ 'colspan' => 3 ], $msg )
352        );
353    }
354
355    /**
356     * @param string $key
357     * @param array|string $old
358     * @param array|string $new
359     * @return string
360     */
361    public function getDiffRow( $key, $old, $new ) {
362        if ( !is_array( $old ) ) {
363            $old = explode( "\n", TextContent::normalizeLineEndings( $old ) );
364        }
365        if ( !is_array( $new ) ) {
366            $new = explode( "\n", TextContent::normalizeLineEndings( $new ) );
367        }
368
369        if ( $old === $new ) {
370            return '';
371        }
372
373        $diffEngine = new DifferenceEngine( $this->getContext() );
374
375        $diffEngine->showDiffStyle();
376
377        $diff = new Diff( $old, $new );
378        $formatter = new TableDiffFormatterFullContext();
379        $formattedDiff = $diffEngine->addHeader( $formatter->format( $diff ), '', '' );
380
381        $heading = Html::rawElement( 'th', [], $this->msg( $key )->parse() );
382        $bodyCell = Html::rawElement( 'td', [ 'colspan' => 2 ], $formattedDiff );
383        return Html::rawElement(
384            'tr',
385            [],
386            $heading . $bodyCell
387        ) . "\n";
388    }
389}