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