Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
38.02% covered (danger)
38.02%
46 / 121
38.46% covered (danger)
38.46%
10 / 26
CRAP
0.00% covered (danger)
0.00%
0 / 1
Util
38.02% covered (danger)
38.02%
46 / 121
38.46% covered (danger)
38.46%
10 / 26
802.80
0.00% covered (danger)
0.00%
0 / 1
 getWikiID
75.00% covered (warning)
75.00%
3 / 4
0.00% covered (danger)
0.00%
0 / 1
2.06
 getORESLanguageAgnosticModelName
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getORESMultiLingualModelName
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getRevertRiskModel
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
2
 getAutoModeratorUser
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
12
 getJsonUrl
0.00% covered (danger)
0.00%
0 / 17
0.00% covered (danger)
0.00%
0 / 1
20
 getRawUrl
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
20
 getRevertThreshold
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 getMultiLingualThreshold
50.00% covered (danger)
50.00%
2 / 4
0.00% covered (danger)
0.00%
0 / 1
2.50
 isWikiMultilingual
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 isMultiLingualRevertRiskEnabled
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
5
 getFalsePositivePageTitleText
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 getMaxRevertsEnabled
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 getMaxReverts
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 getSkipUserRights
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 getUseEditFlagMinor
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 getEnableBotFlag
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 getHelpPageLink
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 getRevertTalkPageMessageEnabled
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 getRevertTalkPageMessageRegisteredUsersOnly
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 getEnableRevisionCheck
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 getEnableLogOnlyMode
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 initializeLiftWingClient
100.00% covered (success)
100.00%
13 / 13
100.00% covered (success)
100.00%
1 / 1
3
 initializeApiClient
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getLanguageConfiguration
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 getCautionLevel
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2/**
3 * This program is free software; you can redistribute it and/or modify
4 * it under the terms of the GNU General Public License as published by
5 * the Free Software Foundation; either version 2 of the License, or
6 * (at your option) any later version.
7 *
8 * This program is distributed in the hope that it will be useful,
9 * but WITHOUT ANY WARRANTY; without even the implied warranty of
10 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11 * GNU General Public License for more details.
12 *
13 * You should have received a copy of the GNU General Public License along
14 * with this program; if not, write to the Free Software Foundation, Inc.,
15 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
16 *
17 * @file
18 */
19
20namespace AutoModerator;
21
22use MediaWiki\Config\Config;
23use MediaWiki\Context\RequestContext;
24use MediaWiki\Http\HttpRequestFactory;
25use MediaWiki\Json\FormatJson;
26use MediaWiki\Linker\LinkTarget;
27use MediaWiki\Logger\LoggerFactory;
28use MediaWiki\Title\TitleFactory;
29use MediaWiki\User\User;
30use MediaWiki\User\UserGroupManager;
31use MediaWiki\Utils\UrlUtils;
32use MediaWiki\WikiMap\WikiMap;
33use RuntimeException;
34use StatusValue;
35use UnexpectedValueException;
36
37class Util {
38
39    /**
40     * @param Config $config
41     *
42     * @return string Wiki ID used by AutoModerator.
43     */
44    public static function getWikiID( $config ): string {
45        $autoModeratorWikiId = $config->get( 'AutoModeratorWikiId' );
46        if ( $autoModeratorWikiId ) {
47            return $autoModeratorWikiId;
48        }
49        return WikiMap::getCurrentWikiId();
50    }
51
52    /**
53     * @return string The name that ORES uses for the language-agnostic model
54     */
55    public static function getORESLanguageAgnosticModelName() {
56        return 'revertrisklanguageagnostic';
57    }
58
59    /**
60     * @return string The name that ORES uses for the multilingual model
61     */
62    public static function getORESMultiLingualModelName() {
63        // TODO: This hasn't been added to ORES; check if the name matches the model name when it is
64        return 'revertriskmultilingual';
65    }
66
67    /**
68     * Returns the revert risk model the revision will be scored from
69     * @param Config $config
70     * @return string
71     */
72    public static function getRevertRiskModel( Config $config ) {
73        $languageAgnosticModel = self::getORESLanguageAgnosticModelName();
74        $multiLingualModel = self::getORESMultiLingualModelName();
75
76        $isLanguageModelEnabledConfig = self::isMultiLingualRevertRiskEnabled( $config );
77
78        // Check if a multilingual configuration exists and is enabled
79        if ( $isLanguageModelEnabledConfig ) {
80            return $multiLingualModel;
81        } else {
82            return $languageAgnosticModel;
83        }
84    }
85
86    /**
87     * Get a user to perform moderation actions.
88     * @param Config $config
89     * @param UserGroupManager $userGroupManager
90     *
91     * @return User
92     */
93    public static function getAutoModeratorUser( $config, $userGroupManager ): User {
94        $username = $config->get( 'AutoModeratorUsername' );
95        $autoModeratorUser = User::newSystemUser( $username, [ 'steal' => true ] );
96        '@phan-var User $autoModeratorUser';
97        if ( !$autoModeratorUser ) {
98            throw new UnexpectedValueException(
99                "{$username} is invalid. Please change it."
100            );
101        }
102        // Assign the 'bot' group to the user, so that it looks like a bot
103        if ( !in_array( 'bot', $userGroupManager->getUserGroups( $autoModeratorUser ) ) ) {
104            $userGroupManager->addUserToGroup( $autoModeratorUser, 'bot' );
105        }
106        return $autoModeratorUser;
107    }
108
109    /**
110     * Fetch JSON data from a remote URL, parse it and return the results.
111     * @param HttpRequestFactory $requestFactory
112     * @param string $url
113     * @param bool $isSameFarm Is the URL on the same wiki farm we are making the request from?
114     * @return StatusValue A status object with the parsed JSON value, or any errors.
115     *   (Warnings coming from the HTTP library will be logged and not included here.)
116     */
117    public static function getJsonUrl(
118        HttpRequestFactory $requestFactory, $url, $isSameFarm = false
119    ): StatusValue {
120        $options = [
121            'method' => 'GET',
122            'userAgent' => $requestFactory->getUserAgent() . ' AutoModerator',
123        ];
124        if ( $isSameFarm ) {
125            $options['originalRequest'] = RequestContext::getMain()->getRequest();
126        }
127        $request = $requestFactory->create( $url, $options, __METHOD__ );
128        $status = $request->execute();
129        if ( $status->isOK() ) {
130            $status->merge( FormatJson::parse( $request->getContent(), FormatJson::FORCE_ASSOC ), true );
131        }
132        // Log warnings here. The caller is expected to handle errors so do not double-log them.
133        [ $errorStatus, $warningStatus ] = $status->splitByErrorType();
134        if ( !$warningStatus->isGood() ) {
135            // @todo replace 'en' with correct language configuration
136            LoggerFactory::getInstance( 'AutoModerator' )->warning(
137                $warningStatus->getWikiText( false, false, 'en' ),
138                [ 'exception' => new RuntimeException ]
139            );
140        }
141        return $errorStatus;
142    }
143
144    /**
145     * Get the action=raw URL for a (probably remote) title.
146     * Normal title methods would return nice URLs, which are usually disallowed for action=raw.
147     * We assume both wikis use the same URL structure.
148     * @param LinkTarget $title
149     * @param TitleFactory $titleFactory
150     * @return string
151     */
152    public static function getRawUrl(
153        LinkTarget $title,
154        TitleFactory $titleFactory,
155        UrlUtils $urlUtils
156    ): string {
157        // Use getFullURL to get the interwiki domain.
158        $url = $titleFactory->newFromLinkTarget( $title )->getFullURL();
159        $parts = $urlUtils->parse( (string)$urlUtils->expand( $url, PROTO_CANONICAL ) );
160        if ( !$parts ) {
161            throw new UnexpectedValueException( 'URL is expected to be valid' );
162        }
163        $baseUrl = $parts['scheme'] . $parts['delimiter'] . $parts['host'];
164        if ( isset( $parts['port'] ) && $parts['port'] ) {
165            $baseUrl .= ':' . $parts['port'];
166        }
167
168        $localPageTitle = $titleFactory->makeTitle( $title->getNamespace(), $title->getDBkey() );
169        return $baseUrl . $localPageTitle->getLocalURL( [ 'action' => 'raw' ] );
170    }
171
172    /**
173     * @param Config $config
174     * @return float An AutoModeratorRevertProbability threshold  will be chosen depending on the model
175     */
176    public static function getRevertThreshold( Config $config ): float {
177        if ( self::isWikiMultilingual( $config ) ) {
178            return self::getMultiLingualThreshold( $config );
179        } else {
180            $cautionLevel = $config->get( 'AutoModeratorCautionLevel' );
181            return self::getCautionLevel( $cautionLevel );
182        }
183    }
184
185    /**
186     * Returns the revert risk model the revision will be scored from
187     * @param Config $config
188     * @return float
189     */
190    public static function getMultiLingualThreshold( Config $config ) {
191        if ( $config->get( 'AutoModeratorMultilingualConfigMultilingualThreshold' ) ) {
192            return $config->get( 'AutoModeratorMultilingualConfigMultilingualThreshold' );
193        }
194        $cautionLevel = $config->get( 'AutoModeratorMultilingualConfigCautionLevel' );
195        return self::getCautionLevel( $cautionLevel );
196    }
197
198    /**
199     * Checks if multilingual revert risk is enabled on the wiki
200     * See: https://meta.wikimedia.org/wiki/Machine_learning_models/Production/Multilingual_revert_risk#Motivation
201     * for more information
202     * @param Config $config
203     * @return bool
204     */
205    public static function isWikiMultilingual( Config $config ): bool {
206        return $config->has( 'AutoModeratorMultiLingualRevertRisk' ) &&
207            $config->get( 'AutoModeratorMultiLingualRevertRisk' );
208    }
209
210    /**
211     * Checks if multilingual revert risk is enabled on the wiki
212     * See: https://meta.wikimedia.org/wiki/Machine_learning_models/Production/Multilingual_revert_risk#Motivation
213     * for more information
214     * @param Config $config
215     * @return bool
216     */
217    public static function isMultiLingualRevertRiskEnabled( Config $config ): bool {
218        return self::isWikiMultilingual( $config ) &&
219            (
220                $config->has( 'AutoModeratorMultilingualConfigEnableMultilingual' ) &&
221                $config->get( 'AutoModeratorMultilingualConfigEnableMultilingual' )
222            ) && (
223                !$config->has( 'AutoModeratorMultilingualConfigEnableLanguageAgnostic' ) ||
224                $config->get( 'AutoModeratorMultilingualConfigEnableLanguageAgnostic' ) !== true
225            );
226    }
227
228    /**
229     * @param Config $config
230     * @return mixed
231     */
232    public static function getFalsePositivePageTitleText( Config $config ): mixed {
233        return self::isWikiMultilingual( $config ) ?
234            $config->get( 'AutoModeratorMultilingualConfigFalsePositivePageTitle' ) :
235            $config->get( 'AutoModeratorFalsePositivePageTitle' );
236    }
237
238    /**
239     * @param Config $config
240     * @return mixed
241     */
242    public static function getMaxRevertsEnabled( Config $config ): mixed {
243        return self::isWikiMultilingual( $config ) ?
244            $config->get( 'AutoModeratorMultilingualConfigEnableUserRevertsPerPage' ) :
245            $config->get( 'AutoModeratorEnableUserRevertsPerPage' );
246    }
247
248    /**
249     * @param Config $config
250     * @return mixed
251     */
252    public static function getMaxReverts( Config $config ): mixed {
253        return self::isWikiMultilingual( $config ) ?
254            $config->get( 'AutoModeratorMultilingualConfigUserRevertsPerPage' ) :
255            $config->get( 'AutoModeratorUserRevertsPerPage' );
256    }
257
258    /**
259     * @param Config $config
260     * @return mixed
261     */
262    public static function getSkipUserRights( Config $config ): mixed {
263        return self::isWikiMultilingual( $config ) ?
264            $config->get( 'AutoModeratorMultilingualConfigSkipUserRights' ) :
265            $config->get( 'AutoModeratorSkipUserRights' );
266    }
267
268    /**
269     * @param Config $config
270     * @return mixed
271     */
272    public static function getUseEditFlagMinor( Config $config ): mixed {
273        return self::isWikiMultilingual( $config ) ?
274            $config->get( 'AutoModeratorMultilingualConfigUseEditFlagMinor' ) :
275            $config->get( 'AutoModeratorUseEditFlagMinor' );
276    }
277
278    /**
279     * @param Config $config
280     * @return mixed
281     */
282    public static function getEnableBotFlag( Config $config ): mixed {
283        return self::isWikiMultilingual( $config ) ?
284            $config->get( 'AutoModeratorMultilingualConfigEnableBotFlag' ) :
285            $config->get( 'AutoModeratorEnableBotFlag' );
286    }
287
288    /**
289     * @param Config $config
290     * @return mixed
291     */
292    public static function getHelpPageLink( Config $config ): mixed {
293        return self::isWikiMultilingual( $config ) ?
294            $config->get( 'AutoModeratorMultilingualConfigHelpPageLink' ) :
295            $config->get( 'AutoModeratorHelpPageLink' );
296    }
297
298    /**
299     * @param Config $config
300     * @return mixed
301     */
302    public static function getRevertTalkPageMessageEnabled( Config $config ): mixed {
303        return self::isWikiMultilingual( $config ) ?
304            $config->get( 'AutoModeratorMultilingualConfigRevertTalkPageMessageEnabled' ) :
305            $config->get( 'AutoModeratorRevertTalkPageMessageEnabled' );
306    }
307
308    /**
309     * @param Config $config
310     * @return mixed
311     */
312    public static function getRevertTalkPageMessageRegisteredUsersOnly( Config $config ): mixed {
313        return self::isWikiMultilingual( $config ) ?
314            $config->get( 'AutoModeratorMultilingualConfigRevertTalkPageMessageRegisteredUsersOnly' ) :
315            $config->get( 'AutoModeratorRevertTalkPageMessageRegisteredUsersOnly' );
316    }
317
318    /**
319     * @param Config $config
320     * @return mixed
321     */
322    public static function getEnableRevisionCheck( Config $config ): mixed {
323        return self::isWikiMultilingual( $config ) ?
324            $config->get( 'AutoModeratorMultilingualConfigEnableRevisionCheck' ) :
325            $config->get( 'AutoModeratorEnableRevisionCheck' );
326    }
327
328    /**
329     * @param Config $config
330     * @return mixed
331     */
332    public static function getEnableLogOnlyMode( Config $config ): mixed {
333        return self::isWikiMultilingual( $config ) ?
334            $config->get( 'AutoModeratorMultilingualEnableLogOnlyMode' ) :
335            $config->get( 'AutoModeratorEnableLogOnlyMode' );
336    }
337
338    /**
339     * @param Config $config
340     * @return LiftWingClient
341     */
342    public static function initializeLiftWingClient( Config $config ): LiftWingClient {
343        $isMultiLingualModelEnabled = self::isMultiLingualRevertRiskEnabled( $config );
344        if ( $isMultiLingualModelEnabled ) {
345            $model = 'revertrisk-multilingual';
346            $hostHeaderKey = 'AutoModeratorLiftWingMultiLingualRevertRiskHostHeader';
347        } else {
348            $model = 'revertrisk-language-agnostic';
349            $hostHeaderKey = 'AutoModeratorLiftWingRevertRiskHostHeader';
350        }
351        $hostHeader = $config->get( 'AutoModeratorLiftWingAddHostHeader' ) ? $config->get( $hostHeaderKey ) : null;
352        $lang = self::getLanguageConfiguration( $config );
353        return new LiftWingClient(
354            $model,
355            $lang,
356            $config->get( 'AutoModeratorLiftWingBaseUrl' ),
357            $hostHeader );
358    }
359
360    /**
361     * @return ApiClient
362     */
363    public static function initializeApiClient(): ApiClient {
364        return new ApiClient();
365    }
366
367    /**
368     * @param Config $config
369     * @return false|string
370     */
371    public static function getLanguageConfiguration( Config $config ) {
372        $wikiId = self::getWikiID( $config );
373        return substr( $wikiId, 0, strpos( $wikiId, "wiki" ) );
374    }
375
376    /**
377     * @param mixed $cautionLevel
378     * @return float
379     */
380    public static function getCautionLevel( mixed $cautionLevel ): float {
381        $languageAgnosticThresholds = [
382            'very-cautious' => 0.990,
383            'cautious' => 0.985,
384            'somewhat-cautious' => 0.980,
385            'less-cautious' => 0.975
386        ];
387        return $languageAgnosticThresholds[$cautionLevel];
388    }
389}