Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
23.30% |
72 / 309 |
|
15.79% |
3 / 19 |
CRAP | |
0.00% |
0 / 1 |
UncachedMenteeOverviewDataProvider | |
23.30% |
72 / 309 |
|
15.79% |
3 / 19 |
1087.57 | |
0.00% |
0 / 1 |
__construct | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
2 | |||
getReadConnection | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
resetService | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
getProfilingInfo | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
storeProfilingData | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getIds | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
2 | |||
getLockedMenteesIds | |
0.00% |
0 / 20 |
|
0.00% |
0 / 1 |
20 | |||
getFilteredMenteesForMentor | |
0.00% |
0 / 78 |
|
0.00% |
0 / 1 |
42 | |||
getLastEditTimestampForUsersInternal | |
0.00% |
0 / 22 |
|
0.00% |
0 / 1 |
6 | |||
getLastEditTimestampForUsers | |
0.00% |
0 / 9 |
|
0.00% |
0 / 1 |
12 | |||
filterMenteesByLastEdit | |
0.00% |
0 / 13 |
|
0.00% |
0 / 1 |
20 | |||
getFormattedDataForMentor | |
0.00% |
0 / 28 |
|
0.00% |
0 / 1 |
42 | |||
getUsernames | |
100.00% |
12 / 12 |
|
100.00% |
1 / 1 |
2 | |||
getRevertedEditsForUsers | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
2 | |||
getQuestionsAskedForUsers | |
0.00% |
0 / 10 |
|
0.00% |
0 / 1 |
2 | |||
getTaggedEditsForUsers | |
0.00% |
0 / 34 |
|
0.00% |
0 / 1 |
30 | |||
getRegistrationTimestampForUsers | |
100.00% |
12 / 12 |
|
100.00% |
1 / 1 |
2 | |||
getBlocksForUsers | |
97.30% |
36 / 37 |
|
0.00% |
0 / 1 |
4 | |||
getEditCountsForUsers | |
100.00% |
12 / 12 |
|
100.00% |
1 / 1 |
2 |
1 | <?php |
2 | |
3 | namespace GrowthExperiments\MentorDashboard\MenteeOverview; |
4 | |
5 | use ChangeTags; |
6 | use GrowthExperiments\HomepageHooks; |
7 | use GrowthExperiments\HomepageModules\Mentorship; |
8 | use GrowthExperiments\Mentorship\MentorPageMentorManager; |
9 | use GrowthExperiments\Mentorship\Store\MentorStore; |
10 | use MediaWiki\Extension\CentralAuth\CentralAuthServices; |
11 | use MediaWiki\Registration\ExtensionRegistry; |
12 | use MediaWiki\Storage\NameTableAccessException; |
13 | use MediaWiki\Storage\NameTableStore; |
14 | use MediaWiki\User\ActorMigration; |
15 | use MediaWiki\User\TempUser\TempUserConfig; |
16 | use MediaWiki\User\UserIdentity; |
17 | use MediaWiki\User\UserIdentityLookup; |
18 | use Psr\Log\LoggerAwareTrait; |
19 | use Psr\Log\NullLogger; |
20 | use Wikimedia\Rdbms\IConnectionProvider; |
21 | use Wikimedia\Rdbms\IExpression; |
22 | use Wikimedia\Rdbms\IReadableDatabase; |
23 | use Wikimedia\Timestamp\ConvertibleTimestamp; |
24 | |
25 | /** |
26 | * Provides data about interesting mentees mentored by a particular mentor |
27 | * |
28 | * WARNING: This class implements no caching, and it may be only used from CLI |
29 | * scripts or jobs. |
30 | */ |
31 | class UncachedMenteeOverviewDataProvider implements MenteeOverviewDataProvider { |
32 | use LoggerAwareTrait; |
33 | |
34 | /** @var int Number of seconds in a day */ |
35 | private const SECONDS_DAY = 86400; |
36 | |
37 | private MentorStore $mentorStore; |
38 | |
39 | private NameTableStore $changeTagDefStore; |
40 | |
41 | private ActorMigration $actorMigration; |
42 | |
43 | private UserIdentityLookup $userIdentityLookup; |
44 | private TempUserConfig $tempUserConfig; |
45 | |
46 | private IConnectionProvider $mainConnProvider; |
47 | |
48 | /** @var array Cache used by getLastEditTimestampForUsers */ |
49 | private $lastTimestampCache = []; |
50 | |
51 | /** |
52 | * @var array Profiling information |
53 | * |
54 | * Stored by storeProfilingData, can be printed from |
55 | * updateMenteeData.php maintenance script. |
56 | */ |
57 | private $profilingInfo = []; |
58 | |
59 | /** |
60 | * @param MentorStore $mentorStore |
61 | * @param NameTableStore $changeTagDefStore |
62 | * @param ActorMigration $actorMigration |
63 | * @param UserIdentityLookup $userIdentityLookup |
64 | * @param TempUserConfig $tempUserConfig |
65 | * @param IConnectionProvider $mainConnProvider |
66 | */ |
67 | public function __construct( |
68 | MentorStore $mentorStore, |
69 | NameTableStore $changeTagDefStore, |
70 | ActorMigration $actorMigration, |
71 | UserIdentityLookup $userIdentityLookup, |
72 | TempUserConfig $tempUserConfig, |
73 | IConnectionProvider $mainConnProvider |
74 | ) { |
75 | $this->setLogger( new NullLogger() ); |
76 | |
77 | $this->mentorStore = $mentorStore; |
78 | $this->changeTagDefStore = $changeTagDefStore; |
79 | $this->actorMigration = $actorMigration; |
80 | $this->userIdentityLookup = $userIdentityLookup; |
81 | $this->tempUserConfig = $tempUserConfig; |
82 | $this->mainConnProvider = $mainConnProvider; |
83 | } |
84 | |
85 | private function getReadConnection(): IReadableDatabase { |
86 | return $this->mainConnProvider->getReplicaDatabase( false, 'vslow' ); |
87 | } |
88 | |
89 | /** |
90 | * Do stuff that needs to happen before calculating the data |
91 | */ |
92 | private function resetService(): void { |
93 | $this->lastTimestampCache = []; |
94 | $this->profilingInfo = []; |
95 | } |
96 | |
97 | /** |
98 | * Get profiling information |
99 | * |
100 | * @internal Only use from updateMenteeData.php |
101 | * @return array |
102 | */ |
103 | public function getProfilingInfo(): array { |
104 | return $this->profilingInfo; |
105 | } |
106 | |
107 | /** |
108 | * @param string $section |
109 | * @param float $seconds |
110 | */ |
111 | private function storeProfilingData( string $section, float $seconds ): void { |
112 | $this->profilingInfo[$section] = $seconds; |
113 | } |
114 | |
115 | /** |
116 | * @param UserIdentity[] $users |
117 | * @return int[] |
118 | */ |
119 | private function getIds( array $users ): array { |
120 | return array_map( static function ( $user ) { |
121 | return $user->getId(); |
122 | }, $users ); |
123 | } |
124 | |
125 | /** |
126 | * Return local user IDs of all globally locked mentees |
127 | * |
128 | * If CentralAuth is not installed, this returns an empty array (as |
129 | * locking is not possible in that case). |
130 | * |
131 | * If CentralAuth is available, CentralAuthServices::getGlobalUserSelectQueryBuilderFactory |
132 | * is used to get locked mentees only. The select query builder does not impose any explicit |
133 | * limit on number of users that can be processed at once. UncachedMenteeOverviewDataProvider |
134 | * runs certain queries similar to the CentralAuth extension, which run fine with the current |
135 | * number of mentees (as of March 2022, the upper bound is 42,716). |
136 | * |
137 | * @param UserIdentity[] $mentees |
138 | * @return int[] |
139 | */ |
140 | private function getLockedMenteesIds( array $mentees ): array { |
141 | if ( |
142 | !ExtensionRegistry::getInstance()->isLoaded( 'CentralAuth' ) || |
143 | $mentees === [] |
144 | ) { |
145 | return []; |
146 | } |
147 | |
148 | if ( |
149 | !method_exists( CentralAuthServices::class, 'getGlobalUserSelectQueryBuilderFactory' ) |
150 | ) { |
151 | $this->logger->error( |
152 | 'Old version of CentralAuth found, CentralAuthServices::getGlobalUserSelectQueryBuilderFactory' . |
153 | ' was not found' |
154 | ); |
155 | return []; |
156 | } |
157 | |
158 | $userIdentities = CentralAuthServices::getGlobalUserSelectQueryBuilderFactory() |
159 | ->newGlobalUserSelectQueryBuilder() |
160 | ->whereUserNames( array_map( static function ( UserIdentity $user ) { |
161 | return $user->getName(); |
162 | }, $mentees ) ) |
163 | ->whereLocked( true ) |
164 | ->caller( __METHOD__ ) |
165 | ->fetchLocalUserIdentitites(); |
166 | |
167 | return array_map( static function ( UserIdentity $user ) { |
168 | return $user->getId(); |
169 | }, iterator_to_array( $userIdentities ) ); |
170 | } |
171 | |
172 | /** |
173 | * Filter mentees according to business rules |
174 | * |
175 | * Only mentees that meet all of the following conditions |
176 | * should be considered: |
177 | * * user is not a bot |
178 | * * user is not a temporary account (safety check should temp users end up as mentees) |
179 | * * user is not indefinitely blocked |
180 | * * user is not globally locked (via CentralAuth's implementation) |
181 | * * user registered less than 2 weeks ago OR made at least one edit in the last 6 months |
182 | * |
183 | * @param UserIdentity $mentor |
184 | * @return int[] User IDs of the mentees |
185 | */ |
186 | private function getFilteredMenteesForMentor( UserIdentity $mentor ): array { |
187 | $startTime = microtime( true ); |
188 | |
189 | $mentees = $this->mentorStore->getMenteesByMentor( $mentor, MentorStore::ROLE_PRIMARY ); |
190 | $menteeIds = array_diff( |
191 | $this->getIds( $mentees ), |
192 | $this->getLockedMenteesIds( $mentees ) |
193 | ); |
194 | |
195 | if ( $menteeIds === [] ) { |
196 | return []; |
197 | } |
198 | |
199 | $dbr = $this->getReadConnection(); |
200 | |
201 | $queryBuilder = $dbr->newSelectQueryBuilder() |
202 | ->select( [ |
203 | 'user_id', |
204 | 'has_edits' => 'user_editcount > 0' |
205 | ] ) |
206 | ->from( 'user' ) |
207 | ->where( [ |
208 | // filter to mentees only |
209 | 'user_id' => $menteeIds, |
210 | |
211 | // ensure mentees have homepage enabled |
212 | 'user_id IN (' . $dbr->newSelectQueryBuilder() |
213 | ->select( 'up_user' ) |
214 | ->from( 'user_properties' ) |
215 | ->where( [ |
216 | 'up_property' => HomepageHooks::HOMEPAGE_PREF_ENABLE, |
217 | 'up_value' => '1', |
218 | ] ) |
219 | ->getSQL() . |
220 | ')', |
221 | |
222 | // ensure mentees do not have mentorship disabled |
223 | 'user_id NOT IN (' . $dbr->newSelectQueryBuilder() |
224 | ->select( 'up_user' ) |
225 | ->from( 'user_properties' ) |
226 | ->where( [ |
227 | 'up_property' => MentorPageMentorManager::MENTORSHIP_ENABLED_PREF, |
228 | // sanity check, should never match (1 is the default value) |
229 | $dbr->expr( 'up_value', '!=', '1' ), |
230 | ] ) |
231 | ->getSQL() . |
232 | ')', |
233 | |
234 | // user is not a bot, |
235 | 'user_id NOT IN (' . $dbr->newSelectQueryBuilder() |
236 | ->select( 'ug_user' ) |
237 | ->from( 'user_groups' ) |
238 | ->where( [ 'ug_group' => 'bot' ] ) |
239 | ->getSQL() . |
240 | ')', |
241 | |
242 | // only users who either made an edit or registered less than 2 weeks ago |
243 | $dbr->expr( 'user_editcount', '>', 0 ) |
244 | ->or( 'user_registration', '>', $dbr->timestamp( |
245 | (int)wfTimestamp( TS_UNIX ) - 2 * 7 * self::SECONDS_DAY |
246 | ) ), |
247 | ] ) |
248 | ->caller( __METHOD__ ); |
249 | |
250 | // Exclude users which are indefinitely blocked |
251 | $queryBuilder->andWhere( |
252 | 'user_id NOT IN (' . $dbr->newSelectQueryBuilder() |
253 | ->select( 'bt_user' ) |
254 | ->from( 'block' ) |
255 | ->join( 'block_target', null, [ |
256 | 'bt_id=bl_target' |
257 | ] ) |
258 | ->where( [ |
259 | 'bl_expiry' => $dbr->getInfinity(), |
260 | // not an IP block |
261 | $dbr->expr( 'bt_user', '!=', null ) |
262 | ] ) |
263 | ->getSQL() . |
264 | ')' |
265 | ); |
266 | |
267 | // exclude temporary accounts, if enabled (T341389) |
268 | if ( $this->tempUserConfig->isKnown() ) { |
269 | foreach ( $this->tempUserConfig->getMatchPatterns() as $pattern ) { |
270 | $queryBuilder->andWhere( |
271 | $dbr->expr( 'user_name', IExpression::NOT_LIKE, $pattern->toLikeValue( $dbr ) ) |
272 | ); |
273 | } |
274 | } |
275 | |
276 | $res = $queryBuilder->fetchResultSet(); |
277 | |
278 | $editingUsers = []; |
279 | $notEditingUsers = []; |
280 | foreach ( $res as $row ) { |
281 | if ( $row->has_edits ) { |
282 | $editingUsers[] = (int)$row->user_id; |
283 | } else { |
284 | $notEditingUsers[] = (int)$row->user_id; |
285 | } |
286 | } |
287 | |
288 | $this->storeProfilingData( 'filtermentees', microtime( true ) - $startTime ); |
289 | |
290 | return array_merge( |
291 | $notEditingUsers, |
292 | $this->filterMenteesByLastEdit( $editingUsers ) |
293 | ); |
294 | } |
295 | |
296 | /** |
297 | * @param int[] $userIds |
298 | * @return array |
299 | */ |
300 | private function getLastEditTimestampForUsersInternal( array $userIds ): array { |
301 | $startTime = microtime( true ); |
302 | |
303 | $rows = $this->getReadConnection()->newSelectQueryBuilder() |
304 | ->select( [ |
305 | 'actor_user', |
306 | 'last_edit' => 'MAX(rev_timestamp)' |
307 | ] ) |
308 | ->from( 'revision' ) |
309 | ->join( 'actor', null, 'rev_actor = actor_id' ) |
310 | ->where( [ |
311 | 'actor_user' => $userIds, |
312 | ] ) |
313 | ->caller( __METHOD__ ) |
314 | ->groupBy( 'actor_user' ) |
315 | ->fetchResultSet(); |
316 | $res = []; |
317 | foreach ( $rows as $row ) { |
318 | $res[$row->actor_user] = $row->last_edit; |
319 | } |
320 | |
321 | $this->storeProfilingData( |
322 | 'edittimestampinternal', |
323 | microtime( true ) - $startTime |
324 | ); |
325 | return $res; |
326 | } |
327 | |
328 | /** |
329 | * @param int[] $userIds |
330 | * @return array |
331 | */ |
332 | private function getLastEditTimestampForUsers( array $userIds ): array { |
333 | if ( $userIds === [] ) { |
334 | return []; |
335 | } |
336 | |
337 | $data = array_intersect_key( $this->lastTimestampCache, array_fill_keys( $userIds, true ) ); |
338 | $notInCache = array_diff( $userIds, array_keys( $this->lastTimestampCache ) ); |
339 | if ( $notInCache ) { |
340 | $new = $this->getLastEditTimestampForUsersInternal( $notInCache ); |
341 | $data += $new; |
342 | $this->lastTimestampCache += $new; |
343 | } |
344 | return $data; |
345 | } |
346 | |
347 | /** |
348 | * Filter provided user IDs to IDs of users who edited up to 6 months ago |
349 | * |
350 | * @param array $allUserIds |
351 | * @return int[] |
352 | */ |
353 | private function filterMenteesByLastEdit( array $allUserIds ): array { |
354 | if ( $allUserIds === [] ) { |
355 | return []; |
356 | } |
357 | |
358 | $allLastEdits = $this->getLastEditTimestampForUsers( $allUserIds ); |
359 | $userIds = []; |
360 | foreach ( $allLastEdits as $userId => $lastEdit ) { |
361 | $secondsSinceLastEdit = (int)wfTimestamp( TS_UNIX ) - |
362 | (int)ConvertibleTimestamp::convert( |
363 | TS_UNIX, |
364 | $lastEdit |
365 | ); |
366 | if ( $secondsSinceLastEdit <= self::SECONDS_DAY * 6 * 30 ) { |
367 | $userIds[] = $userId; |
368 | } |
369 | } |
370 | return $userIds; |
371 | } |
372 | |
373 | /** |
374 | * Calculates data for a given mentor's mentees |
375 | * |
376 | * @param UserIdentity $mentor |
377 | * @return array |
378 | */ |
379 | public function getFormattedDataForMentor( UserIdentity $mentor ): array { |
380 | $this->resetService(); |
381 | |
382 | $userIds = $this->getFilteredMenteesForMentor( $mentor ); |
383 | if ( $userIds === [] ) { |
384 | return []; |
385 | } |
386 | |
387 | $mainData = [ |
388 | 'username' => $this->getUsernames( $userIds ), |
389 | 'reverted' => $this->getRevertedEditsForUsers( $userIds ), |
390 | 'questions' => $this->getQuestionsAskedForUsers( $userIds ), |
391 | 'editcount' => $this->getEditCountsForUsers( $userIds ), |
392 | 'registration' => $this->getRegistrationTimestampForUsers( $userIds ), |
393 | 'last_edit' => $this->getLastEditTimestampForUsers( $userIds ), |
394 | 'blocks' => $this->getBlocksForUsers( $userIds ), |
395 | ]; |
396 | |
397 | $res = []; |
398 | foreach ( $mainData as $key => $data ) { |
399 | foreach ( $data as $userId => $value ) { |
400 | $res[$userId][$key] = $value; |
401 | } |
402 | } |
403 | foreach ( $res as $userId => $userData ) { |
404 | $res[$userId]['last_active'] = $userData['last_edit'] ?? $userData['registration'] ?? null; |
405 | if ( $res[$userId]['last_active'] === null ) { |
406 | $this->logger->error( |
407 | __METHOD__ . ': Registration and last_edit timestamps not found for user ID {userId}', |
408 | [ |
409 | 'userId' => $userId, |
410 | 'exception' => new \RuntimeException |
411 | ] |
412 | ); |
413 | } |
414 | } |
415 | return $res; |
416 | } |
417 | |
418 | /** |
419 | * @param int[] $userIds |
420 | * @return array |
421 | */ |
422 | private function getUsernames( array $userIds ): array { |
423 | $startTime = microtime( true ); |
424 | |
425 | $rows = $this->getReadConnection()->newSelectQueryBuilder() |
426 | ->select( [ 'user_id', 'user_name' ] ) |
427 | ->from( 'user' ) |
428 | ->where( [ 'user_id' => $userIds ] ) |
429 | ->caller( __METHOD__ ) |
430 | ->fetchResultSet(); |
431 | $res = []; |
432 | foreach ( $rows as $row ) { |
433 | $res[$row->user_id] = $row->user_name; |
434 | } |
435 | |
436 | $this->storeProfilingData( 'usernames', microtime( true ) - $startTime ); |
437 | |
438 | return $res; |
439 | } |
440 | |
441 | /** |
442 | * @param int[] $userIds |
443 | * @return array |
444 | */ |
445 | private function getRevertedEditsForUsers( array $userIds ): array { |
446 | $startTime = microtime( true ); |
447 | $res = $this->getTaggedEditsForUsers( |
448 | [ ChangeTags::TAG_REVERTED ], |
449 | $userIds |
450 | ); |
451 | $this->storeProfilingData( 'reverted', microtime( true ) - $startTime ); |
452 | return $res; |
453 | } |
454 | |
455 | /** |
456 | * @param int[] $userIds |
457 | * @return array |
458 | */ |
459 | private function getQuestionsAskedForUsers( array $userIds ): array { |
460 | $startTime = microtime( true ); |
461 | $res = $this->getTaggedEditsForUsers( |
462 | [ |
463 | Mentorship::MENTORSHIP_HELPPANEL_QUESTION_TAG, |
464 | Mentorship::MENTORSHIP_MODULE_QUESTION_TAG |
465 | ], |
466 | $userIds |
467 | ); |
468 | $this->storeProfilingData( 'questions', microtime( true ) - $startTime ); |
469 | return $res; |
470 | } |
471 | |
472 | /** |
473 | * @param string[] $tags |
474 | * @param int[] $userIds |
475 | * @return int[] |
476 | */ |
477 | private function getTaggedEditsForUsers( array $tags, array $userIds ) { |
478 | $tagIds = []; |
479 | foreach ( $tags as $tag ) { |
480 | try { |
481 | $tagIds[] = $this->changeTagDefStore->getId( $tag ); |
482 | } catch ( NameTableAccessException $e ) { |
483 | // Skip non-existing tags gracefully |
484 | } |
485 | } |
486 | if ( $tagIds === [] ) { |
487 | return array_fill_keys( $userIds, 0 ); |
488 | } |
489 | |
490 | $dbr = $this->getReadConnection(); |
491 | $queryInfo = $this->actorMigration->getJoin( 'rev_user' ); |
492 | $taggedEditsSubquery = $dbr->newSelectQueryBuilder() |
493 | ->select( [ |
494 | 'rev_user' => $queryInfo['fields']['rev_user'], |
495 | 'ct_rev_id' |
496 | ] ) |
497 | ->from( 'change_tag' ) |
498 | ->join( 'revision', null, 'rev_id=ct_rev_id' ) |
499 | ->tables( $queryInfo['tables'] ) |
500 | ->where( [ |
501 | 'actor_user' => $userIds, |
502 | 'ct_tag_id' => $tagIds |
503 | ] ) |
504 | ->joinConds( $queryInfo['joins'] ) |
505 | ->caller( __METHOD__ ); |
506 | $rows = $dbr->newSelectQueryBuilder() |
507 | ->select( [ 'user_id', 'tagged' => 'COUNT(ct_rev_id)' ] ) |
508 | ->from( 'user' ) |
509 | ->leftJoin( $taggedEditsSubquery, 'tagged_edits', 'rev_user=user_id' ) |
510 | ->where( [ 'user_id' => $userIds ] ) |
511 | ->caller( __METHOD__ ) |
512 | ->groupBy( 'user_id' ) |
513 | ->fetchResultSet(); |
514 | |
515 | $res = []; |
516 | foreach ( $rows as $row ) { |
517 | $res[$row->user_id] = (int)$row->tagged; |
518 | } |
519 | return $res; |
520 | } |
521 | |
522 | /** |
523 | * @param int[] $userIds |
524 | * @return array |
525 | */ |
526 | private function getRegistrationTimestampForUsers( array $userIds ): array { |
527 | $startTime = microtime( true ); |
528 | $rows = $this->getReadConnection()->newSelectQueryBuilder() |
529 | ->select( [ 'user_id', 'user_registration' ] ) |
530 | ->from( 'user' ) |
531 | ->where( [ 'user_id' => $userIds ] ) |
532 | ->caller( __METHOD__ ) |
533 | ->fetchResultSet(); |
534 | $res = []; |
535 | foreach ( $rows as $row ) { |
536 | $res[$row->user_id] = $row->user_registration; |
537 | } |
538 | $this->storeProfilingData( 'registration', microtime( true ) - $startTime ); |
539 | return $res; |
540 | } |
541 | |
542 | /** |
543 | * Get number of blocks placed against the mentees |
544 | * |
545 | * @param int[] $userIds |
546 | * @return int[] |
547 | */ |
548 | private function getBlocksForUsers( array $userIds ): array { |
549 | if ( $userIds === [] ) { |
550 | return []; |
551 | } |
552 | |
553 | $startTime = microtime( true ); |
554 | |
555 | // fetch usernames (assoc. array; username => user ID) |
556 | // NOTE: username has underscores, not spaces |
557 | $users = []; |
558 | $userNamesAsStrings = []; |
559 | '@phan-var array<string> $userNamesAsStrings'; |
560 | $userIdentities = iterator_to_array( $this->userIdentityLookup->newSelectQueryBuilder() |
561 | ->whereUserIds( $userIds ) |
562 | ->caller( __METHOD__ ) |
563 | ->fetchUserIdentities() ); |
564 | array_walk( |
565 | $userIdentities, |
566 | static function ( UserIdentity $value, $key ) use ( &$users, &$userNamesAsStrings ) { |
567 | $username = str_replace( ' ', '_', $value->getName() ); |
568 | $userNamesAsStrings[] = $username; |
569 | $users[$username] = $value->getId(); |
570 | } |
571 | ); |
572 | |
573 | $rows = $this->getReadConnection()->newSelectQueryBuilder() |
574 | ->select( [ 'log_title', 'blocks' => 'COUNT(log_id)' ] ) |
575 | ->from( 'logging' ) |
576 | ->where( [ |
577 | 'log_type' => 'block', |
578 | 'log_action' => 'block', |
579 | 'log_namespace' => NS_USER, |
580 | 'log_title' => $userNamesAsStrings |
581 | ] ) |
582 | ->groupBy( 'log_title' ) |
583 | ->caller( __METHOD__ ) |
584 | ->fetchResultSet(); |
585 | |
586 | $res = []; |
587 | foreach ( $rows as $row ) { |
588 | $res[$users[$row->log_title]] = (int)$row->blocks; |
589 | } |
590 | |
591 | // fill missing IDs with zeros |
592 | $missingIds = array_diff( $userIds, array_keys( $res ) ); |
593 | foreach ( $missingIds as $id ) { |
594 | $res[$id] = 0; |
595 | } |
596 | |
597 | $this->storeProfilingData( 'blocks', microtime( true ) - $startTime ); |
598 | |
599 | return $res; |
600 | } |
601 | |
602 | /** |
603 | * @param int[] $userIds |
604 | * @return int[] |
605 | */ |
606 | private function getEditCountsForUsers( array $userIds ): array { |
607 | $startTime = microtime( true ); |
608 | |
609 | $rows = $this->getReadConnection()->newSelectQueryBuilder() |
610 | ->select( [ 'user_id', 'user_editcount' ] ) |
611 | ->from( 'user' ) |
612 | ->where( [ 'user_id' => $userIds ] ) |
613 | ->caller( __METHOD__ ) |
614 | ->fetchResultSet(); |
615 | $res = []; |
616 | foreach ( $rows as $row ) { |
617 | $res[$row->user_id] = (int)$row->user_editcount; |
618 | } |
619 | |
620 | $this->storeProfilingData( 'editcount', microtime( true ) - $startTime ); |
621 | |
622 | return $res; |
623 | } |
624 | } |