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