Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
2.20% covered (danger)
2.20%
4 / 182
11.11% covered (danger)
11.11%
1 / 9
CRAP
0.00% covered (danger)
0.00%
0 / 1
AbuseFilterViewDiff
2.20% covered (danger)
2.20%
4 / 182
11.11% covered (danger)
11.11%
1 / 9
1692.23
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
0.00% covered (danger)
0.00%
0 / 39
0.00% covered (danger)
0.00%
0 / 1
56
 loadData
0.00% covered (danger)
0.00%
0 / 23
0.00% covered (danger)
0.00%
0 / 1
72
 loadSpec
0.00% covered (danger)
0.00%
0 / 23
0.00% covered (danger)
0.00%
0 / 1
156
 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 IContextSource;
7use MediaWiki\Extension\AbuseFilter\AbuseFilterPermissionManager;
8use MediaWiki\Extension\AbuseFilter\Filter\ClosestFilterVersionNotFoundException;
9use MediaWiki\Extension\AbuseFilter\Filter\FilterNotFoundException;
10use MediaWiki\Extension\AbuseFilter\Filter\FilterVersionNotFoundException;
11use MediaWiki\Extension\AbuseFilter\Filter\HistoryFilter;
12use MediaWiki\Extension\AbuseFilter\FilterLookup;
13use MediaWiki\Extension\AbuseFilter\SpecsFormatter;
14use MediaWiki\Extension\AbuseFilter\TableDiffFormatterFullContext;
15use MediaWiki\Html\Html;
16use MediaWiki\Linker\Linker;
17use MediaWiki\Linker\LinkRenderer;
18use OOUI;
19use TextContent;
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        try {
161            $this->nextHistoryId = $this->filterLookup->getClosestVersion(
162                $this->newVersion->getHistoryID(),
163                $this->filter,
164                FilterLookup::DIR_NEXT
165            )->getHistoryID();
166        } catch ( ClosestFilterVersionNotFoundException $_ ) {
167            $this->nextHistoryId = null;
168        }
169
170        return true;
171    }
172
173    /**
174     * @param string $spec
175     * @param string $otherSpec
176     * @return HistoryFilter|null
177     */
178    public function loadSpec( $spec, $otherSpec ): ?HistoryFilter {
179        static $dependentSpecs = [ 'prev', 'next' ];
180        static $cache = [];
181
182        if ( isset( $cache[$spec] ) ) {
183            return $cache[$spec];
184        }
185
186        $filterObj = null;
187        if ( ( $spec === 'prev' || $spec === 'next' ) && !in_array( $otherSpec, $dependentSpecs ) ) {
188            $other = $this->loadSpec( $otherSpec, $spec );
189
190            if ( !$other ) {
191                return null;
192            }
193
194            $dir = $spec === 'prev' ? FilterLookup::DIR_PREV : FilterLookup::DIR_NEXT;
195            try {
196                $filterObj = $this->filterLookup->getClosestVersion( $other->getHistoryID(), $this->filter, $dir );
197            } catch ( ClosestFilterVersionNotFoundException $_ ) {
198                $t = $this->getTitle( "history/$this->filter/item/" . $other->getHistoryID() );
199                $this->getOutput()->redirect( $t->getFullURL() );
200                return null;
201            }
202        }
203
204        if ( $filterObj === null ) {
205            try {
206                if ( is_numeric( $spec ) ) {
207                    $filterObj = $this->filterLookup->getFilterVersion( (int)$spec );
208                } elseif ( $spec === 'cur' ) {
209                    $filterObj = $this->filterLookup->getLastHistoryVersion( $this->filter );
210                }
211            } catch ( FilterNotFoundException | FilterVersionNotFoundException $_ ) {
212            }
213        }
214
215        $cache[$spec] = $filterObj;
216        return $cache[$spec];
217    }
218
219    /**
220     * @param HistoryFilter $filterVersion
221     * @return string raw html for the <th> element
222     */
223    private function getVersionHeading( HistoryFilter $filterVersion ) {
224        $text = $this->getLanguage()->userTimeAndDate(
225            $filterVersion->getTimestamp(),
226            $this->getUser()
227        );
228        $history_id = $filterVersion->getHistoryID();
229        $title = $this->getTitle( "history/$this->filter/item/$history_id" );
230
231        $versionLink = $this->linkRenderer->makeLink( $title, $text );
232        $userLink = Linker::userLink(
233            $filterVersion->getUserID(),
234            $filterVersion->getUserName()
235        );
236        return Html::rawElement(
237            'th',
238            [],
239            $this->msg( 'abusefilter-diff-version' )
240                ->rawParams( $versionLink, $userLink )
241                ->params( $filterVersion->getUserName() )
242                ->parse()
243        );
244    }
245
246    /**
247     * @return string
248     */
249    public function formatDiff() {
250        $oldVersion = $this->oldVersion;
251        $newVersion = $this->newVersion;
252
253        // headings
254        $headings = Html::rawElement(
255            'th',
256            [],
257            $this->msg( 'abusefilter-diff-item' )->parse()
258        );
259        $headings .= $this->getVersionHeading( $oldVersion );
260        $headings .= $this->getVersionHeading( $newVersion );
261        $headings = Html::rawElement( 'tr', [], $headings );
262
263        $body = '';
264        // Basic info
265        $info = $this->getDiffRow( 'abusefilter-edit-description', $oldVersion->getName(), $newVersion->getName() );
266        $info .= $this->getDiffRow(
267            'abusefilter-edit-group',
268            $this->specsFormatter->nameGroup( $oldVersion->getGroup() ),
269            $this->specsFormatter->nameGroup( $newVersion->getGroup() )
270        );
271        $info .= $this->getDiffRow(
272            'abusefilter-edit-flags',
273            $this->specsFormatter->formatFilterFlags( $oldVersion, $this->getLanguage() ),
274            $this->specsFormatter->formatFilterFlags( $newVersion, $this->getLanguage() )
275        );
276
277        $info .= $this->getDiffRow( 'abusefilter-edit-notes', $oldVersion->getComments(), $newVersion->getComments() );
278        if ( $info !== '' ) {
279            $body .= $this->getHeaderRow( 'abusefilter-diff-info' ) . $info;
280        }
281
282        $pattern = $this->getDiffRow( 'abusefilter-edit-rules', $oldVersion->getRules(), $newVersion->getRules() );
283        if ( $pattern !== '' ) {
284            $body .= $this->getHeaderRow( 'abusefilter-diff-pattern' ) . $pattern;
285        }
286
287        $actions = $this->getDiffRow(
288            'abusefilter-edit-consequences',
289            $this->stringifyActions( $oldVersion->getActions() ) ?: [ '' ],
290            $this->stringifyActions( $newVersion->getActions() ) ?: [ '' ]
291        );
292        if ( $actions !== '' ) {
293            $body .= $this->getHeaderRow( 'abusefilter-edit-consequences' ) . $actions;
294        }
295
296        $tableHead = Html::rawElement( 'thead', [], $headings );
297        $tableBody = Html::rawElement( 'tbody', [], $body );
298        $table = Html::rawElement(
299            'table',
300            [ 'class' => 'wikitable' ],
301            $tableHead . $tableBody
302        );
303
304        return Html::rawElement( 'h2', [], $this->msg( 'abusefilter-diff-title' )->parse() ) .
305            $table;
306    }
307
308    /**
309     * @param string[][] $actions
310     * @return string[]
311     */
312    private function stringifyActions( array $actions ): array {
313        $lines = [];
314
315        ksort( $actions );
316        $language = $this->getLanguage();
317        foreach ( $actions as $action => $parameters ) {
318            $lines[] = $this->specsFormatter->formatAction( $action, $parameters, $language );
319        }
320
321        return $lines;
322    }
323
324    /**
325     * @param string $key
326     * @return string
327     */
328    public function getHeaderRow( $key ) {
329        $msg = $this->msg( $key )->parse();
330        return Html::rawElement(
331            'tr',
332            [ 'class' => 'mw-abusefilter-diff-header' ],
333            Html::rawElement( 'th', [ 'colspan' => 3 ], $msg )
334        );
335    }
336
337    /**
338     * @param string $key
339     * @param array|string $old
340     * @param array|string $new
341     * @return string
342     */
343    public function getDiffRow( $key, $old, $new ) {
344        if ( !is_array( $old ) ) {
345            $old = explode( "\n", TextContent::normalizeLineEndings( $old ) );
346        }
347        if ( !is_array( $new ) ) {
348            $new = explode( "\n", TextContent::normalizeLineEndings( $new ) );
349        }
350
351        if ( $old === $new ) {
352            return '';
353        }
354
355        $diffEngine = new DifferenceEngine( $this->getContext() );
356
357        $diffEngine->showDiffStyle();
358
359        $diff = new Diff( $old, $new );
360        $formatter = new TableDiffFormatterFullContext();
361        $formattedDiff = $diffEngine->addHeader( $formatter->format( $diff ), '', '' );
362
363        $heading = Html::rawElement( 'th', [], $this->msg( $key )->parse() );
364        $bodyCell = Html::rawElement( 'td', [ 'colspan' => 2 ], $formattedDiff );
365        return Html::rawElement(
366            'tr',
367            [],
368            $heading . $bodyCell
369        ) . "\n";
370    }
371}