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