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