Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
74.39% covered (warning)
74.39%
61 / 82
61.54% covered (warning)
61.54%
8 / 13
CRAP
0.00% covered (danger)
0.00%
0 / 1
Util
74.39% covered (warning)
74.39%
61 / 82
61.54% covered (warning)
61.54%
8 / 13
43.13
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
70.59% covered (warning)
70.59%
12 / 17
0.00% covered (danger)
0.00%
0 / 1
4.41
 getRawUrl
77.78% covered (warning)
77.78%
7 / 9
0.00% covered (danger)
0.00%
0 / 1
4.18
 getRevertThreshold
73.33% covered (warning)
73.33%
11 / 15
0.00% covered (danger)
0.00%
0 / 1
4.30
 getMultiLingualThreshold
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 isMultiLingualRevertRiskEnabled
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 initializeLiftWingClient
100.00% covered (success)
100.00%
12 / 12
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
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     *
54     * @return string The name that ORES uses for the language-agnostic model
55     */
56    public static function getORESLanguageAgnosticModelName() {
57        return 'revertrisklanguageagnostic';
58    }
59
60    /**
61     *
62     * @return string The name that ORES uses for the multilingual model
63     */
64    public static function getORESMultiLingualModelName() {
65        // TODO: This hasn't been added to ORES; check if the name matches the model name when it is
66        return 'revertriskmultilingual';
67    }
68
69    /**
70     * Returns the revert risk model the revision will be scored from
71     * @param Config $config
72     * @param Config $wikiConfig
73     * @return string
74     */
75    public static function getRevertRiskModel( Config $config, Config $wikiConfig ) {
76        $languageAgnosticModel = self::getORESLanguageAgnosticModelName();
77        $multiLingualModel = self::getORESMultiLingualModelName();
78
79        $isLanguageModelEnabledConfig = self::isMultiLingualRevertRiskEnabled( $config, $wikiConfig );
80
81        // Check if a multilingual configuration exists and is enabled
82        if ( $isLanguageModelEnabledConfig ) {
83            return $multiLingualModel;
84        } else {
85            return $languageAgnosticModel;
86        }
87    }
88
89    /**
90     * Get a user to perform moderation actions.
91     * @param Config $config
92     * @param UserGroupManager $userGroupManager
93     *
94     * @return User
95     */
96    public static function getAutoModeratorUser( $config, $userGroupManager ): User {
97        $username = $config->get( 'AutoModeratorUsername' );
98        $autoModeratorUser = User::newSystemUser( $username, [ 'steal' => true ] );
99        '@phan-var User $autoModeratorUser';
100        if ( !$autoModeratorUser ) {
101            throw new UnexpectedValueException(
102                "{$username} is invalid. Please change it."
103            );
104        }
105        // Assign the 'bot' group to the user, so that it looks like a bot
106        if ( !in_array( 'bot', $userGroupManager->getUserGroups( $autoModeratorUser ) ) ) {
107            $userGroupManager->addUserToGroup( $autoModeratorUser, 'bot' );
108        }
109        return $autoModeratorUser;
110    }
111
112    /**
113     * Fetch JSON data from a remote URL, parse it and return the results.
114     * @param HttpRequestFactory $requestFactory
115     * @param string $url
116     * @param bool $isSameFarm Is the URL on the same wiki farm we are making the request from?
117     * @return StatusValue A status object with the parsed JSON value, or any errors.
118     *   (Warnings coming from the HTTP library will be logged and not included here.)
119     */
120    public static function getJsonUrl(
121        HttpRequestFactory $requestFactory, $url, $isSameFarm = false
122    ): StatusValue {
123        $options = [
124            'method' => 'GET',
125            'userAgent' => $requestFactory->getUserAgent() . ' AutoModerator',
126        ];
127        if ( $isSameFarm ) {
128            $options['originalRequest'] = RequestContext::getMain()->getRequest();
129        }
130        $request = $requestFactory->create( $url, $options, __METHOD__ );
131        $status = $request->execute();
132        if ( $status->isOK() ) {
133            $status->merge( FormatJson::parse( $request->getContent(), FormatJson::FORCE_ASSOC ), true );
134        }
135        // Log warnings here. The caller is expected to handle errors so do not double-log them.
136        [ $errorStatus, $warningStatus ] = $status->splitByErrorType();
137        if ( !$warningStatus->isGood() ) {
138            // @todo replace 'en' with correct language configuration
139            LoggerFactory::getInstance( 'AutoModerator' )->warning(
140                $warningStatus->getWikiText( false, false, 'en' ),
141                [ 'exception' => new RuntimeException ]
142            );
143        }
144        return $errorStatus;
145    }
146
147    /**
148     * Get the action=raw URL for a (probably remote) title.
149     * Normal title methods would return nice URLs, which are usually disallowed for action=raw.
150     * We assume both wikis use the same URL structure.
151     * @param LinkTarget $title
152     * @param TitleFactory $titleFactory
153     * @return string
154     */
155    public static function getRawUrl(
156        LinkTarget $title,
157        TitleFactory $titleFactory,
158        UrlUtils $urlUtils
159    ): string {
160        // Use getFullURL to get the interwiki domain.
161        $url = $titleFactory->newFromLinkTarget( $title )->getFullURL();
162        $parts = $urlUtils->parse( (string)$urlUtils->expand( $url, PROTO_CANONICAL ) );
163        if ( !$parts ) {
164            throw new UnexpectedValueException( 'URL is expected to be valid' );
165        }
166        $baseUrl = $parts['scheme'] . $parts['delimiter'] . $parts['host'];
167        if ( isset( $parts['port'] ) && $parts['port'] ) {
168            $baseUrl .= ':' . $parts['port'];
169        }
170
171        $localPageTitle = $titleFactory->makeTitle( $title->getNamespace(), $title->getDBkey() );
172        return $baseUrl . $localPageTitle->getLocalURL( [ 'action' => 'raw' ] );
173    }
174
175    /**
176     * @param Config $wikiConfig
177     * @param string $revertRiskModelName
178     * @return float An AutoModeratorRevertProbability threshold  will be chosen depending on the model
179     */
180    public static function getRevertThreshold( Config $wikiConfig, string $revertRiskModelName ): float {
181        $threshold = 0.990;
182        if ( $wikiConfig->has( 'AutoModeratorCautionLevel' ) ) {
183            $cautionLevel = $wikiConfig->get( 'AutoModeratorCautionLevel' );
184        } else {
185            $cautionLevel = $wikiConfig->get( 'AutoModeratorMultilingualConfigCautionLevel' );
186        }
187        if ( $revertRiskModelName === self::getORESLanguageAgnosticModelName() ) {
188            $languageAgnosticThresholds = [
189                'very-cautious' => 0.990,
190                'cautious' => 0.985,
191                'somewhat-cautious' => 0.980,
192                'less-cautious' => 0.975
193            ];
194            return $languageAgnosticThresholds[ $cautionLevel ];
195        } elseif ( $revertRiskModelName === self::getORESMultiLingualModelName() ) {
196            return self::getMultiLingualThreshold( $wikiConfig );
197        } else {
198            return $threshold;
199        }
200    }
201
202    /**
203     * Returns the revert risk model the revision will be scored from
204     * @param Config $wikiConfig
205     * @return float
206     */
207    public static function getMultiLingualThreshold( Config $wikiConfig ) {
208        return $wikiConfig->get( 'AutoModeratorMultilingualConfigMultilingualThreshold' );
209    }
210
211    /**
212     * Checks if multilingual revert risk is enabled on the wiki
213     * See: https://meta.wikimedia.org/wiki/Machine_learning_models/Production/Multilingual_revert_risk#Motivation
214     * for more information
215     * @param Config $config
216     * @param Config $wikiConfig
217     * @return bool
218     */
219    public static function isMultiLingualRevertRiskEnabled( Config $config, Config $wikiConfig ): bool {
220        $wikiId = self::getWikiID( $config );
221        $multiLingualCompatibleWikis = $config->get( 'AutoModeratorMultiLingualRevertRisk' );
222        return $wikiConfig->get( 'AutoModeratorMultilingualConfigEnableMultilingual' ) &&
223            in_array( $wikiId, $multiLingualCompatibleWikis );
224    }
225
226    /**
227     * @param Config $config
228     * @param Config $wikiConfig
229     * @return LiftWingClient
230     */
231    public static function initializeLiftWingClient( Config $config, Config $wikiConfig ): LiftWingClient {
232        $isMultiLingualModelEnabled = self::isMultiLingualRevertRiskEnabled( $config, $wikiConfig );
233        if ( $isMultiLingualModelEnabled ) {
234            $model = 'revertrisk-multilingual';
235        } else {
236            $model = 'revertrisk-language-agnostic';
237        }
238
239        $lang = self::getLanguageConfiguration( $config );
240        $hostHeader = $config->get( 'AutoModeratorLiftWingAddHostHeader' ) ?
241            $config->get( 'AutoModeratorLiftWingRevertRiskHostHeader' ) : null;
242        return new LiftWingClient(
243            $model,
244            $lang,
245            $config->get( 'AutoModeratorLiftWingBaseUrl' ),
246            $hostHeader );
247    }
248
249    /**
250     * @return ApiClient
251     */
252    public static function initializeApiClient(): ApiClient {
253        return new ApiClient();
254    }
255
256    /**
257     * @param Config $config
258     * @return false|string
259     */
260    public static function getLanguageConfiguration( Config $config ) {
261        $wikiId = self::getWikiID( $config );
262        return substr( $wikiId, 0, strpos( $wikiId, "wiki" ) );
263    }
264}