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