Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
97.58% covered (success)
97.58%
161 / 165
75.00% covered (warning)
75.00%
6 / 8
CRAP
0.00% covered (danger)
0.00%
0 / 1
RCVariableGenerator
97.58% covered (success)
97.58%
161 / 165
75.00% covered (warning)
75.00%
6 / 8
26
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
95.83% covered (success)
95.83%
23 / 24
0.00% covered (danger)
0.00%
0 / 1
5
 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 MediaWiki\Utils\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        if ( $createdUser === null ) {
131            throw new \UnexpectedValueException( 'Failed to retrieve the created user' );
132        }
133        $this->vars->setVar( 'account_name', $name );
134        $this->vars->setLazyLoadVar(
135            'account_type',
136            'account-type',
137            [ 'autocreate' => $autocreate, 'createdUser' => $createdUser ]
138        );
139
140        $this->hookRunner->onAbuseFilterGenerateAccountCreationVars(
141            $this->vars, $userIdentity, $createdUser, $autocreate, $this->rc
142        );
143
144        return $this;
145    }
146
147    /**
148     * @return $this
149     */
150    private function addDeleteVars(): self {
151        $title = Title::castFromPageReference( $this->rc->getPage() ) ?: Title::makeTitle( NS_SPECIAL, 'BadTitle' );
152        $userIdentity = $this->rc->getPerformerIdentity();
153
154        $this->addUserVars( $userIdentity, $this->rc )
155            ->addTitleVars( $title, 'page', $this->rc );
156
157        $this->vars->setVar( 'action', 'delete' );
158        $this->vars->setVar( 'summary', $this->rc->getAttribute( 'rc_comment' ) );
159        // TODO: add page_last_edit_age
160        // TODO: add old_wikitext etc. (T173663)
161
162        return $this;
163    }
164
165    /**
166     * @return $this
167     */
168    private function addUploadVars(): self {
169        $title = Title::castFromPageReference( $this->rc->getPage() ) ?: Title::makeTitle( NS_SPECIAL, 'BadTitle' );
170        $userIdentity = $this->rc->getPerformerIdentity();
171
172        $this->addUserVars( $userIdentity, $this->rc )
173            ->addTitleVars( $title, 'page', $this->rc );
174
175        $this->vars->setVar( 'action', 'upload' );
176        $this->vars->setVar( 'summary', $this->rc->getAttribute( 'rc_comment' ) );
177
178        $this->vars->setLazyLoadVar(
179            'page_last_edit_age',
180            'revision-age',
181            [
182                // rc_last_oldid is zero (RecentChangeFactory::createLogRecentChange)
183                'revid' => $this->rc->getAttribute( 'rc_this_oldid' ),
184                'parent' => true,
185                'asof' => $this->rc->getAttribute( 'rc_timestamp' ),
186            ]
187        );
188
189        $time = $this->rc->getParam( 'img_timestamp' );
190        $file = $this->repoGroup->findFile(
191            $title, [ 'time' => $time, 'private' => $this->contextUser ]
192        );
193        if ( !$file ) {
194            // @fixme Ensure this cannot happen!
195            // @codeCoverageIgnoreStart
196            $logger = LoggerFactory::getInstance( 'AbuseFilter' );
197            $logger->warning( "Cannot find file from RC row with title $title" );
198            return $this;
199            // @codeCoverageIgnoreEnd
200        }
201
202        // This is the same as FilteredActionsHandler::filterUpload, but from a different source
203        $this->vars->setVar( 'file_sha1', \Wikimedia\base_convert( $file->getSha1(), 36, 16, 40 ) );
204        $this->vars->setVar( 'file_size', $file->getSize() );
205
206        $this->vars->setVar( 'file_mime', $file->getMimeType() );
207        $this->vars->setVar(
208            'file_mediatype',
209            $this->mimeAnalyzer->getMediaType( null, $file->getMimeType() )
210        );
211        $this->vars->setVar( 'file_width', $file->getWidth() );
212        $this->vars->setVar( 'file_height', $file->getHeight() );
213
214        $mwProps = new MWFileProps( $this->mimeAnalyzer );
215        $bits = $mwProps->getPropsFromPath( $file->getLocalRefPath(), true )['bits'];
216        $this->vars->setVar( 'file_bits_per_channel', $bits );
217
218        $this->vars->setLazyLoadVar( 'new_wikitext', 'revision-text',
219            [ 'revid' => $this->rc->getAttribute( 'rc_this_oldid' ), 'contextUser' => $this->contextUser ] );
220        $this->vars->setLazyLoadVar( 'old_wikitext', 'revision-text',
221            [
222                // rc_last_oldid is zero (RecentChangeFactory::createLogRecentChange)
223                'revid' => $this->rc->getAttribute( 'rc_this_oldid' ),
224                'parent' => true,
225                'contextUser' => $this->contextUser,
226            ] );
227
228        $this->addDerivedVarsForTitle( $title );
229
230        return $this;
231    }
232
233    private function addDerivedVarsForTitle( Title $title ) {
234        $page = $this->wikiPageFactory->newFromTitle( $title );
235        $this->addDerivedEditVars();
236
237        // TODO: all these are legacy methods
238        $this->vars->setLazyLoadVar( 'new_links', 'links-from-wikitext',
239            [
240                'text-var' => 'new_wikitext',
241                'article' => $page,
242                'forFilter' => false,
243                'contextUserIdentity' => $this->contextUser
244            ] );
245
246        // Note: this claims "or database" but it will never reach it
247        $this->vars->setLazyLoadVar( 'old_links', 'links-from-wikitext-or-database',
248            [
249                'article' => $page,
250                'text-var' => 'old_wikitext',
251                'forFilter' => false,
252                'contextUserIdentity' => $this->contextUser
253            ] );
254
255        $this->vars->setLazyLoadVar( 'new_pst', 'parse-wikitext',
256            [
257                'wikitext-var' => 'new_wikitext',
258                'article' => $page,
259                'pst' => true,
260                'contextUserIdentity' => $this->contextUser
261            ] );
262
263        $this->vars->setLazyLoadVar( 'new_html', 'parse-wikitext',
264            [
265                'wikitext-var' => 'new_wikitext',
266                'article' => $page,
267                'contextUserIdentity' => $this->contextUser
268            ] );
269    }
270
271    /**
272     * @return $this
273     */
274    private function addEditVarsForRow(): self {
275        $title = Title::castFromPageReference( $this->rc->getPage() ) ?: Title::makeTitle( NS_SPECIAL, 'BadTitle' );
276        $userIdentity = $this->rc->getPerformerIdentity();
277
278        $this->addUserVars( $userIdentity, $this->rc )
279            ->addTitleVars( $title, 'page', $this->rc );
280
281        $this->vars->setVar( 'action', 'edit' );
282        $this->vars->setVar( 'summary', $this->rc->getAttribute( 'rc_comment' ) );
283
284        $this->vars->setLazyLoadVar( 'new_wikitext', 'revision-text',
285            [ 'revid' => $this->rc->getAttribute( 'rc_this_oldid' ), 'contextUser' => $this->contextUser ] );
286        $this->vars->setLazyLoadVar( 'new_content_model', 'content-model',
287            [ 'revid' => $this->rc->getAttribute( 'rc_this_oldid' ) ] );
288
289        $parentId = $this->rc->getAttribute( 'rc_last_oldid' );
290        if ( $parentId ) {
291            $this->vars->setLazyLoadVar( 'old_wikitext', 'revision-text',
292                [ 'revid' => $parentId, 'contextUser' => $this->contextUser ] );
293            $this->vars->setLazyLoadVar( 'old_content_model', 'content-model',
294                [ 'revid' => $parentId ] );
295            $this->vars->setLazyLoadVar( 'page_last_edit_age', 'revision-age',
296                [ 'revid' => $parentId, 'asof' => $this->rc->getAttribute( 'rc_timestamp' ) ] );
297        } else {
298            $this->vars->setVar( 'old_wikitext', '' );
299            $this->vars->setVar( 'old_content_model', '' );
300            $this->vars->setVar( 'page_last_edit_age', null );
301        }
302
303        $this->addDerivedVarsForTitle( $title );
304
305        return $this;
306    }
307}