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