Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
60.91% covered (warning)
60.91%
240 / 394
38.71% covered (danger)
38.71%
12 / 31
CRAP
0.00% covered (danger)
0.00%
0 / 1
PageTriageUtil
60.91% covered (warning)
60.91%
240 / 394
38.71% covered (danger)
38.71%
12 / 31
420.91
0.00% covered (danger)
0.00%
0 / 1
 isPageUnreviewed
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
6
 getStatus
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
2
 getNamespaces
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
3
 validatePageNamespace
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 getUnreviewedArticleStat
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getUnreviewedRedirectStat
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getUnreviewedPageStat
0.00% covered (danger)
0.00%
0 / 33
0.00% covered (danger)
0.00%
0 / 1
12
 getArticleFilterStat
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
12
 getReviewedArticleStat
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getReviewedRedirectStat
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getReviewedPageStat
0.00% covered (danger)
0.00%
0 / 30
0.00% covered (danger)
0.00%
0 / 1
12
 getUnreviewedDraftStats
95.24% covered (success)
95.24%
40 / 42
0.00% covered (danger)
0.00%
0 / 1
4
 userStatusKey
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
2
 pageStatusForUser
0.00% covered (danger)
0.00%
0 / 50
0.00% covered (danger)
0.00%
0 / 1
272
 updateMetadataOnBlockChange
91.43% covered (success)
91.43%
32 / 35
0.00% covered (danger)
0.00%
0 / 1
5.02
 createNotificationEvent
86.67% covered (warning)
86.67%
13 / 15
0.00% covered (danger)
0.00%
0 / 1
4.04
 truncateLongText
66.67% covered (warning)
66.67%
2 / 3
0.00% covered (danger)
0.00%
0 / 1
2.15
 getOresArticleQualityApiParams
100.00% covered (success)
100.00%
20 / 20
100.00% covered (success)
100.00%
1 / 1
1
 getOresDraftQualityApiParams
100.00% covered (success)
100.00%
14 / 14
100.00% covered (success)
100.00%
1 / 1
1
 getOresApiParams
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getCommonApiParams
100.00% covered (success)
100.00%
59 / 59
100.00% covered (success)
100.00%
1 / 1
1
 isOresArticleQualityQuery
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 isOresDraftQualityQuery
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 queryContains
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
4
 mapOresParamsToClassNames
100.00% covered (success)
100.00%
21 / 21
100.00% covered (success)
100.00%
1 / 1
4
 oresIsAvailable
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
12
 getCopyvioApiParam
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
2
 isCopyvioQuery
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getLinkCount
100.00% covered (success)
100.00%
17 / 17
100.00% covered (success)
100.00%
1 / 1
1
 getPrimaryConnection
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getReplicaConnection
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2
3namespace MediaWiki\Extension\PageTriage;
4
5use ApiRawMessage;
6use Exception;
7use ExtensionRegistry;
8use MediaWiki\Block\DatabaseBlock;
9use MediaWiki\Config\Config;
10use MediaWiki\Extension\Notifications\Model\Event;
11use MediaWiki\Extension\PageTriage\Api\ApiPageTriageList;
12use MediaWiki\Extension\PageTriage\ArticleCompile\ArticleCompileAfcTag;
13use MediaWiki\Linker\LinksMigration;
14use MediaWiki\MediaWikiServices;
15use MediaWiki\Title\Title;
16use MediaWiki\User\User;
17use MediaWiki\User\UserIdentity;
18use ORES\Hooks\Helpers;
19use RequestContext;
20use StatusValue;
21use Wikimedia\ParamValidator\ParamValidator;
22use Wikimedia\Rdbms\IDatabase;
23use Wikimedia\Rdbms\IReadableDatabase;
24use WikiPage;
25
26/**
27 * Utility class for PageTriage
28 */
29class PageTriageUtil {
30
31    /**
32     * Get whether a page needs triaging
33     *
34     * @param WikiPage $page
35     *
36     * @throws Exception
37     * @return bool|null Null if the page is not in the triage system,
38     * true if the page is unreviewed, false otherwise.
39     */
40    public static function isPageUnreviewed( WikiPage $page ): ?bool {
41        $queueLookup = PageTriageServices::wrap( MediaWikiServices::getInstance() )
42            ->getQueueLookup();
43        $queueRecord = $queueLookup->getByPageId( $page->getId() );
44        if ( !$queueRecord ) {
45            return null;
46        }
47        return $queueRecord->getReviewedStatus() === QueueRecord::REVIEW_STATUS_UNREVIEWED;
48    }
49
50    /**
51     * Return the ptrl_reviewed status of a page.
52     *
53     * @param WikiPage $page
54     *
55     * @throws Exception
56     * @return int|null 0 = unreviewed, 1 = marked as reviewed, 2 = marked as
57     * patrolled, 3 = autopatrolled, null = not in queue (treated as marked as
58     * reviewed)
59     */
60    public static function getStatus( WikiPage $page ) {
61        $queueLookup = PageTriageServices::wrap( MediaWikiServices::getInstance() )
62            ->getQueueLookup();
63        $queueRecord = $queueLookup->getByPageId( $page->getId() );
64        if ( !$queueRecord ) {
65            return null;
66        }
67        return $queueRecord->getReviewedStatus();
68    }
69
70    /**
71     * Get the IDs of applicable PageTriage namespaces.
72     * @param Config|null $config
73     * @return int[]
74     */
75    public static function getNamespaces( ?Config $config = null ): array {
76        $config ??= MediaWikiServices::getInstance()->getMainConfig();
77        $pageTriageDraftNamespaceId = $config->get( 'PageTriageDraftNamespaceId' );
78        $pageTriageNamespaces = $config->get( 'PageTriageNamespaces' );
79        // Add the Draft namespace if configured.
80        if ( $pageTriageDraftNamespaceId
81            && !in_array( $pageTriageDraftNamespaceId, $pageTriageNamespaces )
82        ) {
83            $pageTriageNamespaces[] = $pageTriageDraftNamespaceId;
84        }
85        return $pageTriageNamespaces;
86    }
87
88    /**
89     * Validate a page namespace ID.
90     * @param int $namespace The namespace ID to validate.
91     * @return int The provided namespace if valid, otherwise 0 (main namespace).
92     */
93    public static function validatePageNamespace( $namespace ) {
94        $pageTriageNamespaces = static::getNamespaces();
95        if ( !in_array( $namespace, $pageTriageNamespaces ) ) {
96            $namespace = NS_MAIN;
97        }
98
99        return (int)$namespace;
100    }
101
102    /**
103     * Get a list of stat for unreviewed articles
104     * @param int $namespace Namespace number
105     * @return array
106     *
107     */
108    public static function getUnreviewedArticleStat( $namespace = 0 ) {
109        return self::getUnreviewedPageStat( $namespace, false );
110    }
111
112    /**
113     * Get a list of stat for unreviewed redirects
114     * @param int $namespace Namespace number
115     * @return array
116     *
117     */
118    public static function getUnreviewedRedirectStat( $namespace = 0 ) {
119        return self::getUnreviewedPageStat( $namespace, true );
120    }
121
122    /**
123     * Get a list of stat for unreviewed pages
124     * @param int $namespace Namespace number
125     * @param bool $redirect
126     * @return array
127     *
128     * @todo - Limit the number of records by a timestamp filter, maybe 30 days etc,
129     *         depends on the time the triage queue should look back for listview
130     */
131    public static function getUnreviewedPageStat( $namespace = 0, $redirect = false ) {
132        $namespace = self::validatePageNamespace( $namespace );
133
134        $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
135        $fname = __METHOD__;
136
137        $key = ( $redirect ) ? 'pagetriage-unreviewed-redirects-stat' : 'pagetriage-unreviewed-articles-stat';
138
139        return $cache->getWithSetCallback(
140            $cache->makeKey( $key, $namespace ),
141            10 * $cache::TTL_MINUTE,
142            static function () use ( $namespace, $fname, $redirect ) {
143                $dbr = self::getReplicaConnection();
144
145                $conds = [
146                    'ptrp_reviewed' => 0,
147                    // remove redirect from the unreviewd number per bug40540
148                    'page_is_redirect' => (int)$redirect,
149                    // remove deletion nominations from stats per T205741
150                    'ptrp_deleted' => 0,
151                    'page_namespace' => $namespace
152                ];
153
154                $res = $dbr->newSelectQueryBuilder()
155                    ->select( [
156                        'total' => 'COUNT(ptrp_page_id)',
157                        'oldest' => 'MIN(ptrp_reviewed_updated)'
158                    ] )
159                    ->from( 'pagetriage_page' )
160                    ->join( 'page', null, 'page_id = ptrp_page_id' )
161                    ->where( $conds )
162                    ->caller( $fname )
163                    ->fetchRow();
164
165                $data = [ 'count' => 0, 'oldest' => '' ];
166
167                if ( $res ) {
168                    $data['count'] = (int)$res->total;
169                    $data['oldest'] = wfTimestamp( TS_ISO_8601, $res->oldest );
170                }
171
172                return $data;
173            },
174            [ 'version' => PageTriage::CACHE_VERSION ]
175        );
176    }
177
178    /**
179     * Get the number of pages based on the selected filters.
180     * @param array $filters Associative array of filter names/values.
181     *                       See ApiPageTriageStats->getAllowedParams() for possible values,
182     *                       which are the same that the ApiPageTriageList endpoint accepts.
183     * @return int Number of pages based on the selected filters
184     */
185    public static function getArticleFilterStat( $filters ) {
186        if ( !isset( $filters['showreviewed'] ) && !isset( $filters['showunreviewed'] ) ) {
187            $filters['showunreviewed'] = 'showunreviewed';
188        }
189
190        return ApiPageTriageList::getPageIds( $filters, true );
191    }
192
193    /**
194     * Get number of reviewed articles in the past week
195     * @param int $namespace Namespace number
196     * @return array Stats to be returned
197     */
198    public static function getReviewedArticleStat( $namespace = 0 ) {
199        return self::getReviewedPageStat( $namespace, false );
200    }
201
202    /**
203     * Get number of reviewed redirects in the past week
204     * @param int $namespace Namespace number
205     * @return array Stats to be returned
206     */
207    public static function getReviewedRedirectStat( $namespace = 0 ) {
208        return self::getReviewedPageStat( $namespace, true );
209    }
210
211    /**
212     * Get number of reviewed articles in the past week
213     * @param int $namespace Namespace number
214     * @param bool $redirect
215     * @return array Stats to be returned
216     */
217    public static function getReviewedPageStat( $namespace = 0, $redirect = false ) {
218        $namespace = self::validatePageNamespace( $namespace );
219
220        $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
221        $fname = __METHOD__;
222
223        $key = ( $redirect ) ? 'pagetriage-reviewed-redirects-stat' : 'pagetriage-reviewed-articles-stat';
224
225        return $cache->getWithSetCallback(
226            $cache->makeKey( $key, $namespace ),
227            10 * $cache::TTL_MINUTE,
228            static function () use ( $namespace, $fname, $redirect ) {
229                $time = (int)wfTimestamp( TS_UNIX ) - 7 * 24 * 60 * 60;
230
231                $dbr = self::getReplicaConnection();
232                $conds = [
233                    // T310108
234                    'ptrp_reviewed' => [ 1, 2 ],
235                    'page_namespace' => $namespace,
236                    'page_is_redirect' => (int)$redirect,
237                    $dbr->expr( 'ptrp_reviewed_updated', '>', $dbr->timestamp( $time ) ),
238                ];
239
240                $res = $dbr->newSelectQueryBuilder()
241                    ->select( [ 'reviewed_count' => 'COUNT(ptrp_page_id)' ] )
242                    ->from( 'pagetriage_page' )
243                    ->join( 'page', null, 'page_id = ptrp_page_id' )
244                    ->where( $conds )
245                    ->caller( $fname )
246                    ->fetchRow();
247
248                $data = [ 'reviewed_count' => 0 ];
249
250                if ( $res ) {
251                    $data['reviewed_count'] = (int)$res->reviewed_count;
252                }
253
254                return $data;
255            },
256            [ 'version' => PageTriage::CACHE_VERSION ]
257        );
258    }
259
260    /**
261     * Get number of drafts awaiting review and the age of the oldest submitted draft
262     *
263     * @return array ['count' => (int) number of unreviewed drafts,
264     * 'oldest' => (string) timestamp of oldest unreviewed draft].
265     * An empty array is returned if $wgPageTriageDraftNamespaceId is not enabled.
266     */
267    public static function getUnreviewedDraftStats(): array {
268        $config = MediaWikiServices::getInstance()->getMainConfig();
269        if ( !$config->get( 'PageTriageDraftNamespaceId' ) ) {
270            return [];
271        }
272
273        $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
274        $fname = __METHOD__;
275
276        return $cache->getWithSetCallback(
277            'pagetriage-unreviewed-drafts-stats',
278            // 10 minute cache
279            10 * $cache::TTL_MINUTE,
280            static function () use ( $fname, $config ) {
281                $dbr = self::getReplicaConnection();
282
283                $afcStateTagId = $dbr->newSelectQueryBuilder()
284                    ->select( 'ptrt_tag_id' )
285                    ->from( 'pagetriage_tags' )
286                    ->where( [ 'ptrt_tag_name' => 'afc_state' ] )
287                    ->caller( $fname )
288                    ->fetchField();
289
290                if ( !$afcStateTagId ) {
291                    return [];
292                }
293
294                $conds = [
295                    'ptrpt_tag_id' => $afcStateTagId,
296                    'ptrpt_value' => (string)ArticleCompileAfcTag::PENDING,
297                    'page_namespace' => $config->get( 'PageTriageDraftNamespaceId' )
298                ];
299
300                $res = $dbr->newSelectQueryBuilder()
301                    ->select( [
302                        'total' => 'COUNT(ptrp_page_id)',
303                        'oldest' => 'MIN(ptrp_reviewed_updated)',
304                    ] )
305                    ->from( 'page' )
306                    ->join( 'pagetriage_page', null, 'page_id = ptrp_page_id' )
307                    ->join( 'pagetriage_page_tags', null, 'ptrp_page_id = ptrpt_page_id' )
308                    ->where( $conds )
309                    ->caller( $fname )
310                    ->fetchRow();
311
312                $data = [];
313
314                if ( $res ) {
315                    $data['count'] = (int)$res->total;
316                    $data['oldest'] = wfTimestamp( TS_ISO_8601, $res->oldest );
317                }
318
319                return $data;
320            },
321            [ 'version' => PageTriage::CACHE_VERSION ]
322        );
323    }
324
325    /**
326     * returns the cache key for user status
327     * @param string $userName
328     * @return string
329     */
330    public static function userStatusKey( $userName ) {
331        $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
332
333        return $cache->makeKey(
334            'pagetriage-user-page-status',
335            sha1( $userName ),
336            PageTriage::CACHE_VERSION
337        );
338    }
339
340    /**
341     * Check the existance of user page and talk page for a list of users
342     * @param array $users contains user_name db keys
343     * @return array
344     */
345    public static function pageStatusForUser( $users ) {
346        $return = [];
347        $title  = [];
348
349        $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
350
351        foreach ( $users as $user ) {
352            $user = (array)$user;
353            $searchKey = [ 'user_name', 'reviewer' ];
354
355            foreach ( $searchKey as $val ) {
356                if ( !isset( $user[$val] ) || !$user[$val] ) {
357                    continue;
358                }
359                $data = $cache->get( self::userStatusKey( $user[$val] ) );
360                // data is in memcache
361                if ( $data !== false ) {
362                    foreach ( $data as $pageKey => $status ) {
363                        if ( $status === 1 ) {
364                            $return[$pageKey] = $status;
365                        }
366                    }
367                // data is not in memcache and will be checked against database
368                } else {
369                    $u = Title::newFromText( $user[$val], NS_USER );
370                    if ( $u ) {
371                        if ( isset( $title[$u->getDBkey()] ) ) {
372                            continue;
373                        }
374                        $t = Title::makeTitle( NS_USER_TALK, $u->getDBkey() );
375                        // store the data in $title, 'u' is for user page, 't' is for talk page
376                        $title[$u->getDBkey()] = [ 'user_name' => $user[$val], 'u' => $u, 't' => $t ];
377                    }
378                }
379            }
380        }
381
382        if ( $title ) {
383            $dbr = self::getReplicaConnection();
384            $res = $dbr->newSelectQueryBuilder()
385                ->select( [ 'page_namespace', 'page_title' ] )
386                ->from( 'page' )
387                ->where( [
388                    'page_title' => array_map( 'strval', array_keys( $title ) ),
389                    'page_namespace' => [ NS_USER, NS_USER_TALK ]
390                ] )
391                ->caller( __METHOD__ )
392                ->fetchResultSet();
393
394            $dataToCache = [];
395            // if there is result from the database, that means the page exists, set it to the
396            // cache array with value 1
397            foreach ( $res as $row ) {
398                $user = $title[$row->page_title];
399                if ( (int)$row->page_namespace === NS_USER ) {
400                    $dataToCache[$user['user_name']][$user['u']->getPrefixedDBkey()] = 1;
401                } else {
402                    $dataToCache[$user['user_name']][$user['t']->getPrefixedDBkey()] = 1;
403                }
404            }
405            // Loop through the original $title array, set the data not in db result with value 0
406            // then save the cache value to memcache for next time use
407            foreach ( $title as $key => $value ) {
408                if ( !isset( $dataToCache[$value['user_name']][$value['u']->getPrefixedDBkey()] ) ) {
409                    $dataToCache[$value['user_name']][$value['u']->getPrefixedDBkey()] = 0;
410                } else {
411                    $return[$value['u']->getPrefixedDBkey()] = 1;
412                }
413                if ( !isset( $dataToCache[$value['user_name']][$value['t']->getPrefixedDBkey()] ) ) {
414                    $dataToCache[$value['user_name']][$value['t']->getPrefixedDBkey()] = 0;
415                } else {
416                    $return[$value['t']->getPrefixedDBkey()] = 1;
417                }
418                $cache->set(
419                    self::userStatusKey( $value['user_name'] ),
420                    $dataToCache[$value['user_name']],
421                    3600
422                );
423            }
424        }
425
426        return $return;
427    }
428
429    /**
430     * Update user metadata when a user's block status is updated
431     * @param DatabaseBlock $block block object
432     * @param int $userBlockStatusToWrite 1/0
433     */
434    public static function updateMetadataOnBlockChange( $block, $userBlockStatusToWrite = 1 ) {
435        // do instant update if the number of page to be updated is less or equal to
436        // the number below, otherwise, delay this to the cron
437        $maxNumToProcess = 500;
438
439        $tags = ArticleMetadata::getValidTags();
440        if ( !$tags ) {
441            return;
442        }
443
444        $dbr = self::getReplicaConnection();
445
446        // Select all articles in PageTriage queue created by the blocked user
447        $res = $dbr->newSelectQueryBuilder()
448            ->select( [ 'ptrpt_page_id' ] )
449            ->from( 'pagetriage_page_tags' )
450            ->where( [
451                'ptrpt_tag_id' => $tags['user_name'],
452                'ptrpt_value' => $block->getTargetName()
453            ] )
454            ->limit( $maxNumToProcess + 1 )
455            ->caller( __METHOD__ )
456            ->fetchResultSet();
457
458        if ( $res->numRows() > $maxNumToProcess ) {
459            return;
460        }
461
462        $pageIds = [];
463        foreach ( $res as $row ) {
464            $pageIds[] = $row->ptrpt_page_id;
465        }
466
467        $noArticlesNeedUpdating = !$pageIds;
468        if ( $noArticlesNeedUpdating ) {
469            return;
470        }
471
472        $dbw = self::getPrimaryConnection();
473        $dbw->startAtomic( __METHOD__ );
474        $dbw->newUpdateQueryBuilder()
475            ->update( 'pagetriage_page_tags' )
476            ->set( [ 'ptrpt_value' => (string)$userBlockStatusToWrite ] )
477            ->where( [ 'ptrpt_page_id' => $pageIds, 'ptrpt_tag_id' => $tags['user_block_status'] ] )
478            ->caller( __METHOD__ )
479            ->execute();
480
481        PageTriage::bulkSetTagsUpdated( $pageIds );
482        $dbw->endAtomic( __METHOD__ );
483
484        $metadata = new ArticleMetadata( $pageIds );
485        $metadata->flushMetadataFromCache();
486    }
487
488    /**
489     * Attempt to create an Echo notification event for
490     * 1. 'Mark as Reviewed' curation flyout
491     * 2. 'Mark as Patrolled' from Special:NewPages
492     * 3. 'Add maintenance tag' curation flyout
493     * 4. 'Add deletion tag' curation flyout
494     *
495     * @param Title $title
496     * @param User $user
497     * @param string $type notification type
498     * @param array|null $extra
499     * @return StatusValue
500     */
501    public static function createNotificationEvent(
502        Title $title, UserIdentity $user, string $type, ?array $extra = null
503    ): StatusValue {
504        $status = StatusValue::newGood();
505        if ( !ExtensionRegistry::getInstance()->isLoaded( 'Echo' ) ) {
506            return $status;
507        }
508
509        $params = [
510            'type' => $type,
511            'title' => $title,
512            'agent' => $user,
513        ];
514
515        if ( $extra ) {
516            $extra['note'] = self::truncateLongText( $extra['note'] );
517            $params['extra'] = $extra;
518        }
519
520        $echoEvent = Event::create( $params );
521        if ( $echoEvent instanceof Event ) {
522            return StatusValue::newGood( $echoEvent );
523        } else {
524            return StatusValue::newFatal( new ApiRawMessage( 'Failed to create Echo event.' ) );
525        }
526    }
527
528    /**
529     * @param string $text The text to truncate.
530     * @param int $length Maximum number of characters.
531     * @param string $ellipsis String to append to the end of truncated text.
532     * @return string
533     */
534    public static function truncateLongText( $text, $length = 150, $ellipsis = '...' ) {
535        if ( !is_string( $text ) ) {
536            return $text;
537        }
538
539        return RequestContext::getMain()->getLanguage()->truncateForVisual( $text, $length, $ellipsis );
540    }
541
542    /**
543     * Get an array of ORES articlequality API parameters.
544     *
545     * @return array
546     */
547    private static function getOresArticleQualityApiParams() {
548        return [
549            'show_predicted_class_stub' => [
550                ParamValidator::PARAM_TYPE => 'boolean'
551            ],
552            'show_predicted_class_start' => [
553                ParamValidator::PARAM_TYPE => 'boolean'
554            ],
555            'show_predicted_class_c' => [
556                ParamValidator::PARAM_TYPE => 'boolean'
557            ],
558            'show_predicted_class_b' => [
559                ParamValidator::PARAM_TYPE => 'boolean'
560            ],
561            'show_predicted_class_good' => [
562                ParamValidator::PARAM_TYPE => 'boolean'
563            ],
564            'show_predicted_class_featured' => [
565                ParamValidator::PARAM_TYPE => 'boolean'
566            ],
567        ];
568    }
569
570    /**
571     * Get an array of ORES draftquality API parameters.
572     *
573     * @return array
574     */
575    private static function getOresDraftQualityApiParams() {
576        return [
577            'show_predicted_issues_vandalism' => [
578                ParamValidator::PARAM_TYPE => 'boolean'
579            ],
580            'show_predicted_issues_spam' => [
581                ParamValidator::PARAM_TYPE => 'boolean'
582            ],
583            'show_predicted_issues_attack' => [
584                ParamValidator::PARAM_TYPE => 'boolean'
585            ],
586            'show_predicted_issues_none' => [
587                ParamValidator::PARAM_TYPE => 'boolean'
588            ],
589        ];
590    }
591
592    /**
593     * Get an array of ORES API parameters.
594     *
595     * These are used in both NPP and AFC contexts.
596     *
597     * @return array
598     */
599    public static function getOresApiParams() {
600        return self::getOresArticleQualityApiParams() + self::getOresDraftQualityApiParams();
601    }
602
603    /**
604     * Get array of common API parameters, for use with getAllowedParams().
605     *
606     * @return array
607     */
608    public static function getCommonApiParams() {
609        return [
610            'showbots' => [
611                ParamValidator::PARAM_TYPE => 'boolean',
612            ],
613            'showautopatrolled' => [
614                ParamValidator::PARAM_TYPE => 'boolean',
615            ],
616            'showredirs' => [
617                ParamValidator::PARAM_TYPE => 'boolean',
618            ],
619            'showothers' => [
620                ParamValidator::PARAM_TYPE => 'boolean',
621            ],
622            'showreviewed' => [
623                ParamValidator::PARAM_TYPE => 'boolean',
624            ],
625            'showunreviewed' => [
626                ParamValidator::PARAM_TYPE => 'boolean',
627            ],
628            'showdeleted' => [
629                ParamValidator::PARAM_TYPE => 'boolean',
630            ],
631            'namespace' => [
632                ParamValidator::PARAM_TYPE => 'integer',
633            ],
634            'afc_state' => [
635                ParamValidator::PARAM_TYPE => 'integer',
636            ],
637            'no_category' => [
638                ParamValidator::PARAM_TYPE => 'boolean',
639            ],
640            'unreferenced' => [
641                ParamValidator::PARAM_TYPE => 'boolean',
642            ],
643            'no_inbound_links' => [
644                ParamValidator::PARAM_TYPE => 'boolean',
645            ],
646            'recreated' => [
647                ParamValidator::PARAM_TYPE => 'boolean',
648            ],
649            'non_autoconfirmed_users' => [
650                ParamValidator::PARAM_TYPE => 'boolean',
651            ],
652            'learners' => [
653                ParamValidator::PARAM_TYPE => 'boolean',
654            ],
655            'blocked_users' => [
656                ParamValidator::PARAM_TYPE => 'boolean',
657            ],
658            'username' => [
659                ParamValidator::PARAM_TYPE => 'user',
660            ],
661            'date_range_from' => [
662                ParamValidator::PARAM_TYPE => 'timestamp',
663            ],
664            'date_range_to' => [
665                ParamValidator::PARAM_TYPE => 'timestamp',
666            ],
667        ];
668    }
669
670    /**
671     * Helper method to check if the API call includes ORES articlequality parameters.
672     *
673     * @param array $opts
674     * @return bool
675     */
676    public static function isOresArticleQualityQuery( $opts ) {
677        return self::queryContains( $opts, self::getOresArticleQualityApiParams() );
678    }
679
680    /**
681     * Helper method to check if the API call includes ORES draftquality parameters.
682     *
683     * @param array $opts
684     * @return bool
685     */
686    public static function isOresDraftQualityQuery( $opts ) {
687        return self::queryContains( $opts, self::getOresDraftQualityApiParams() );
688    }
689
690    /**
691     * Helper method to check if $opts contains some of the parameters in $params.
692     *
693     * @param array $opts Selected parameters from API request
694     * @param array $params
695     * @return bool
696     */
697    private static function queryContains( $opts, $params ) {
698        $params = array_keys( $params );
699        foreach ( $params as $key ) {
700            if ( isset( $opts[ $key ] ) && $opts[ $key ] ) {
701                return true;
702            }
703        }
704        return false;
705    }
706
707    /**
708     * Convert ORES param names to class names.
709     *
710     * @param string $model Which model to convert names for ('articlequality' or 'draftquality')
711     * @param array $opts Selected parameters
712     * @return array Corresponding ORES class names
713     */
714    public static function mapOresParamsToClassNames( $model, $opts ) {
715        $paramsToClassesMap = [
716            'articlequality' => [
717                'show_predicted_class_stub' => 'Stub',
718                'show_predicted_class_start' => 'Start',
719                'show_predicted_class_c' => 'C',
720                'show_predicted_class_b' => 'B',
721                'show_predicted_class_good' => 'GA',
722                'show_predicted_class_featured' => 'FA',
723            ],
724            'draftquality' => [
725                'show_predicted_issues_vandalism' => 'vandalism',
726                'show_predicted_issues_spam' => 'spam',
727                'show_predicted_issues_attack' => 'attack',
728                'show_predicted_issues_none' => 'OK',
729            ],
730        ];
731        $result = [];
732        foreach ( $paramsToClassesMap[ $model ] as $param => $className ) {
733            if ( isset( $opts[ $param ] ) && $opts[ $param ] ) {
734                $result[] = $className;
735            }
736        }
737        return $result;
738    }
739
740    /**
741     * Check if the ORES extension is present and configured
742     * correctly for PageTriage to integrate with it.
743     *
744     * @return bool
745     */
746    public static function oresIsAvailable() {
747        return ExtensionRegistry::getInstance()->isLoaded( 'ORES' ) &&
748            Helpers::isModelEnabled( 'articlequality' ) &&
749            Helpers::isModelEnabled( 'draftquality' );
750    }
751
752    /**
753     * @return array The copyvio filter parameter
754     */
755    public static function getCopyvioApiParam() {
756        return [
757            'show_predicted_issues_copyvio' => [
758                ParamValidator::PARAM_TYPE => 'boolean',
759            ],
760        ];
761    }
762
763    /**
764     * Check if $opts contain the copyvio filter parameter
765     *
766     * @param array $opts
767     * @return bool
768     */
769    public static function isCopyvioQuery( $opts ) {
770        return $opts[ 'show_predicted_issues_copyvio' ] ?? false;
771    }
772
773    /**
774     * Get a count of how many links are in a specific page.
775     * @param LinksMigration $linksMigration
776     * @param int $pageId The page for which links need to be fetched
777     * @param int $limit Number of links to fetch, defaults to 51
778     * @return int Number of links
779     */
780    public static function getLinkCount( LinksMigration $linksMigration, int $pageId, int $limit = 51 ): int {
781        [ $blNamespace, $blTitle ] = $linksMigration->getTitleFields( 'pagelinks' );
782        $dbr = self::getReplicaConnection();
783        $queryInfo = $linksMigration->getQueryInfo( 'pagelinks', 'pagelinks' );
784        $res = $dbr->newSelectQueryBuilder()
785            ->select( '1' )
786            ->tables( $queryInfo['tables'] )
787            ->joinConds( $queryInfo['joins'] )
788            ->join( 'page', null, [ "page_namespace = $blNamespace", "page_title = $blTitle" ] )
789            ->where( [
790                'page_id' => $pageId,
791                'page_is_redirect' => 0,
792                // T313777 - only considering backlinks from mainspace pages
793                'pl_from_namespace' => 0,
794            ] )
795            ->limit( $limit )
796            ->caller( __METHOD__ )
797            ->fetchResultSet()->numRows();
798        return $res;
799    }
800
801    /**
802     * Return an SQL primary database connection.
803     *
804     * @return IDatabase
805     */
806    public static function getPrimaryConnection() {
807        return MediaWikiServices::getInstance()->getConnectionProvider()->getPrimaryDatabase();
808    }
809
810    /**
811     * Return an SQL replica database connection.
812     *
813     * @return IReadableDatabase
814     */
815    public static function getReplicaConnection() {
816        return MediaWikiServices::getInstance()->getConnectionProvider()->getReplicaDatabase();
817    }
818}