Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
98.35% covered (success)
98.35%
119 / 121
85.71% covered (warning)
85.71%
6 / 7
CRAP
0.00% covered (danger)
0.00%
0 / 1
RCVariableGenerator
98.35% covered (success)
98.35%
119 / 121
85.71% covered (warning)
85.71%
6 / 7
20
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
1
 getVars
90.48% covered (success)
90.48%
19 / 21
0.00% covered (danger)
0.00%
0 / 1
9.07
 addMoveVars
100.00% covered (success)
100.00%
14 / 14
100.00% covered (success)
100.00%
1 / 1
1
 addCreateAccountVars
100.00% covered (success)
100.00%
17 / 17
100.00% covered (success)
100.00%
1 / 1
4
 addDeleteVars
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
1
 addUploadVars
100.00% covered (success)
100.00%
29 / 29
100.00% covered (success)
100.00%
1 / 1
2
 addEditVarsForRow
100.00% covered (success)
100.00%
27 / 27
100.00% covered (success)
100.00%
1 / 1
2
1<?php
2
3namespace MediaWiki\Extension\AbuseFilter\VariableGenerator;
4
5use LogicException;
6use MediaWiki\Extension\AbuseFilter\Hooks\AbuseFilterHookRunner;
7use MediaWiki\Extension\AbuseFilter\Variables\VariableHolder;
8use MediaWiki\Logger\LoggerFactory;
9use MediaWiki\Page\WikiPageFactory;
10use MediaWiki\Title\Title;
11use MediaWiki\User\User;
12use MediaWiki\User\UserFactory;
13use MimeAnalyzer;
14use MWFileProps;
15use RecentChange;
16use RepoGroup;
17
18/**
19 * This class contains the logic used to create variable holders used to
20 * examine a RecentChanges row.
21 */
22class RCVariableGenerator extends VariableGenerator {
23    /**
24     * @var RecentChange
25     */
26    private $rc;
27
28    /** @var User */
29    private $contextUser;
30
31    /** @var MimeAnalyzer */
32    private $mimeAnalyzer;
33    /** @var RepoGroup */
34    private $repoGroup;
35    /** @var WikiPageFactory */
36    private $wikiPageFactory;
37
38    /**
39     * @param AbuseFilterHookRunner $hookRunner
40     * @param UserFactory $userFactory
41     * @param MimeAnalyzer $mimeAnalyzer
42     * @param RepoGroup $repoGroup
43     * @param WikiPageFactory $wikiPageFactory
44     * @param RecentChange $rc
45     * @param User $contextUser
46     * @param VariableHolder|null $vars
47     */
48    public function __construct(
49        AbuseFilterHookRunner $hookRunner,
50        UserFactory $userFactory,
51        MimeAnalyzer $mimeAnalyzer,
52        RepoGroup $repoGroup,
53        WikiPageFactory $wikiPageFactory,
54        RecentChange $rc,
55        User $contextUser,
56        VariableHolder $vars = null
57    ) {
58        parent::__construct( $hookRunner, $userFactory, $vars );
59
60        $this->mimeAnalyzer = $mimeAnalyzer;
61        $this->repoGroup = $repoGroup;
62        $this->wikiPageFactory = $wikiPageFactory;
63        $this->rc = $rc;
64        $this->contextUser = $contextUser;
65    }
66
67    /**
68     * @return VariableHolder|null
69     */
70    public function getVars(): ?VariableHolder {
71        if ( $this->rc->getAttribute( 'rc_type' ) == RC_LOG ) {
72            switch ( $this->rc->getAttribute( 'rc_log_type' ) ) {
73                case 'move':
74                    $this->addMoveVars();
75                    break;
76                case 'newusers':
77                    $this->addCreateAccountVars();
78                    break;
79                case 'delete':
80                    $this->addDeleteVars();
81                    break;
82                case 'upload':
83                    $this->addUploadVars();
84                    break;
85                default:
86                    return null;
87            }
88        } elseif ( $this->rc->getAttribute( 'rc_this_oldid' ) ) {
89            // It's an edit (or a page creation).
90            $this->addEditVarsForRow();
91        } elseif (
92            !$this->hookRunner->onAbuseFilterGenerateVarsForRecentChange(
93                $this, $this->rc, $this->vars, $this->contextUser )
94        ) {
95            // @codeCoverageIgnoreStart
96            throw new LogicException( 'Cannot understand the given recentchanges row!' );
97            // @codeCoverageIgnoreEnd
98        }
99
100        $this->addGenericVars( $this->rc );
101
102        return $this->vars;
103    }
104
105    /**
106     * @return $this
107     */
108    private function addMoveVars(): self {
109        $userIdentity = $this->rc->getPerformerIdentity();
110
111        $oldTitle = $this->rc->getTitle();
112        $newTitle = Title::newFromText( $this->rc->getParam( '4::target' ) );
113
114        $this->addUserVars( $userIdentity, $this->rc )
115            ->addTitleVars( $oldTitle, 'moved_from', $this->rc )
116            ->addTitleVars( $newTitle, 'moved_to', $this->rc );
117
118        $this->vars->setVar( 'summary', $this->rc->getAttribute( 'rc_comment' ) );
119        $this->vars->setVar( 'action', 'move' );
120
121        $this->vars->setLazyLoadVar(
122            'moved_from_last_edit_age',
123            'previous-revision-age',
124            // rc_last_oldid is zero (RecentChange::newLogEntry)
125            [ 'revid' => $this->rc->getAttribute( 'rc_this_oldid' ) ]
126        );
127        // TODO: add moved_to_last_edit_age (is it possible?)
128        // TODO: add old_wikitext etc. (T320347)
129
130        return $this;
131    }
132
133    /**
134     * @return $this
135     */
136    private function addCreateAccountVars(): self {
137        $this->vars->setVar(
138            'action',
139            $this->rc->getAttribute( 'rc_log_action' ) === 'autocreate'
140                ? 'autocreateaccount'
141                : 'createaccount'
142        );
143
144        $name = $this->rc->getTitle()->getText();
145        // Add user data if the account was created by a registered user
146        $userIdentity = $this->rc->getPerformerIdentity();
147        if ( $userIdentity->isRegistered() && $name !== $userIdentity->getName() ) {
148            $this->addUserVars( $userIdentity, $this->rc );
149        } else {
150            // Set the user_type so that creations of temporary accounts vs named accounts can be filtered for an
151            // abuse filter that matches account creations.
152            $this->vars->setLazyLoadVar(
153                'user_type',
154                'user-type',
155                [ 'user-identity' => $userIdentity ]
156            );
157        }
158
159        $this->vars->setVar( 'accountname', $name );
160
161        return $this;
162    }
163
164    /**
165     * @return $this
166     */
167    private function addDeleteVars(): self {
168        $title = $this->rc->getTitle();
169        $userIdentity = $this->rc->getPerformerIdentity();
170
171        $this->addUserVars( $userIdentity, $this->rc )
172            ->addTitleVars( $title, 'page', $this->rc );
173
174        $this->vars->setVar( 'action', 'delete' );
175        $this->vars->setVar( 'summary', $this->rc->getAttribute( 'rc_comment' ) );
176        // TODO: add page_last_edit_age
177        // TODO: add old_wikitext etc. (T173663)
178
179        return $this;
180    }
181
182    /**
183     * @return $this
184     */
185    private function addUploadVars(): self {
186        $title = $this->rc->getTitle();
187        $userIdentity = $this->rc->getPerformerIdentity();
188
189        $this->addUserVars( $userIdentity, $this->rc )
190            ->addTitleVars( $title, 'page', $this->rc );
191
192        $this->vars->setVar( 'action', 'upload' );
193        $this->vars->setVar( 'summary', $this->rc->getAttribute( 'rc_comment' ) );
194
195        $this->vars->setLazyLoadVar(
196            'page_last_edit_age',
197            'previous-revision-age',
198            // rc_last_oldid is zero (RecentChange::newLogEntry)
199            [ 'revid' => $this->rc->getAttribute( 'rc_this_oldid' ) ]
200        );
201
202        $time = $this->rc->getParam( 'img_timestamp' );
203        $file = $this->repoGroup->findFile(
204            $title, [ 'time' => $time, 'private' => $this->contextUser ]
205        );
206        if ( !$file ) {
207            // @fixme Ensure this cannot happen!
208            // @codeCoverageIgnoreStart
209            $logger = LoggerFactory::getInstance( 'AbuseFilter' );
210            $logger->warning( "Cannot find file from RC row with title $title" );
211            return $this;
212            // @codeCoverageIgnoreEnd
213        }
214
215        // This is the same as FilteredActionsHandler::filterUpload, but from a different source
216        $this->vars->setVar( 'file_sha1', \Wikimedia\base_convert( $file->getSha1(), 36, 16, 40 ) );
217        $this->vars->setVar( 'file_size', $file->getSize() );
218
219        $this->vars->setVar( 'file_mime', $file->getMimeType() );
220        $this->vars->setVar(
221            'file_mediatype',
222            $this->mimeAnalyzer->getMediaType( null, $file->getMimeType() )
223        );
224        $this->vars->setVar( 'file_width', $file->getWidth() );
225        $this->vars->setVar( 'file_height', $file->getHeight() );
226
227        $mwProps = new MWFileProps( $this->mimeAnalyzer );
228        $bits = $mwProps->getPropsFromPath( $file->getLocalRefPath(), true )['bits'];
229        $this->vars->setVar( 'file_bits_per_channel', $bits );
230
231        return $this;
232    }
233
234    /**
235     * @return $this
236     */
237    private function addEditVarsForRow(): self {
238        $title = $this->rc->getTitle();
239        $userIdentity = $this->rc->getPerformerIdentity();
240
241        $this->addUserVars( $userIdentity, $this->rc )
242            ->addTitleVars( $title, 'page', $this->rc );
243
244        $this->vars->setVar( 'action', 'edit' );
245        $this->vars->setVar( 'summary', $this->rc->getAttribute( 'rc_comment' ) );
246
247        $this->vars->setLazyLoadVar( 'new_wikitext', 'revision-text-by-id',
248            [ 'revid' => $this->rc->getAttribute( 'rc_this_oldid' ), 'contextUser' => $this->contextUser ] );
249        $this->vars->setLazyLoadVar( 'new_content_model', 'content-model-by-id',
250            [ 'revid' => $this->rc->getAttribute( 'rc_this_oldid' ) ] );
251
252        $parentId = $this->rc->getAttribute( 'rc_last_oldid' );
253        if ( $parentId ) {
254            $this->vars->setLazyLoadVar( 'old_wikitext', 'revision-text-by-id',
255                [ 'revid' => $parentId, 'contextUser' => $this->contextUser ] );
256            $this->vars->setLazyLoadVar( 'old_content_model', 'content-model-by-id',
257                [ 'revid' => $parentId ] );
258            $this->vars->setLazyLoadVar( 'page_last_edit_age', 'revision-age-by-id',
259                [ 'revid' => $parentId, 'asof' => $this->rc->getAttribute( 'rc_timestamp' ) ] );
260        } else {
261            $this->vars->setVar( 'old_wikitext', '' );
262            $this->vars->setVar( 'old_content_model', '' );
263            $this->vars->setVar( 'page_last_edit_age', null );
264        }
265
266        $this->addEditVars(
267            $this->wikiPageFactory->newFromTitle( $title ),
268            $this->contextUser,
269            false
270        );
271
272        return $this;
273    }
274}