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