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