Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
89.60% |
293 / 327 |
|
69.23% |
9 / 13 |
CRAP | |
0.00% |
0 / 1 |
ApiPageTriageList | |
89.60% |
293 / 327 |
|
69.23% |
9 / 13 |
93.12 | |
0.00% |
0 / 1 |
__construct | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
1 | |||
execute | |
84.51% |
60 / 71 |
|
0.00% |
0 / 1 |
15.84 | |||
joinWithTagCopyvio | |
100.00% |
10 / 10 |
|
100.00% |
1 / 1 |
1 | |||
joinWithTags | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
1 | |||
buildCopyvioCond | |
85.71% |
6 / 7 |
|
0.00% |
0 / 1 |
4.05 | |||
getTalkpageFeedbackCount | |
100.00% |
14 / 14 |
|
100.00% |
1 / 1 |
1 | |||
isOrphan | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
createUserInfo | |
100.00% |
14 / 14 |
|
100.00% |
1 / 1 |
1 | |||
getPageIds | |
86.47% |
115 / 133 |
|
0.00% |
0 / 1 |
58.70 | |||
joinWithOres | |
100.00% |
10 / 10 |
|
100.00% |
1 / 1 |
2 | |||
buildTagQuery | |
100.00% |
22 / 22 |
|
100.00% |
1 / 1 |
4 | |||
getAllowedParams | |
100.00% |
30 / 30 |
|
100.00% |
1 / 1 |
1 | |||
getExamplesMessages | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
2 |
1 | <?php |
2 | |
3 | namespace MediaWiki\Extension\PageTriage\Api; |
4 | |
5 | use ApiBase; |
6 | use ApiMain; |
7 | use ApiResult; |
8 | use MediaWiki\Extension\PageTriage\ArticleMetadata; |
9 | use MediaWiki\Extension\PageTriage\OresMetadata; |
10 | use MediaWiki\Extension\PageTriage\PageTriageUtil; |
11 | use MediaWiki\Linker\LinksMigration; |
12 | use MediaWiki\Logger\LoggerFactory; |
13 | use MediaWiki\Page\RedirectLookup; |
14 | use MediaWiki\SpecialPage\SpecialPage; |
15 | use MediaWiki\Title\Title; |
16 | use MediaWiki\Title\TitleFormatter; |
17 | use MediaWiki\User\UserFactory; |
18 | use ORES\Services\ORESServices; |
19 | use Wikimedia\ParamValidator\ParamValidator; |
20 | use Wikimedia\ParamValidator\TypeDef\IntegerDef; |
21 | use Wikimedia\Rdbms\IExpression; |
22 | use Wikimedia\Rdbms\OrExpressionGroup; |
23 | |
24 | /** |
25 | * API module to generate a list of pages to triage |
26 | * |
27 | * @ingroup API |
28 | * @ingroup Extensions |
29 | */ |
30 | class 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 | } |