Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
90.12% covered (success)
90.12%
73 / 81
85.71% covered (warning)
85.71%
6 / 7
CRAP
0.00% covered (danger)
0.00%
0 / 1
AbuseFilterView
90.12% covered (success)
90.12%
73 / 81
85.71% covered (warning)
85.71%
6 / 7
15.22
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 getTitle
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 show
n/a
0 / 0
n/a
0 / 0
0
 buildFilterLoader
100.00% covered (success)
100.00%
31 / 31
100.00% covered (success)
100.00%
1 / 1
1
 buildTestConditions
68.00% covered (warning)
68.00%
17 / 25
0.00% covered (danger)
0.00%
0 / 1
7.18
 buildVisibilityConditions
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
4
 getLinkToLatestDiff
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 showUnrecoverableError
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2
3namespace MediaWiki\Extension\AbuseFilter\View;
4
5use Flow\Data\Listener\RecentChangesListener;
6use MediaWiki\Context\ContextSource;
7use MediaWiki\Context\IContextSource;
8use MediaWiki\Extension\AbuseFilter\AbuseFilterPermissionManager;
9use MediaWiki\Html\Html;
10use MediaWiki\Linker\LinkRenderer;
11use MediaWiki\Permissions\Authority;
12use MediaWiki\RecentChanges\RecentChange;
13use MediaWiki\Revision\RevisionRecord;
14use MediaWiki\SpecialPage\SpecialPage;
15use MediaWiki\Title\Title;
16use OOUI;
17use UnexpectedValueException;
18use Wikimedia\Assert\Assert;
19use Wikimedia\Message\MessageSpecifier;
20use Wikimedia\Rdbms\IExpression;
21use Wikimedia\Rdbms\IReadableDatabase;
22use Wikimedia\Rdbms\Platform\ISQLPlatform;
23
24abstract class AbuseFilterView extends ContextSource {
25
26    private const MAP_ACTION_TO_LOG_TYPE = [
27        // action => [ rc_log_type, rc_log_action ]
28        'move' => [ 'move', [ 'move', 'move_redir' ] ],
29        'createaccount' => [ 'newusers', [ 'create', 'create2', 'byemail', 'autocreate' ] ],
30        'delete' => [ 'delete', 'delete' ],
31        'upload' => [ 'upload', [ 'upload', 'overwrite', 'revert' ] ],
32    ];
33
34    /**
35     * @var array The parameters of the current request
36     */
37    protected array $mParams;
38
39    /**
40     * @param AbuseFilterPermissionManager $afPermManager
41     * @param IContextSource $context
42     * @param LinkRenderer $linkRenderer
43     * @param string $basePageName
44     * @param array $params
45     */
46    public function __construct(
47        protected readonly AbuseFilterPermissionManager $afPermManager,
48        protected readonly IContextSource $context,
49        protected readonly LinkRenderer $linkRenderer,
50        protected readonly string $basePageName,
51        array $params
52    ) {
53        $this->mParams = $params;
54        $this->setContext( $context );
55    }
56
57    /**
58     * @param string|int $subpage
59     * @return Title
60     */
61    public function getTitle( $subpage = '' ) {
62        return SpecialPage::getTitleFor( $this->basePageName, $subpage );
63    }
64
65    /**
66     * Function to show the page
67     */
68    abstract public function show();
69
70    /**
71     * Build input and button for loading a filter
72     *
73     * @return string
74     */
75    public function buildFilterLoader() {
76        $loadText =
77            new OOUI\TextInputWidget(
78                [
79                    'type' => 'number',
80                    'name' => 'wpInsertFilter',
81                    'id' => 'mw-abusefilter-load-filter'
82                ]
83            );
84        $loadButton =
85            new OOUI\ButtonWidget(
86                [
87                    'label' => $this->msg( 'abusefilter-test-loadfilter-load' )->text(),
88                    'id' => 'mw-abusefilter-load',
89                    'infusable' => true,
90                ]
91            );
92        $loadGroup =
93            new OOUI\ActionFieldLayout(
94                $loadText,
95                $loadButton,
96                [
97                    'label' => $this->msg( 'abusefilter-test-loadfilter' )->text(),
98                    'id' => 'mw-abusefilter-load-layout',
99                    'infusable' => true,
100                ]
101            );
102        // CSS class for reducing default input field width
103        return Html::rawElement(
104            'div',
105            [ 'class' => 'mw-abusefilter-load-filter-id' ],
106            (string)$loadGroup
107        );
108    }
109
110    /**
111     * @param IReadableDatabase $db
112     * @param string|false $action 'edit', 'move', 'createaccount', 'delete' or false for all
113     * @return IExpression
114     */
115    public function buildTestConditions( IReadableDatabase $db, $action = false ) {
116        Assert::parameterType( [ 'string', 'false' ], $action, '$action' );
117        $editSources = [
118            RecentChange::SRC_EDIT,
119            RecentChange::SRC_NEW,
120        ];
121        if ( in_array( 'flow', $this->getConfig()->get( 'AbuseFilterValidGroups' ), true ) ) {
122            // TODO Should this be separated somehow? Also, this case should be handled via a hook, not
123            // by special-casing Flow here.
124            // @phan-suppress-next-line PhanUndeclaredClassConstant Temporary solution
125            $editSources[] = RecentChangesListener::SRC_FLOW;
126        }
127        if ( $action === 'edit' ) {
128            return $db->expr( 'rc_source', '=', $editSources );
129        }
130        if ( $action !== false ) {
131            if ( !isset( self::MAP_ACTION_TO_LOG_TYPE[$action] ) ) {
132                throw new UnexpectedValueException( __METHOD__ . ' called with invalid action: ' . $action );
133            }
134            [ $logType, $logAction ] = self::MAP_ACTION_TO_LOG_TYPE[$action];
135            return $db->expr( 'rc_source', '=', RecentChange::SRC_LOG )
136                ->and( 'rc_log_type', '=', $logType )
137                ->and( 'rc_log_action', '=', $logAction );
138        }
139
140        // filter edit and log actions
141        $conds = [];
142        foreach ( self::MAP_ACTION_TO_LOG_TYPE as [ $logType, $logAction ] ) {
143            $conds[] = $db->expr( 'rc_log_type', '=', $logType )
144                ->and( 'rc_log_action', '=', $logAction );
145        }
146
147        return $db->expr( 'rc_source', '=', $editSources )
148            ->orExpr(
149                $db->expr( 'rc_source', '=', RecentChange::SRC_LOG )
150                    ->andExpr( $db->orExpr( $conds ) )
151            );
152    }
153
154    /**
155     * @todo Core should provide a method for this (T233222)
156     * @param ISQLPlatform $db
157     * @param Authority $authority
158     * @return array
159     */
160    public function buildVisibilityConditions( ISQLPlatform $db, Authority $authority ): array {
161        if ( !$authority->isAllowed( 'deletedhistory' ) ) {
162            $bitmask = RevisionRecord::DELETED_USER;
163        } elseif ( !$authority->isAllowedAny( 'suppressrevision', 'viewsuppressed' ) ) {
164            $bitmask = RevisionRecord::DELETED_USER | RevisionRecord::DELETED_RESTRICTED;
165        } else {
166            $bitmask = 0;
167        }
168        return $bitmask
169            ? [ $db->bitAnd( 'rc_deleted', $bitmask ) . " != $bitmask" ]
170            : [];
171    }
172
173    /**
174     * @param string|int $id
175     * @param string|null $text
176     * @return string HTML
177     */
178    public function getLinkToLatestDiff( $id, $text = null ) {
179        return $this->linkRenderer->makeKnownLink(
180            $this->getTitle( "history/$id/diff/prev/cur" ),
181            $text
182        );
183    }
184
185    /**
186     * Adds an unrecoverable error message and a button to return to the current page.
187     *
188     * @param string|string[]|MessageSpecifier $key See `ContextSource::msg()`
189     */
190    protected function showUnrecoverableError( string|array|MessageSpecifier $key ): void {
191        $out = $this->getOutput();
192        $out->enableOOUI();
193        $out->addModuleStyles( 'mediawiki.codex.messagebox.styles' );
194        $out->addHTML( Html::errorBox( $this->msg( $key )->parseAsBlock() ) );
195        $href = $this->getTitle()->getFullURL();
196        $btn = new OOUI\ButtonWidget( [
197            'label' => $this->msg( 'abusefilter-return' )->text(),
198            'href' => $href
199        ] );
200        $out->addHTML( (string)$btn );
201    }
202
203}