Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
89.60% covered (warning)
89.60%
293 / 327
69.23% covered (warning)
69.23%
9 / 13
CRAP
0.00% covered (danger)
0.00%
0 / 1
ApiPageTriageList
89.60% covered (warning)
89.60%
293 / 327
69.23% covered (warning)
69.23%
9 / 13
93.12
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
1
 execute
84.51% covered (warning)
84.51%
60 / 71
0.00% covered (danger)
0.00%
0 / 1
15.84
 joinWithTagCopyvio
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
1
 joinWithTags
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
1
 buildCopyvioCond
85.71% covered (warning)
85.71%
6 / 7
0.00% covered (danger)
0.00%
0 / 1
4.05
 getTalkpageFeedbackCount
100.00% covered (success)
100.00%
14 / 14
100.00% covered (success)
100.00%
1 / 1
1
 isOrphan
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 createUserInfo
100.00% covered (success)
100.00%
14 / 14
100.00% covered (success)
100.00%
1 / 1
1
 getPageIds
86.47% covered (warning)
86.47%
115 / 133
0.00% covered (danger)
0.00%
0 / 1
58.70
 joinWithOres
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
2
 buildTagQuery
100.00% covered (success)
100.00%
22 / 22
100.00% covered (success)
100.00%
1 / 1
4
 getAllowedParams
100.00% covered (success)
100.00%
30 / 30
100.00% covered (success)
100.00%
1 / 1
1
 getExamplesMessages
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2
3namespace MediaWiki\Extension\PageTriage\Api;
4
5use ApiBase;
6use ApiMain;
7use ApiResult;
8use MediaWiki\Extension\PageTriage\ArticleMetadata;
9use MediaWiki\Extension\PageTriage\OresMetadata;
10use MediaWiki\Extension\PageTriage\PageTriageUtil;
11use MediaWiki\Linker\LinksMigration;
12use MediaWiki\Logger\LoggerFactory;
13use MediaWiki\Page\RedirectLookup;
14use MediaWiki\SpecialPage\SpecialPage;
15use MediaWiki\Title\Title;
16use MediaWiki\Title\TitleFormatter;
17use MediaWiki\User\UserFactory;
18use ORES\Services\ORESServices;
19use Wikimedia\ParamValidator\ParamValidator;
20use Wikimedia\ParamValidator\TypeDef\IntegerDef;
21use Wikimedia\Rdbms\IExpression;
22use Wikimedia\Rdbms\OrExpressionGroup;
23
24/**
25 * API module to generate a list of pages to triage
26 *
27 * @ingroup API
28 * @ingroup Extensions
29 */
30class ApiPageTriageList extends ApiBase {
31
32    /** @var UserFactory */
33    private UserFactory $userFactory;
34
35    /** @var RedirectLookup */
36    private $redirectLookup;
37
38    /** @var TitleFormatter */
39    private $titleFormatter;
40
41    /** @var LinksMigration */
42    private $linksMigration;
43
44    /**
45     * @param ApiMain $query
46     * @param string $moduleName
47     */
48    public function __construct(
49        ApiMain $query,
50        string $moduleName,
51        UserFactory $userFactory,
52        RedirectLookup $redirectLookup,
53        TitleFormatter $titleFormatter,
54        LinksMigration $linksMigration
55    ) {
56        $this->userFactory = $userFactory;
57        $this->linksMigration = $linksMigration;
58        $this->redirectLookup = $redirectLookup;
59        $this->titleFormatter = $titleFormatter;
60        parent::__construct( $query, $moduleName );
61    }
62
63    public function execute() {
64        // Get the API parameters and store them
65        $opts = $this->extractRequestParams();
66        $pages = null;
67
68        if ( $opts['page_id'] ) {
69            // page id was specified
70            $pages = [ $opts['page_id'] ];
71            $pageIdValidated = false;
72        } else {
73            // Retrieve the list of page IDs
74            $pages = self::getPageIds( $opts );
75            $pageIdValidated = true;
76        }
77        $pageIdValidateDb = DB_REPLICA;
78
79        $sortedMetaData = [];
80
81        $result = [
82            'result' => 'success',
83            'pages_missing_metadata' => [],
84        ];
85
86        if ( $pages ) {
87            // fetch metadata for those pages
88            $articleMetadata = new ArticleMetadata( $pages, $pageIdValidated, $pageIdValidateDb );
89            $metaData = $articleMetadata->getMetadata();
90
91            $userPageStatus = PageTriageUtil::pageStatusForUser( $metaData );
92            $oresMetadata = null;
93
94            if ( PageTriageUtil::oresIsAvailable() ) {
95                $oresMetadata = OresMetadata::newFromGlobalState( $this->getContext(), $pages );
96            }
97
98            // Sort data according to page order returned by our query. Also convert it to a
99            // slightly different format that's more Backbone-friendly.
100            foreach ( $pages as $page ) {
101                if ( !isset( $metaData[$page] ) || !ArticleMetadata::isValidMetadata( $metaData[$page] ) ) {
102                    // If metadata is missing for a page, add warning to API output and exclude
103                    // from feed.
104                    $result['pages_missing_metadata'][] = $page;
105                    $result['result'] = 'warning';
106                    continue;
107                }
108                $metaData[$page]['creation_date_utc'] = $metaData[$page]['creation_date'];
109                $metaData[$page]['creation_date'] = $this->getContext()->getLanguage()->userAdjust(
110                    $metaData[$page]['creation_date']
111                );
112
113                if ( $metaData[$page]['user_name'] ) {
114                    // Page creator
115                    $user = $this->userFactory->newFromName( $metaData[$page]['user_name'] );
116                    if ( $user && $user->isHidden() ) {
117                        $metaData[$page]['user_name'] = null;
118                        $metaData[$page]['creator_hidden'] = true;
119                    } else {
120                        $metaData[$page] += $this->createUserInfo(
121                            $metaData[$page]['user_name'],
122                            $userPageStatus,
123                            'creator'
124                        );
125                    }
126                }
127
128                // Page reviewer
129                if ( $metaData[$page]['reviewer'] ) {
130                    $metaData[$page] += $this->createUserInfo(
131                        $metaData[$page]['reviewer'],
132                        $userPageStatus,
133                        'reviewer'
134                    );
135                }
136
137                $pageTitle = Title::newFromText( $metaData[ $page ]['title'] );
138
139                // Talk page feedback count and URL.
140                if ( $opts['page_id'] ) {
141                    // Only add when a single page is being requested, e.g. for the PageTriage toolbar.
142                    $talkPage = $pageTitle->getTalkPageIfDefined();
143                    $metaData[$page]['talk_page_title'] = $talkPage->getPrefixedText();
144                    $metaData[$page]['talkpage_feedback_count'] = $this->getTalkpageFeedbackCount( $talkPage );
145                    $metaData[$page]['talk_page_url'] = $talkPage->getInternalURL();
146                    $metaData[$page]['is_orphan'] = $this->isOrphan( $page );
147                }
148
149                $redirectTarget = $this->redirectLookup->getRedirectTarget( $pageTitle );
150
151                if ( $redirectTarget !== null ) {
152                    $metaData[$page]['redirect_target'] = $this->titleFormatter->getFullText( $redirectTarget );
153                }
154
155                // Add ORES data
156                if ( $oresMetadata !== null ) {
157                    $metaData[$page] += $oresMetadata->getMetadata( $page );
158                }
159
160                $metaData[$page][ApiResult::META_BC_BOOLS] = [
161                    'creator_hidden', 'creator_user_page_exist', 'creator_user_talk_page_exist',
162                    'reviewer_user_page_exist', 'reviewer_user_talk_page_exist', 'is_orphan'
163                ];
164
165                $sortedMetaData[] = [ 'pageid' => $page ] + $metaData[$page];
166            }
167        }
168
169        // Log missing metadata.
170        if ( count( $result['pages_missing_metadata'] ) ) {
171            LoggerFactory::getInstance( 'PageTriage' )->debug( 'Metadata is missing for some pages.',
172                [
173                    'pages_missing_metadata' => implode( ',', $result['pages_missing_metadata'] ),
174                    'opts' => json_encode( $opts, JSON_PRETTY_PRINT ),
175                ]
176            );
177        }
178
179        // Output the results
180        $result['pages'] = $sortedMetaData;
181        $this->getResult()->addValue( null, $this->getModuleName(), $result );
182    }
183
184    /**
185     * @param string[] &$tables
186     * @param array &$join_conds
187     */
188    private static function joinWithTagCopyvio( &$tables, &$join_conds ) {
189        $tags = ArticleMetadata::getValidTags();
190        $tagId = $tags[ 'copyvio' ];
191
192        $tables[ 'pagetriage_page_tags_copyvio' ] = 'pagetriage_page_tags';
193        $join_conds[ 'pagetriage_page_tags_copyvio' ] = [
194            'LEFT JOIN',
195            [
196                'pagetriage_page_tags_copyvio.ptrpt_page_id = ptrp_page_id',
197                'pagetriage_page_tags_copyvio.ptrpt_tag_id' => $tagId,
198            ]
199        ];
200    }
201
202    /**
203     * @param string[] &$tables
204     * @param array &$join_conds
205     */
206    private static function joinWithTags( &$tables, &$join_conds ) {
207        $tables[ 'pagetriage_pt' ] = 'pagetriage_page_tags';
208        $join_conds[ 'pagetriage_pt' ] = [
209            'INNER JOIN',
210            "pagetriage_pt.ptrpt_page_id = ptrp_page_id",
211        ];
212    }
213
214    /**
215     * @param array $opts
216     *
217     * @return string|false
218     */
219    private static function buildCopyvioCond( $opts ) {
220        if (
221            !isset( $opts[ 'show_predicted_issues_copyvio' ] ) ||
222            !$opts[ 'show_predicted_issues_copyvio' ]
223        ) {
224            return false;
225        }
226        $tags = ArticleMetadata::getValidTags();
227        if ( !isset( $tags[ 'copyvio' ] ) ) {
228            return false;
229        }
230
231        return "pagetriage_page_tags_copyvio.ptrpt_value IS NOT NULL";
232    }
233
234    /**
235     * Get the total number of pagetriage-tagged revisions of a talk page.
236     * @param Title $pageTitle The title of the talk page
237     * @return int
238     */
239    protected function getTalkpageFeedbackCount( Title $pageTitle ) {
240        $dbr = PageTriageUtil::getReplicaConnection();
241        $feedbackCount = $dbr->newSelectQueryBuilder()
242            ->select( '*' )
243            ->from( 'change_tag_def' )
244            ->join( 'change_tag', 'change_tag', 'ctd_id = ct_tag_id' )
245            ->join( 'revision', 'revision', 'ct_rev_id = rev_id' )
246            ->join( 'page', 'page', 'rev_page = page_id' )
247            ->where( [
248                'ctd_name' => 'pagetriage',
249                'page_id' => $pageTitle->getArticleID(),
250            ] )
251            ->caller( __METHOD__ )
252            ->fetchRowCount();
253        return $feedbackCount;
254    }
255
256    /**
257     * Get if a specific page is a orphan. This will only be used
258     * when the user is on a specific page and not the feed. The feed will continue to use
259     * the compiled and cached link count.
260     * @param int $pageId The ID of the page
261     * @return bool
262     */
263    protected function isOrphan( int $pageId ): bool {
264        $linkCount = PageTriageUtil::getLinkCount( $this->linksMigration, $pageId, 1 );
265        return $linkCount === 0;
266    }
267
268    /**
269     * Create user info like user page, user talk page, user contribution page
270     * @param string $userName a valid username
271     * @param array $userPageStatus an array of user page, user talk page existing status
272     * @param string $prefix array key prefix
273     * @return array
274     */
275    private function createUserInfo( $userName, $userPageStatus, $prefix ) {
276        $userPage = Title::makeTitle( NS_USER, $userName );
277        $userTalkPage = Title::makeTitle( NS_USER_TALK, $userName );
278        $userContribsPage = SpecialPage::getTitleFor( 'Contributions', $userName );
279
280        return [
281            $prefix . '_user_page' => $userPage->getPrefixedText(),
282            $prefix . '_user_page_url' => $userPage->getFullURL(),
283            $prefix . '_user_page_exist' => isset( $userPageStatus[$userPage->getPrefixedDBkey()] ),
284            $prefix . '_user_talk_page' => $userTalkPage->getPrefixedText(),
285            $prefix . '_user_talk_page_url' => $userTalkPage->getFullURL(),
286            $prefix . '_user_talk_page_exist' => isset( $userPageStatus[$userTalkPage->getPrefixedDBkey()] ),
287            $prefix . '_contribution_page' => $userContribsPage->getPrefixedText(),
288            $prefix . '_contribution_page_url' => $userContribsPage->getFullURL(),
289            $prefix . '_hidden' => false,
290        ];
291    }
292
293    /**
294     * Return all the page ids in PageTriage matching the specified filters
295     * @param array $opts Array of filtering options
296     * @param bool $count Set to true to return a count instead
297     * @return array|int an array of ids or total number of pages
298     *
299     * @todo - enforce a range of timestamp to reduce tag record scan
300     */
301    public static function getPageIds( $opts = [], $count = false ) {
302        // Initialize required variables
303        $pages = [];
304        $options = [];
305        $conds = [];
306        $join_conds = [];
307        $offsetOperator = '';
308
309        // Database setup
310        $dbr = PageTriageUtil::getReplicaConnection();
311
312        if ( !$count ) {
313            // Get the expected limit as defined in getAllowedParams
314            $options['LIMIT'] = $opts['limit'] + 1;
315
316            switch ( strtolower( $opts['dir'] ?? '' ) ) {
317                // Created date (oldest)
318                case 'oldestfirst':
319                    $options['ORDER BY'] = [ 'ptrp_created ASC', 'ptrp_page_id ASC' ];
320                    $offsetOperator = '>';
321                    break;
322                // Submitted date (oldest)
323                case 'oldestreview':
324                    $options['ORDER BY'] = [ 'ptrp_reviewed_updated ASC', 'ptrp_page_id ASC' ];
325                    $offsetOperator = '>';
326                    break;
327                // Submitted date (newest)
328                case 'newestreview':
329                    $options['ORDER BY'] = [ 'ptrp_reviewed_updated DESC', 'ptrp_page_id DESC' ];
330                    $offsetOperator = '<';
331                    break;
332                // Created date (newest)
333                case 'newestfirst':
334                default:
335                    $options['ORDER BY'] = [ 'ptrp_created DESC', 'ptrp_page_id DESC' ];
336                    $offsetOperator = '<';
337            }
338        }
339
340        // Start building the massive filter which includes meta data
341        $tables    = [ 'pagetriage_page', 'page' ];
342        $join_conds['page'] = [
343            'INNER JOIN',
344            'ptrp_page_id = page_id',
345        ];
346
347        // Helpful hint: In the ptrp_reviewed column...
348        // 0 = unreviewed
349        // 1 = reviewed
350        // 2 = patrolled
351        // 3 = autopatrolled
352        $reviewOpr = '';
353        if ( isset( $opts['showreviewed'] ) && $opts['showreviewed'] ) {
354            $reviewOpr .= '>';
355        }
356        if ( isset( $opts['showunreviewed'] ) && $opts['showunreviewed'] ) {
357            $reviewOpr .= '=';
358        }
359        if ( !$reviewOpr ) {
360            if ( $count ) {
361                return 0;
362            } else {
363                return $pages;
364            }
365        }
366        if ( $reviewOpr !== '>=' ) {
367            $conds[] = $dbr->expr( 'ptrp_reviewed', $reviewOpr, 0 );
368        }
369
370        if ( isset( $opts['showautopatrolled'] ) && $opts['showautopatrolled'] ) {
371            $conds['ptrp_reviewed'] = 3;
372        }
373
374        if ( isset( $opts['date_range_from'] ) && $opts['date_range_from'] ) {
375            $conds[] = $dbr->expr( 'ptrp_created', '>=', $dbr->timestamp( $opts['date_range_from'] ) );
376        }
377
378        if ( isset( $opts['date_range_to'] ) && $opts['date_range_to'] ) {
379            $conds[] = $dbr->expr( 'ptrp_created', '<=', $dbr->timestamp( $opts['date_range_to'] ) );
380        }
381
382        // Filter on types
383        $redirects = $opts['showredirs'] ?? false;
384        $deleted = $opts['showdeleted'] ?? false;
385        $others = $opts['showothers'] ?? false;
386        $typeConds = [];
387
388        if ( $redirects !== $others ) {
389            $typeConds['page_is_redirect'] = $redirects ? 1 : 0;
390        }
391        if ( $deleted !== $others ) {
392            $typeConds['ptrp_deleted'] = $deleted ? 1 : 0;
393        }
394        if ( $typeConds ) {
395            $conds[] = $dbr->makeList( $typeConds, $others ? LIST_AND : LIST_OR );
396        }
397
398        // Show by namespace. Defaults to main namespace.
399        $nsId = ( isset( $opts['namespace'] ) && $opts['namespace'] ) ? $opts['namespace'] : NS_MAIN;
400        $conds['page_namespace'] = PageTriageUtil::validatePageNamespace( $nsId );
401
402        // Offset the list by timestamp
403        $offsetConds = [];
404        if (
405            array_key_exists( 'offset', $opts ) &&
406            is_numeric( $opts['offset'] ) &&
407            $opts['offset'] > 0 &&
408            !$count
409        ) {
410            $offsetConds['ptrp_created'] = $dbr->timestamp( $opts['offset'] );
411            // Offset the list by page ID as well (in case multiple pages have the same timestamp)
412            if (
413                array_key_exists( 'pageoffset', $opts ) &&
414                is_numeric( $opts['pageoffset'] ) &&
415                $opts['pageoffset'] > 0
416            ) {
417                $offsetConds['ptrp_page_id'] = $opts['pageoffset'];
418            }
419            $conds[] = $dbr->buildComparison( $offsetOperator, $offsetConds );
420        }
421
422        $tagConds = self::buildTagQuery( $opts );
423        $numberOfTagConds = count( $tagConds );
424
425        if ( $numberOfTagConds > 0 ) {
426            $conds[] = new OrExpressionGroup( ...$tagConds );
427            $options['GROUP BY'] = "ptrp_page_id";
428            $options['HAVING'] = "COUNT(*) = $numberOfTagConds";
429            self::joinWithTags( $tables, $join_conds );
430        }
431
432        // ORES articlequality filter
433        if ( PageTriageUtil::oresIsAvailable() &&
434            PageTriageUtil::isOresArticleQualityQuery( $opts ) ) {
435            $oresCond = ORESServices::getDatabaseQueryBuilder()->buildQuery(
436                'articlequality',
437                PageTriageUtil::mapOresParamsToClassNames( 'articlequality', $opts )
438            );
439            if ( $oresCond ) {
440                self::joinWithOres( 'articlequality', $tables, $join_conds );
441                $conds[] = $oresCond;
442            }
443        }
444
445        // ORES draftquality and copyvio filters
446        if ( PageTriageUtil::oresIsAvailable() &&
447            ( PageTriageUtil::isOresDraftQualityQuery( $opts ) ||
448            PageTriageUtil::isCopyvioQuery( $opts ) )
449        ) {
450            $draftqualityCopyvioConds = [];
451
452            // "Issues: none" used to be map straight to DraftQuality class OK
453            // It now means: no known ORES DraftQuality issues or Copyvio
454            // It has to be removed from the $opts ORES will used to build a query
455            // and handled separately.
456            $showOK = $opts[ 'show_predicted_issues_none' ] ?? false;
457            if ( $showOK ) {
458                unset( $opts[ 'show_predicted_issues_none' ] );
459                $draftqualityCopyvioConds[] = $dbr->makeList( [
460                    'ores_draftquality_cls.oresc_class' => [ 1, null ],
461                    'pagetriage_page_tags_copyvio.ptrpt_value' => null,
462                ], LIST_AND );
463            }
464
465            $oresCond = ORESServices::getDatabaseQueryBuilder()->buildQuery(
466                'draftquality',
467                PageTriageUtil::mapOresParamsToClassNames( 'draftquality', $opts ),
468                true
469            );
470            if ( $oresCond ) {
471                $draftqualityCopyvioConds[] = $oresCond;
472            }
473
474            $copyvioCond = self::buildCopyvioCond( $opts );
475            if ( $copyvioCond ) {
476                $draftqualityCopyvioConds[] = $copyvioCond;
477            }
478
479            if ( $draftqualityCopyvioConds ) {
480                $conds[] = $dbr->makeList( $draftqualityCopyvioConds, LIST_OR );
481            }
482
483            if ( $showOK || $oresCond ) {
484                self::joinWithOres( 'draftquality', $tables, $join_conds );
485            }
486
487            if ( $showOK || $copyvioCond ) {
488                self::joinWithTagCopyvio( $tables, $join_conds );
489            }
490        }
491
492        if ( $count ) {
493            $res = $dbr->newSelectQueryBuilder()
494                ->select( 'ptrp_page_id' )
495                ->tables( $tables )
496                ->joinConds( $join_conds )
497                ->conds( $conds )
498                ->options( $options )
499                ->caller( __METHOD__ )
500                ->fetchRowCount();
501
502            return $res;
503        } else {
504            // Pull page IDs from database
505            $res = $dbr->newSelectQueryBuilder()
506                ->select( 'ptrp_page_id' )
507                ->tables( $tables )
508                ->joinConds( $join_conds )
509                ->conds( $conds )
510                ->options( $options )
511                ->caller( __METHOD__ )
512                ->fetchResultSet();
513
514            // Loop through result set and return ids
515            foreach ( $res as $row ) {
516                $pages[] = $row->ptrp_page_id;
517            }
518            return $pages;
519        }
520    }
521
522    /**
523     * @param string $model Name of the model this join is for
524     * @param array &$tables
525     * @param array &$join_conds
526     */
527    private static function joinWithOres( $model, &$tables, &$join_conds ) {
528        $modelId = ORESServices::getModelLookup()->getModelId( $model );
529        $tableAlias = "ores_{$model}_cls";
530        $tables[ $tableAlias ] = 'ores_classification';
531        $innerJoinConds = [
532            "$tableAlias.oresc_rev = page_latest",
533            "$tableAlias.oresc_model" => $modelId,
534        ];
535        if ( $model === 'draftquality' ) {
536            $innerJoinConds[] = "$tableAlias.oresc_is_predicted = 1";
537        }
538        $join_conds[ $tableAlias ] = [ 'LEFT JOIN', $innerJoinConds ];
539    }
540
541    /**
542     * @param array $opts
543     * @return IExpression[] SQL condition for use in a WHERE clause
544     */
545    private static function buildTagQuery( array $opts ) {
546        $dbr = PageTriageUtil::getReplicaConnection();
547        $tagConds = [];
548
549        $searchableTags = [
550            // no categories assigned
551            'no_category' => [ 'name' => 'category_count', 'val' => 0 ],
552            // No citations
553            'unreferenced' => [ 'name' => 'reference', 'val' => 0 ],
554            // AfC status
555            'afc_state' => [ 'name' => 'afc_state', 'val' => null ],
556            // no inbound links
557            'no_inbound_links' => [ 'name' => 'linkcount', 'val' => 0 ],
558            // previously deleted
559            'recreated' => [ 'name' => 'recreated', 'val' => 1 ],
560            // non auto confirmed users
561            'non_autoconfirmed_users' => [ 'name' => 'user_autoconfirmed', 'val' => 0 ],
562            // learning users (newly autoconfirmed)
563            'learners' => [ 'name' => 'user_experience', 'val' => 'learner' ],
564            // blocked users
565            'blocked_users' => [ 'name' => 'user_block_status', 'val' => 1 ],
566            // bots
567            'showbots' => [ 'name' => 'user_bot', 'val' => 1 ],
568            // user name
569            // false means use the actual value
570            'username' => [ 'name' => 'user_name', 'val' => null ]
571        ];
572
573        $tagIDs = ArticleMetadata::getValidTags();
574        // "pagetriage_pt" alias from self::joinWithTags
575        $table = 'pagetriage_pt';
576
577        // only single tag search is allowed
578        foreach ( $searchableTags as $key => $val ) {
579            if ( isset( $opts[$key] ) && $opts[$key] ) {
580                $tagConds[] = $dbr
581                    ->expr( "$table.ptrpt_tag_id", "=", $tagIDs[$val['name']] )
582                    ->and( "$table.ptrpt_value", "=", (string)( $val['val'] ?? $opts[$key] ) );
583            }
584        }
585
586        return $tagConds;
587    }
588
589    /**
590     * @inheritDoc
591     */
592    public function getAllowedParams() {
593        return array_merge(
594            PageTriageUtil::getOresApiParams(),
595            PageTriageUtil::getCopyvioApiParam(),
596            PageTriageUtil::getCommonApiParams(),
597            [
598                'page_id' => [
599                    ParamValidator::PARAM_TYPE => 'integer',
600                ],
601                'limit' => [
602                    IntegerDef::PARAM_MAX => 200,
603                    ParamValidator::PARAM_DEFAULT => 20,
604                    IntegerDef::PARAM_MIN => 1,
605                    ParamValidator::PARAM_TYPE => 'integer',
606                ],
607                'offset' => [
608                    ParamValidator::PARAM_TYPE => 'integer',
609                ],
610                'pageoffset' => [
611                    ParamValidator::PARAM_TYPE => 'integer',
612                ],
613                'dir' => [
614                    ParamValidator::PARAM_TYPE => [
615                        'newestfirst',
616                        'oldestfirst',
617                        'oldestreview',
618                        'newestreview',
619                    ],
620                ]
621            ]
622        );
623    }
624
625    /**
626     * @see ApiBase::getExamplesMessages()
627     * @return array
628     */
629    protected function getExamplesMessages() {
630        return [
631            'action=pagetriagelist&limit=100&namespace=0&showunreviewed=1'
632                => 'apihelp-pagetriagelist-example-1',
633        ];
634    }
635
636}